diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 370a43fb46..2645d535d8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,7 +14,6 @@ # contracts /contracts/mixnet @durch @jstuczyn /contracts/vesting @durch @jstuczyn -/contracts/service-provider-directory @octol # crypto code /common/crypto/ @jstuczyn @@ -22,14 +21,5 @@ /common/dkg/ @jstuczyn /common/nymsphinx/ @jstuczyn -# rust sdk -/sdk/rust/ @octol - -# nym-connect (rust) -/nym-connect/desktop/src-tauri/ @octol - -# nym-wallet (rust) -/nym-wallet/src-tauri/ @octol - # documentation -/documentation @mfahampshire \ No newline at end of file +/documentation @mfahampshire diff --git a/.github/actions/nym-hash-releases/src/package-lock.json b/.github/actions/nym-hash-releases/src/package-lock.json index 2560602d49..618d9896c7 100644 --- a/.github/actions/nym-hash-releases/src/package-lock.json +++ b/.github/actions/nym-hash-releases/src/package-lock.json @@ -415,9 +415,9 @@ } }, "node_modules/undici": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", - "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 839c34c714..37d85d98d0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -31,5 +31,3 @@ updates: update-types: - "patch" open-pull-requests-limit: 10 - assignees: - - "octol" diff --git a/.github/workflows/cd-docs.yml b/.github/workflows/cd-docs.yml index db46b85911..cd7c0891aa 100644 --- a/.github/workflows/cd-docs.yml +++ b/.github/workflows/cd-docs.yml @@ -21,7 +21,7 @@ jobs: run: sudo apt-get install -y rsync - uses: rlespinasse/github-slug-action@v3.x - name: Setup pnpm - uses: pnpm/action-setup@v4.0.0 + uses: pnpm/action-setup@v4.1.0 with: version: 9 - uses: actions/setup-node@v4 diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index 7df2cd4ad4..3986f1ddae 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -38,15 +38,14 @@ jobs: rm -rf ci-builds || true mkdir -p $OUTPUT_DIR echo $OUTPUT_DIR - - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get -y install libudev-dev - name: Sets env vars for tokio if set in manual dispatch inputs - run: | - echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true - + run: | + echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV + echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV - name: Install Rust stable uses: actions-rs/toolchain@v1 with: @@ -103,7 +102,6 @@ jobs: if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then cp target/debian/*.deb $OUTPUT_DIR fi - - name: Deploy branch to CI www continue-on-error: true uses: easingthemes/ssh-deploy@main diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 68ba52e81e..cd3cfb848b 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -5,7 +5,6 @@ on: paths: - 'clients/**' - 'common/**' - - 'explorer-api/**' - 'gateway/**' - 'integrations/**' - 'nym-api/**' @@ -13,6 +12,7 @@ on: - 'nym-network-monitor/**' - 'nym-node/**' - 'nym-node-status-api/**' + - 'nym-statistics-api/**' - 'nym-outfox/**' - 'nym-validator-rewarder/**' - 'nyx-chain-watcher/**' @@ -38,7 +38,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ] + os: [ arc-linux-latest, custom-windows-11, custom-macos-15 ] runs-on: ${{ matrix.os }} env: CARGO_TERM_COLOR: always @@ -46,9 +46,9 @@ jobs: RUSTUP_PERMIT_COPY_RENAME: 1 steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler + run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler cmake continue-on-error: true - if: contains(matrix.os, 'ubuntu') + if: contains(matrix.os, 'linux') - name: Check out repository code uses: actions/checkout@v4 @@ -63,7 +63,7 @@ jobs: # To avoid running out of disk space, skip generating debug symbols - name: Set debug to false (unix) - if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac') + if: contains(matrix.os, 'linux') || contains(matrix.os, 'mac') run: | sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml git diff @@ -93,14 +93,14 @@ jobs: command: build - name: Build all examples - if: contains(matrix.os, 'ubuntu') + if: contains(matrix.os, 'linux') uses: actions-rs/cargo@v1 with: command: build args: --workspace --examples - name: Run all tests - if: contains(matrix.os, 'ubuntu') + if: contains(matrix.os, 'linux') uses: actions-rs/cargo@v1 env: NYM_API: https://sandbox-nym-api1.nymtech.net/api @@ -109,7 +109,7 @@ jobs: args: --workspace - name: Run expensive tests - if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'ubuntu') + if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'linux') uses: actions-rs/cargo@v1 with: command: test diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index 7ea704d703..6eb6508899 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml @@ -44,8 +44,10 @@ jobs: echo "Tag is empty" exit 1 fi + # first, list all tags for logging purposes curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq - exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' ) + # check if there's a matching tag + exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' ) if [[ $exists = "true" ]]; then echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" exit 1 @@ -53,5 +55,5 @@ jobs: echo "Version '$TAG' doesn't exist on the remote" else echo "Unknown output '$exists'" - exit 1 + exit 2 fi diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml new file mode 100644 index 0000000000..cfdddf2b25 --- /dev/null +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -0,0 +1,59 @@ +name: ci-check-nym-stats-api-version + +on: + pull_request: + paths: + - "nym-statistics-api/**" + +env: + WORKING_DIRECTORY: "nym-statistics-api" + +jobs: + check-if-tag-exists: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.47.1 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if git tag exists + run: | + TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + if [[ -z "$TAG" ]]; then + echo "Tag is empty" + exit 1 + fi + git ls-remote --tags origin | awk '{print $2}' + if git ls-remote --tags origin | awk '{print $2}' | grep -q "refs/tags/$TAG$" ; then + echo "Tag '$TAG' ALREADY EXISTS on the remote" + exit 1 + else + echo "Tag '$TAG' does not exist on the remote" + fi + - name: Check if harbor tag exists + run: | + TAG=${{ steps.get_version.outputs.result }} + registry=https://harbor.nymte.ch + repo_name=nym/nym-statistics-api + if [[ -z $TAG ]]; then + echo "Tag is empty" + exit 1 + fi + # first, list all tags for logging purposes + curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq + # check if there's a matching tag + exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' ) + if [[ $exists = "true" ]]; then + echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" + exit 1 + elif [[ $exists = "false" ]]; then + echo "Version '$TAG' doesn't exist on the remote" + else + echo "Unknown output '$exists'" + exit 2 + fi diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index b3acd7f0ac..98c41478a8 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -31,31 +31,26 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: + toolchain: stable target: wasm32-unknown-unknown override: true - - name: Install wasm-opt - uses: ./.github/actions/install-wasm-opt - with: - version: '114' - - name: Install cosmwasm-check run: cargo install cosmwasm-check - name: Build release contracts - run: make contracts + run: make publish-contracts - name: Prepare build output shell: bash env: OUTPUT_DIR: ci-contract-builds/${{ github.ref_name }} run: | - cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR - cp contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm $OUTPUT_DIR - cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR - cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR - cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR - cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR + find contracts/artifacts -maxdepth 1 -type f -name '*.wasm' -exec cp {} $OUTPUT_DIR \; + # Also include the optimizer-generated checksums if present + if [ -f contracts/artifacts/checksums.txt ]; then + cp contracts/artifacts/checksums.txt $OUTPUT_DIR + fi - name: Deploy branch to CI www continue-on-error: true diff --git a/.github/workflows/ci-contracts.yml b/.github/workflows/ci-contracts.yml index 2814fd0a26..35a4fb4f15 100644 --- a/.github/workflows/ci-contracts.yml +++ b/.github/workflows/ci-contracts.yml @@ -20,6 +20,7 @@ jobs: runs-on: ubuntu-22.04 env: CARGO_TERM_COLOR: always + RUSTUP_PERMIT_COPY_RENAME: 1 steps: - uses: actions/checkout@v4 @@ -27,7 +28,8 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + # pinned due to issues building contracts + toolchain: 1.86.0 target: wasm32-unknown-unknown override: true components: rustfmt, clippy diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index a7e3e13946..2e41593dcb 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -28,7 +28,7 @@ jobs: run: sudo apt-get install -y rsync - uses: rlespinasse/github-slug-action@v3.x - name: Setup pnpm - uses: pnpm/action-setup@v4.0.0 + uses: pnpm/action-setup@v4.1.0 with: version: 9 - uses: actions/setup-node@v4 diff --git a/.github/workflows/ci-sonar.yml b/.github/workflows/ci-sonar.yml new file mode 100644 index 0000000000..a06986dfcd --- /dev/null +++ b/.github/workflows/ci-sonar.yml @@ -0,0 +1,19 @@ +name: Run SonarQube Scan +on: + push: + branches: + - develop +# pull_request: +# types: [opened, synchronize, reopened] +jobs: + sonarqube: + name: SonarQube + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 6b31063c73..de25d37594 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -6,7 +6,7 @@ jobs: greeting: runs-on: ubuntu-latest steps: - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: 'Thank you for raising this issue' diff --git a/.github/workflows/nightly-security-audit.yml b/.github/workflows/nightly-security-audit.yml index db975e2384..aeb9fa08c5 100644 --- a/.github/workflows/nightly-security-audit.yml +++ b/.github/workflows/nightly-security-audit.yml @@ -31,7 +31,7 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - name: Download report from previous job - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: report path: .github/workflows/support-files/notifications diff --git a/.github/workflows/nym-api-integration-tests.yml b/.github/workflows/nym-api-integration-tests.yml index ce6e1b9470..9d0bf4a660 100644 --- a/.github/workflows/nym-api-integration-tests.yml +++ b/.github/workflows/nym-api-integration-tests.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Rust uses: actions-rs/toolchain@v1 diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index c1d7c7cffb..5ceb1477b2 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -20,8 +20,10 @@ jobs: strategy: fail-fast: false matrix: - platform: [custom-ubuntu-22.04] - runs-on: ${{ matrix.platform }} + include: + - os: arc-ubuntu-22.04 + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.os }} outputs: release_id: ${{ steps.create-release.outputs.id }} @@ -54,7 +56,7 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: 1.86.0 override: true - name: Build all binaries @@ -68,7 +70,6 @@ jobs: with: name: my-artifact path: | - target/release/explorer-api target/release/nym-client target/release/nym-socks5-client target/release/nym-api @@ -77,14 +78,13 @@ jobs: target/release/nymvisor target/release/nym-node retention-days: 30 - + - id: create-release name: Upload to release based on tag name - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 if: github.event_name == 'release' with: files: | - target/release/explorer-api target/release/nym-client target/release/nym-socks5-client target/release/nym-api diff --git a/.github/workflows/publish-nyms5-android-apk.yml b/.github/workflows/publish-nyms5-android-apk.yml index 2bdaf2b762..9302f3402c 100644 --- a/.github/workflows/publish-nyms5-android-apk.yml +++ b/.github/workflows/publish-nyms5-android-apk.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v4 - name: Install Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: "temurin" java-version: "17" @@ -91,7 +91,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Download binary artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: nyms5-apk-arch64 path: apk diff --git a/.github/workflows/push-credential-proxy.yaml b/.github/workflows/push-credential-proxy.yaml index 686460ad9f..8b433b1d62 100644 --- a/.github/workflows/push-credential-proxy.yaml +++ b/.github/workflows/push-credential-proxy.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml diff --git a/.github/workflows/push-data-observatory.yaml b/.github/workflows/push-data-observatory.yaml index ec4614527f..bd999f45d5 100644 --- a/.github/workflows/push-data-observatory.yaml +++ b/.github/workflows/push-data-observatory.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-network-monitor.yaml b/.github/workflows/push-network-monitor.yaml index 89d2f16265..8baa371411 100644 --- a/.github/workflows/push-network-monitor.yaml +++ b/.github/workflows/push-network-monitor.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml diff --git a/.github/workflows/push-node-status-agent.yaml b/.github/workflows/push-node-status-agent.yaml index ada21da28c..00c8cbc774 100644 --- a/.github/workflows/push-node-status-agent.yaml +++ b/.github/workflows/push-node-status-agent.yaml @@ -5,8 +5,15 @@ on: inputs: gateway_probe_git_ref: type: string + default: nym-vpn-core-v1.4.0 + required: true description: Which gateway probe git ref to build the image with - + release_image: + description: 'Tag image as a release' + required: true + default: false + type: boolean + env: WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-agent" CONTAINER_NAME: "node-status-agent" @@ -31,10 +38,10 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 id: get_version - with: - cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + run: | + VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml) + echo "result=$VERSION" >> $GITHUB_OUTPUT - name: cleanup-gateway-probe-ref id: cleanup_gateway_probe_ref @@ -43,19 +50,35 @@ jobs: GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}" echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT - - name: Remove existing tag if exists - run: | - if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} >/dev/null 2>&1; then - git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} - git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} - fi + - name: Set GIT_TAG variable + run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV - - name: Create tag - run: | - git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" - git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} + - name: Initialize RELEASE_TAG + run: echo "RELEASE_TAG=" >> $GITHUB_ENV + + - name: Set RELEASE_TAG for release + if: github.event.inputs.release_image == 'true' + run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV + + - name: Set IMAGE_NAME_AND_TAGS variable + run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV + + - name: New env vars + run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'" + + # - name: Remove existing tag if exists + # run: | + # if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then + # git push --delete origin $${{ env.GIT_TAG }} + # git tag -d $${{ env.GIT_TAG }} + # fi + + # - name: Create tag + # run: | + # git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" + # git push origin $${{ env.GIT_TAG }} - name: BuildAndPushImageOnHarbor run: | - docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} + docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml index 6afe4fd095..765e28e2e1 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -1,7 +1,13 @@ name: Build and upload Node Status API container to harbor.nymte.ch on: workflow_dispatch: - + inputs: + release_image: + description: 'Tag image as a release' + required: true + default: false + type: boolean + env: WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api" CONTAINER_NAME: "node-status-api" @@ -26,30 +32,43 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 id: get_version - with: - cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml - - - name: Check if tag exists run: | - if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then - echo "Tag ${{ steps.get_version.outputs.result }} already exists" - fi + VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml) + echo "result=$VERSION" >> $GITHUB_OUTPUT - - name: Remove existing tag if exists - run: | - if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then - git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} - git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} - fi + - name: Set GIT_TAG variable + run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV - - name: Create tag - run: | - git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" - git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + - name: Initialise RELEASE_TAG + run: echo "RELEASE_TAG=" >> $GITHUB_ENV + + - name: Set RELEASE_TAG for release + if: github.event.inputs.release_image == 'true' + run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV + + - name: Set IMAGE_NAME_AND_TAGS variable + run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV + + - name: New env vars + run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'" + + # - name: Remove existing tag if exists, then create + # run: | + # if git rev-parse "$GIT_TAG" >/dev/null 2>&1; then + # echo "Tag '$GIT_TAG' already exists, deleting" + # git push --delete origin "$GIT_TAG" + # git tag -d "$GIT_TAG" + # echo "Tag '$GIT_TAG' deleted" + # else + # echo "Tag '$GIT_TAG' does not exist, creating it" + # git tag -a $GIT_TAG -m "Version ${{ steps.get_version.outputs.result }}" + # git push origin $GIT_TAG + # echo "Tag '$GIT_TAG' created" + # fi - name: BuildAndPushImageOnHarbor run: | - docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile-pg . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags + diff --git a/.github/workflows/push-nym-api.yaml b/.github/workflows/push-nym-api.yaml new file mode 100644 index 0000000000..79f5fe9ade --- /dev/null +++ b/.github/workflows/push-nym-api.yaml @@ -0,0 +1,51 @@ +name: Build and upload Nym APU container to harbor.nymte.ch +on: + workflow_dispatch: + +env: + WORKING_DIRECTORY: "." + CONTAINER_NAME: "nym-api" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.47.1 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml + + - name: Remove existing tag if exists + run: | + echo "Checking if tag ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} exists..." + if git rev-parse ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + echo "Tag ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} already exists" + git push --delete origin ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.CONTAINER_NAME }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f nym-api.dockerfile ${{ env.WORKING_DIRECTORY }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/.github/workflows/push-nym-node.yaml b/.github/workflows/push-nym-node.yaml index 4608be2c85..05ad829163 100644 --- a/.github/workflows/push-nym-node.yaml +++ b/.github/workflows/push-nym-node.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nym-statistics-api.yaml b/.github/workflows/push-nym-statistics-api.yaml new file mode 100644 index 0000000000..93e804c305 --- /dev/null +++ b/.github/workflows/push-nym-statistics-api.yaml @@ -0,0 +1,42 @@ +name: Build and upload Nym Statistics API container to harbor.nymte.ch +on: + workflow_dispatch: + +env: + WORKING_DIRECTORY: "nym-statistics-api" + CONTAINER_NAME: "nym-statistics-api" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.47.1 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/.github/workflows/push-nyx-chain-watcher.yaml b/.github/workflows/push-nyx-chain-watcher.yaml index 79f0bb898c..c156a6ecde 100644 --- a/.github/workflows/push-nyx-chain-watcher.yaml +++ b/.github/workflows/push-nyx-chain-watcher.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-validator-rewarder.yaml b/.github/workflows/push-validator-rewarder.yaml index aeb435f02e..7fcb4b166e 100644 --- a/.github/workflows/push-validator-rewarder.yaml +++ b/.github/workflows/push-validator-rewarder.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.1 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.gitignore b/.gitignore index 5646f38f53..3c441754c6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,12 +35,13 @@ validator-api/keypair contracts/mixnet/code_id contracts/mixnet/Justfile contracts/mixnet/Makefile +artifacts +contracts/artifacts validator-config *.patch validator-api-config.toml dist storybook-static -envs/qwerty.env .parcel-cache **/.DS_Store cpu-cycles/libcpucycles/build @@ -59,3 +60,6 @@ nym-api/redocly/formatted-openapi.json *.sqlite .build + +**/settings.sql +**/enter_db.sh diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..0f6bf08925 --- /dev/null +++ b/.npmignore @@ -0,0 +1,58 @@ +# Security and sensitive files +.env* +*.key +*.pem +*.p12 +*.pfx +secrets/ +private/ +config/secrets/ + +# Development files +node_modules/ +.npm/ +.npmrc +.nvmrc +*.log +*.tmp +.DS_Store +Thumbs.db + +# Build artifacts +dist/ +build/ +target/ +*.tgz +*.tar.gz + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Test files +test/ +tests/ +__tests__/ +*.test.js +*.test.ts +*.spec.js +*.spec.ts + +# Documentation +docs/ +*.md +!README.md + +# CI/CD files +.github/ +.gitlab-ci.yml +.travis.yml +.circleci/ +azure-pipelines.yml + +# Scripts +scripts/ +!scripts/security-check.sh diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..032faed70f --- /dev/null +++ b/.npmrc @@ -0,0 +1,21 @@ +audit-level=moderate +fund=false +update-notifier=false +ignore-scripts=false +strict-ssl=true + +registry=https://registry.npmjs.org/ +audit=true +package-lock=true +package-lock-only=false +save-exact=false + +# use npm ci for production builds (faster and more secure) +# this will be enforced in CI/CD scripts + +# prevent installation of optional dependencies that might contain vulnerabilities +optional=false +audit=true +update-notifier=false + +save-exact=false diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..2a393af592 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.18.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b9e4e654..e5e14815ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,308 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2025.15-gruyere] (2025-08-20) + +- Migrate strum to 0.27.2 ([#5960]) +- WG exit policy scripts update ([#5921]) +- Make DNS Resolver fallback optional ([#5920]) +- nym-node debug command to reset providers db ([#5914]) +- basic zulip client for sending messages ([#5913]) +- chore: allow compatibility with 'CDLA-Permissive-2.0' ([#5910]) +- feat: ecash liveness check ([#5890]) +- Remove old free credential handle ([#5864]) + +[#5960]: https://github.com/nymtech/nym/pull/5960 +[#5921]: https://github.com/nymtech/nym/pull/5921 +[#5920]: https://github.com/nymtech/nym/pull/5920 +[#5914]: https://github.com/nymtech/nym/pull/5914 +[#5913]: https://github.com/nymtech/nym/pull/5913 +[#5910]: https://github.com/nymtech/nym/pull/5910 +[#5890]: https://github.com/nymtech/nym/pull/5890 +[#5864]: https://github.com/nymtech/nym/pull/5864 + +## [2025.14-feta] (2025-08-05) + +- chore: nym node tokio console ([#5909]) +- Feature/dkg snapshot epoch ([#5900]) +- Feature/dkg epoch dealers query ([#5899]) +- sqlx-pool-guard: allocate more memory on windows ([#5896]) +- Support mnemonic in the NS agent ([#5883]) +- Allow PG database backend ([#5880]) + +[#5909]: https://github.com/nymtech/nym/pull/5909 +[#5900]: https://github.com/nymtech/nym/pull/5900 +[#5899]: https://github.com/nymtech/nym/pull/5899 +[#5896]: https://github.com/nymtech/nym/pull/5896 +[#5883]: https://github.com/nymtech/nym/pull/5883 +[#5880]: https://github.com/nymtech/nym/pull/5880 + +## [2025.13-emmental] (2025-07-22) + +- fix: don't allow mixnode running in exit mode ([#5898]) +- fix contract build process in Makefile ([#5892]) +- bugfix: ignore 'Send' responses when claiming bandwidth ([#5884]) +- Update push-node-status-agent.yaml ([#5882]) +- listen for shutdown signals during nym-node startup ([#5879]) +- feat: forbid running mixnode + entry on the same node ([#5878]) +- chore: 1.88 clippy ([#5877]) +- Batch SQL writes for packet stats ([#5874]) +- fix the broken link ([#5873]) +- Set busy_timeout in sqlx ([#5872]) +- feat: basic performance contract integration [within Nym API] ([#5871]) +- scraper bugfix: ignore precommits from missing validators ([#5867]) +- Return true remaining ([#5866]) +- Make Mix hops optional for Mixnet Client SURBs ([#5861]) +- Check gateway supported versions ([#5860]) +- Add build info endpoints ([#5857]) +- Clear out screaming logs ([#5856]) +- fix removal of qa env ([#5855]) +- Use display when printing paths ([#5853]) +- feat: initial performance contract ([#5833]) +- Security patches for the `dkg` crate ([#5828]) +- HTTP Discovery objects & network defaults ([#5814]) + +[#5898]: https://github.com/nymtech/nym/pull/5898 +[#5892]: https://github.com/nymtech/nym/pull/5892 +[#5884]: https://github.com/nymtech/nym/pull/5884 +[#5882]: https://github.com/nymtech/nym/pull/5882 +[#5879]: https://github.com/nymtech/nym/pull/5879 +[#5878]: https://github.com/nymtech/nym/pull/5878 +[#5877]: https://github.com/nymtech/nym/pull/5877 +[#5874]: https://github.com/nymtech/nym/pull/5874 +[#5873]: https://github.com/nymtech/nym/pull/5873 +[#5872]: https://github.com/nymtech/nym/pull/5872 +[#5871]: https://github.com/nymtech/nym/pull/5871 +[#5867]: https://github.com/nymtech/nym/pull/5867 +[#5866]: https://github.com/nymtech/nym/pull/5866 +[#5861]: https://github.com/nymtech/nym/pull/5861 +[#5860]: https://github.com/nymtech/nym/pull/5860 +[#5857]: https://github.com/nymtech/nym/pull/5857 +[#5856]: https://github.com/nymtech/nym/pull/5856 +[#5855]: https://github.com/nymtech/nym/pull/5855 +[#5853]: https://github.com/nymtech/nym/pull/5853 +[#5833]: https://github.com/nymtech/nym/pull/5833 +[#5828]: https://github.com/nymtech/nym/pull/5828 +[#5814]: https://github.com/nymtech/nym/pull/5814 + +## [2025.12-dolcelatte] (2025-07-07) + +- bugfix: key-rotation + reply SURBs ([#5876]) +- Bugfix/backwards compat ([#5865]) +- bugfix: allow gateways to permit authentication from v4 clients ([#5862]) +- fixed client route for obtaining v2 list of gateways ([#5859]) +- Updated browser extension piece removal ([#5849]) +- Remove/old env references ([#5848]) +- Remove qa env ([#5847]) +- remove not used old mock-api ([#5845]) +- remove bity dir ([#5844]) +- build(deps-dev): bump webpack-dev-server from 4.13.2 to 5.2.1 in /wasm/mix-fetch/internal-dev ([#5843]) +- Amended the buy section ([#5841]) +- Removing test-net faucet ([#5840]) +- Feature/node status dvpn directory ([#5829]) +- build(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#5826]) +- bugfix: fix swapped total and circulating supplies ([#5822]) +- build(deps): bump tar-fs from 3.0.8 to 3.0.9 in /sdk/typescript/tests/integration-tests/mix-fetch ([#5821]) +- Url scheme warning log ([#5819]) +- chore: adjust heuristic for wireguard peer activity ([#5818]) +- Use the same client bandwidth for top up ([#5813]) +- Replace chrono with time in NS API ([#5811]) +- build(deps-dev): bump http-proxy-middleware from 2.0.4 to 2.0.9 in /clients/native/examples/js-examples/websocket ([#5810]) +- build(deps): bump tokio from 1.44.2 to 1.45.1 ([#5798]) +- Close sqlite pool before moving or reopening databases ([#5796]) +- HTTP Client Retries, Fallbacks, and Redirects ([#5789]) +- feat: key rotation ([#5777]) +- build(deps): bump next from 14.2.15 to 14.2.26 in /documentation/docs ([#5772]) +- build(deps): bump undici from 5.28.5 to 5.29.0 in /.github/actions/nym-hash-releases/src ([#5771]) +- build(deps): bump cargo_metadata from 0.18.1 to 0.19.2 ([#5765]) +- build(deps): bump tempfile from 3.19.1 to 3.20.0 ([#5764]) +- [Feature] Noise XKpsk3 integration (2025 version) ([#5692]) +- feature: nympool contract ([#5464]) +- chore: fixed typo in API endpoint parameter ([#5449]) + +[#5876]: https://github.com/nymtech/nym/pull/5876 +[#5865]: https://github.com/nymtech/nym/pull/5865 +[#5862]: https://github.com/nymtech/nym/pull/5862 +[#5859]: https://github.com/nymtech/nym/pull/5859 +[#5849]: https://github.com/nymtech/nym/pull/5849 +[#5848]: https://github.com/nymtech/nym/pull/5848 +[#5847]: https://github.com/nymtech/nym/pull/5847 +[#5845]: https://github.com/nymtech/nym/pull/5845 +[#5844]: https://github.com/nymtech/nym/pull/5844 +[#5843]: https://github.com/nymtech/nym/pull/5843 +[#5841]: https://github.com/nymtech/nym/pull/5841 +[#5840]: https://github.com/nymtech/nym/pull/5840 +[#5829]: https://github.com/nymtech/nym/pull/5829 +[#5826]: https://github.com/nymtech/nym/pull/5826 +[#5822]: https://github.com/nymtech/nym/pull/5822 +[#5821]: https://github.com/nymtech/nym/pull/5821 +[#5819]: https://github.com/nymtech/nym/pull/5819 +[#5818]: https://github.com/nymtech/nym/pull/5818 +[#5813]: https://github.com/nymtech/nym/pull/5813 +[#5811]: https://github.com/nymtech/nym/pull/5811 +[#5810]: https://github.com/nymtech/nym/pull/5810 +[#5798]: https://github.com/nymtech/nym/pull/5798 +[#5796]: https://github.com/nymtech/nym/pull/5796 +[#5789]: https://github.com/nymtech/nym/pull/5789 +[#5777]: https://github.com/nymtech/nym/pull/5777 +[#5772]: https://github.com/nymtech/nym/pull/5772 +[#5771]: https://github.com/nymtech/nym/pull/5771 +[#5765]: https://github.com/nymtech/nym/pull/5765 +[#5764]: https://github.com/nymtech/nym/pull/5764 +[#5692]: https://github.com/nymtech/nym/pull/5692 +[#5464]: https://github.com/nymtech/nym/pull/5464 +[#5449]: https://github.com/nymtech/nym/pull/5449 + +## [2025.11-cheddar] (2025-06-10) + +- No autoremoval of peers ([#5831]) +- Set cached storage counters to 0 ([#5812]) +- hack: temporarily use next.config.js instead of next.config.ts ([#5805]) +- chore: resolve 1.87 clippy warnings ([#5802]) +- Nym Statistics API ([#5800]) +- QoL: RequestPath trait for http-api-client ([#5788]) +- Fix contains ticketbook function that always returned true ([#5787]) +- swap a decode into a fromrow to please future postgres feature ([#5785]) +- Make address cache configurable ([#5784]) +- Track wireguard credential retries ([#5783]) + +[#5831]: https://github.com/nymtech/nym/pull/5831 +[#5812]: https://github.com/nymtech/nym/pull/5812 +[#5805]: https://github.com/nymtech/nym/pull/5805 +[#5802]: https://github.com/nymtech/nym/pull/5802 +[#5800]: https://github.com/nymtech/nym/pull/5800 +[#5788]: https://github.com/nymtech/nym/pull/5788 +[#5787]: https://github.com/nymtech/nym/pull/5787 +[#5785]: https://github.com/nymtech/nym/pull/5785 +[#5784]: https://github.com/nymtech/nym/pull/5784 +[#5783]: https://github.com/nymtech/nym/pull/5783 + +## [2025.10-brie] (2025-05-27) + +- Backport PR 5779 ([#5801]) +- Expanded Accept Encoding for `reqwest` ([#5779]) +- Teach HttpClientError how to report its status code and timeout ([#5770]) +- Skip refreshing the topology on startup as we already have an initial set ([#5768]) +- Fetch the topology from the nym-api concurrently ([#5767]) +- feat: use bincode by default in NymApiClient + remove feature-lock ([#5761]) +- Instrument create_request ([#5760]) +- Add node_bonded field to delegations ([#5759]) +- build(deps): bump mikefarah/yq from 4.45.1 to 4.45.4 ([#5758]) +- Raw route submissions ([#5756]) +- feat: expires header for `/active` nym-api responses ([#5755]) +- Decrease default average packet delay to 15 ms ([#5754]) +- build(deps): bump the patch-updates group across 1 directory with 12 updates ([#5753]) +- Remove pretty_env_logger and switch remaining crates to use tracing ([#5749]) +- Update pretty_env_logger to latest to not depend on unmaintained crate atty ([#5748]) +- Upgrade prometheus crate to fix security warning ([#5747]) +- Downgrade deranged crate to 0.4.0 ([#5746]) +- feat: nym-api bincode + yaml support ([#5745]) +- fix parallel feature in ecash crate with send + sync ([#5744]) +- Remove old test directory - Update validator docker ([#5743]) +- [Feature] `RememberMe` is the new don't `ForgetMe` ([#5742]) +- build(deps): bump ammonia from 4.0.0 to 4.1.0 ([#5739]) +- build(deps): bump base-x from 3.0.9 to 3.0.11 in /testnet-faucet ([#5737]) +- build(deps): bump http-proxy-middleware from 2.0.8 to 2.0.9 ([#5730]) + +[#5801]: https://github.com/nymtech/nym/pull/5801 +[#5779]: https://github.com/nymtech/nym/pull/5779 +[#5770]: https://github.com/nymtech/nym/pull/5770 +[#5768]: https://github.com/nymtech/nym/pull/5768 +[#5767]: https://github.com/nymtech/nym/pull/5767 +[#5761]: https://github.com/nymtech/nym/pull/5761 +[#5760]: https://github.com/nymtech/nym/pull/5760 +[#5759]: https://github.com/nymtech/nym/pull/5759 +[#5758]: https://github.com/nymtech/nym/pull/5758 +[#5756]: https://github.com/nymtech/nym/pull/5756 +[#5755]: https://github.com/nymtech/nym/pull/5755 +[#5754]: https://github.com/nymtech/nym/pull/5754 +[#5753]: https://github.com/nymtech/nym/pull/5753 +[#5749]: https://github.com/nymtech/nym/pull/5749 +[#5748]: https://github.com/nymtech/nym/pull/5748 +[#5747]: https://github.com/nymtech/nym/pull/5747 +[#5746]: https://github.com/nymtech/nym/pull/5746 +[#5745]: https://github.com/nymtech/nym/pull/5745 +[#5744]: https://github.com/nymtech/nym/pull/5744 +[#5743]: https://github.com/nymtech/nym/pull/5743 +[#5742]: https://github.com/nymtech/nym/pull/5742 +[#5739]: https://github.com/nymtech/nym/pull/5739 +[#5737]: https://github.com/nymtech/nym/pull/5737 +[#5730]: https://github.com/nymtech/nym/pull/5730 + +## [2025.9-appenzeller] (2025-05-13) + +- build(deps): bump clap from 4.5.36 to 4.5.37 in the patch-updates group ([#5722]) +- build(deps): bump golang.org/x/net from 0.36.0 to 0.38.0 in /wasm/mix-fetch/go-mix-conn ([#5720]) +- build(deps-dev): bump http-proxy-middleware from 2.0.6 to 2.0.9 in /wasm/client/internal-dev ([#5719]) +- Add /account/{address} ([#5673]) +- Add contains ticketbook data db query ([#5670]) + +[#5722]: https://github.com/nymtech/nym/pull/5722 +[#5720]: https://github.com/nymtech/nym/pull/5720 +[#5719]: https://github.com/nymtech/nym/pull/5719 +[#5673]: https://github.com/nymtech/nym/pull/5673 +[#5670]: https://github.com/nymtech/nym/pull/5670 + +## [2025.8-tourist] (2025-04-29) + +- add reserved byte to reply surb serialisation ([#5731]) +- Remove inactive peers ([#5721]) +- Update Hickory DNS "0.24.4" to "0.25" ([#5709]) +- build(deps): bump the patch-updates group across 1 directory with 7 updates ([#5708]) +- Peer handle should die more gracefully ([#5704]) +- build(deps): bump crossbeam-channel from 0.5.14 to 0.5.15 ([#5702]) +- build(deps): bump actions/checkout from 3 to 4 ([#5700]) +- Feature/updated sphinx payload keys ([#5698]) +- Bump the nym-vpn deb metapackage to 1.0 ([#5697]) +- Make mix hops optional for Mixnet Client ([#5696]) +- build(deps): bump tokio from 1.44.1 to 1.44.2 ([#5693]) +- Feature/replay protection ([#5682]) +- Adding fresh nym-api tests and workflow ([#5659]) +- build(deps): bump next from 14.2.21 to 14.2.25 ([#5655]) +- build(deps): bump pnpm/action-setup from 4.0.0 to 4.1.0 ([#5436]) + +[#5731]: https://github.com/nymtech/nym/pull/5731 +[#5721]: https://github.com/nymtech/nym/pull/5721 +[#5709]: https://github.com/nymtech/nym/pull/5709 +[#5708]: https://github.com/nymtech/nym/pull/5708 +[#5704]: https://github.com/nymtech/nym/pull/5704 +[#5702]: https://github.com/nymtech/nym/pull/5702 +[#5700]: https://github.com/nymtech/nym/pull/5700 +[#5698]: https://github.com/nymtech/nym/pull/5698 +[#5697]: https://github.com/nymtech/nym/pull/5697 +[#5696]: https://github.com/nymtech/nym/pull/5696 +[#5693]: https://github.com/nymtech/nym/pull/5693 +[#5682]: https://github.com/nymtech/nym/pull/5682 +[#5659]: https://github.com/nymtech/nym/pull/5659 +[#5655]: https://github.com/nymtech/nym/pull/5655 +[#5436]: https://github.com/nymtech/nym/pull/5436 + +## [2025.7-tex] (2025-04-14) + +- Expand /v3/nym-nodes with geodata ([#5686]) +- chore: clippy for 1.86 ([#5685]) +- Featrure: Bash scripts to init and configure VMs conveniently and update docs ([#5681]) +- Update node versions in CI ([#5677]) +- build(deps): bump the patch-updates group across 1 directory with 8 updates ([#5668]) +- Update log crate ([#5667]) +- Minor fixes involving key cloning and hashing ([#5664]) +- mix throughput tester ([#5661]) +- build(deps): bump blake3 from 1.6.1 to 1.7.0 ([#5658]) +- build(deps): bump elliptic from 6.5.5 to 6.6.1 ([#5483]) +- Move all workflows on ubuntu-20 to ubuntu-22 ([#5455]) + +[#5686]: https://github.com/nymtech/nym/pull/5686 +[#5685]: https://github.com/nymtech/nym/pull/5685 +[#5681]: https://github.com/nymtech/nym/pull/5681 +[#5677]: https://github.com/nymtech/nym/pull/5677 +[#5668]: https://github.com/nymtech/nym/pull/5668 +[#5667]: https://github.com/nymtech/nym/pull/5667 +[#5664]: https://github.com/nymtech/nym/pull/5664 +[#5661]: https://github.com/nymtech/nym/pull/5661 +[#5658]: https://github.com/nymtech/nym/pull/5658 +[#5483]: https://github.com/nymtech/nym/pull/5483 +[#5455]: https://github.com/nymtech/nym/pull/5455 + ## [2025.6-chuckles] (2025-03-31) - Remove Google public DNS ([#5660]) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..573e3538ca --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,686 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Nym is a privacy platform that uses mixnet technology to protect against metadata surveillance. The platform consists of several key components: +- Mixnet nodes (mixnodes) for packet mixing +- Gateways (entry/exit points for the network) +- Clients for interacting with the network +- Network monitoring tools +- Validators for network consensus +- Various service providers and integrations + +## Build Commands + +### Rust Components + +```bash +# Default build (debug) +cargo build + +# Release build +cargo build --release + +# Build a specific package +cargo build -p + +# Build main components +make build + +# Build release versions of main binaries and contracts +make build-release + +# Build specific binaries +make build-nym-cli +cargo build -p nym-node --release +cargo build -p nym-api --release +``` + +### Testing + +```bash +# Run clippy, unit tests, and formatting +make test + +# Run all tests including slow tests +make test-all + +# Run clippy on all workspaces +make clippy + +# Run unit tests for a specific package +cargo test -p + +# Run only expensive/ignored tests +cargo test --workspace -- --ignored + +# Run API tests +dotenv -f envs/sandbox.env -- cargo test --test public-api-tests + +# Run tests with specific log level +RUST_LOG=debug cargo test -p + +# Run specific test scripts +./nym-node/tests/test_apis.sh +./scripts/wireguard-exit-policy/exit-policy-tests.sh +``` + +### Linting and Formatting + +```bash +# Run rustfmt on all code +make fmt + +# Check formatting without modifying +cargo fmt --all -- --check + +# Run clippy with all targets +cargo clippy --workspace --all-targets -- -D warnings + +# TypeScript linting +yarn lint +yarn lint:fix +yarn types:lint:fix + +# Check dependencies for security/licensing issues +cargo deny check +``` + +### WASM Components + +```bash +# Build all WASM components +make sdk-wasm-build + +# Build TypeScript SDK +yarn build:sdk +npx lerna run --scope @nymproject/sdk build --stream + +# Build and test WASM components +make sdk-wasm + +# Build specific WASM packages +cd wasm/client && make +cd wasm/mix-fetch && make +cd wasm/node-tester && make +``` + +### Contract Development + +```bash +# Build all contracts +make contracts + +# Build contracts in release mode +make build-release-contracts + +# Generate contract schemas +make contract-schema + +# Run wasm-opt on contracts +make wasm-opt-contracts + +# Check contracts with cosmwasm-check +make cosmwasm-check-contracts +``` + +### Running Components + +```bash +# Run nym-node as a mixnode +cargo run -p nym-node -- run --mode mixnode + +# Run nym-node as a gateway +cargo run -p nym-node -- run --mode gateway + +# Run the network monitor +cargo run -p nym-network-monitor + +# Run the API server +cargo run -p nym-api + +# Run with specific environment +dotenv -f envs/sandbox.env -- cargo run -p nym-api + +# Start a local network +./scripts/localnet_start.sh +``` + +## Architecture + +The Nym platform consists of various components organized as a monorepo: + +1. **Core Mixnet Infrastructure**: + - `nym-node`: Core binary supporting mixnode and gateway modes + - `common/nymsphinx`: Implementation of the Sphinx packet format + - `common/topology`: Network topology management + - `common/types`: Shared data types across components + +2. **Network Monitoring**: + - `nym-network-monitor`: Monitors the network's reliability and performance + - `nym-api`: API server for network stats and monitoring data + - Metrics tracking for nodes, routes, and overall network health + +3. **Client Implementations**: + - `clients/native`: Native Rust client implementation + - `clients/socks5`: SOCKS5 proxy client for standard applications + - `wasm`: WebAssembly client implementations (for browsers) + - `nym-connect`: Desktop and mobile clients + +4. **Blockchain & Smart Contracts**: + - `common/cosmwasm-smart-contracts`: Smart contract implementations + - `contracts`: CosmWasm contracts for the Nym network + - `common/ledger`: Blockchain integration + +5. **Utilities & Tools**: + - `tools`: Various CLI tools and utilities + - `sdk`: SDKs for different languages and platforms + - `documentation`: Documentation generation and management + +## Packet System + +Nym uses a modified Sphinx packet format for its mixnet: + +1. **Message Chunking**: + - Messages are divided into "sets" and "fragments" + - Each fragment fits in a single Sphinx packet + - The `common/nymsphinx/chunking` module handles message fragmentation + +2. **Routing**: + - Packets traverse through 3 layers of mixnodes + - Routing information is encrypted in layers (onion routing) + - The final gateway receives and processes the messages + +3. **Monitoring**: + - Monitoring system tracks packet delivery through the network + - Routes are analyzed for reliability statistics + - Node performance metrics are collected + +## Network Protocol + +Nym implements the Loopix mixnet design with several key privacy features: + +1. **Continuous-time Mixing**: + - Each mixnode delays messages independently with an exponential distribution + - This creates random reordering of packets, destroying timing correlations + - Offers better anonymity properties than batch mixing approaches + +2. **Cover Traffic**: + - Clients and nodes generate dummy "loop" packets that circulate through the network + - These packets are indistinguishable from real traffic + - Creates a baseline level of traffic that hides actual communication patterns + - Provides unobservability (hiding when and how much real traffic is being sent) + +3. **Stratified Network Architecture**: + - Traffic flows through Entry Gateway → 3 Mixnode Layers → Exit Gateway + - Path selection is independent per-message (unlike Tor) + - Each node connects only to adjacent layers + +4. **Anonymous Replies**: + - Single-Use Reply Blocks (SURBs) allow receiving messages without revealing identity + - Enables bidirectional communication while maintaining privacy + +## Network Monitoring Architecture + +The network monitoring system is a core component that measures mixnet reliability: + +1. The `nym-network-monitor` sends test packets through the network +2. These packets follow predefined routes through multiple mixnodes +3. Metrics are collected about: + - Successful and failed packet deliveries + - Node reliability (percentage of successful packet handling) + - Route reliability (which specific route combinations work best) +4. Results are stored in the database and used by `nym-api` to: + - Present node performance statistics + - Determine network rewards + - Provide route selection guidance to clients + +In the current branch, metrics collection is being enhanced with a fanout approach to submit to multiple API endpoints. + +## Development Environment + +### Required Dependencies + +- Rust toolchain (stable, 1.80+) +- Node.js (v20+) and yarn for TypeScript components +- SQLite for local database development +- PostgreSQL for API database (optional, for full API functionality) +- CosmWasm tools for contract development +- For building contracts: `wasm-opt` tool from `binaryen` +- Python 3.8+ for some scripts +- Docker (optional, for containerized development) +- protoc (Protocol Buffers compiler) for some components + +### Environment Configurations + +The `envs/` directory contains pre-configured environments: + +#### Available Environments + +- **`local.env`**: Local development environment + - Points to local services (localhost) + - Uses test mnemonics and keys + - Ideal for testing without external dependencies + +- **`sandbox.env`**: Sandbox test network + - Public test network with real nodes + - Test tokens available from faucet + - Contract addresses for sandbox deployment + - API: https://sandbox-nym-api1.nymtech.net + +- **`mainnet.env`**: Production mainnet + - Real network with real tokens + - Production contract addresses + - API: https://validator.nymtech.net + - Use with caution! + +- **`canary.env`**: Canary deployment + - Pre-release testing environment + - Tests new features before mainnet + +- **`mainnet-local-api.env`**: Hybrid environment + - Uses mainnet contracts but local API + - Useful for API development against mainnet data + +#### Key Environment Variables + +```bash +# Network configuration +NETWORK_NAME=sandbox # Network identifier +BECH32_PREFIX=n # Address prefix (n for sandbox, n for mainnet) +NYM_API=https://sandbox-nym-api1.nymtech.net/api +NYXD=https://rpc.sandbox.nymtech.net +NYM_API_NETWORK=sandbox + +# Contract addresses (network-specific) +MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav +VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz +# ... other contract addresses + +# Mnemonic for testing (NEVER use in production) +MNEMONIC="clutch captain shoe salt awake harvest setup primary inmate ugly among become" + +# API Keys and tokens +IPINFO_API_TOKEN=your_token_here +AUTHENTICATOR_PASSWORD=password_here + +# Logging +RUST_LOG=info # Options: error, warn, info, debug, trace +RUST_BACKTRACE=1 # Enable backtraces + +# Database +DATABASE_URL=postgresql://user:pass@localhost/nym_api +``` + +#### Using Environment Files + +```bash +# Load environment and run command +dotenv -f envs/sandbox.env -- cargo run -p nym-api + +# Export to shell +source envs/sandbox.env + +# Use with make targets +dotenv -f envs/sandbox.env -- make run-api-tests +``` + +## Initial Setup + +### First Time Setup + +1. **Install Prerequisites** + ```bash + # Install Rust + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + + # Install Node.js and yarn + # Via nvm (recommended): + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash + nvm install 20 + npm install -g yarn + + # Install build tools + # Ubuntu/Debian: + sudo apt-get install build-essential pkg-config libssl-dev protobuf-compiler libpq-dev + + # macOS: + brew install protobuf postgresql + + # Install wasm-opt for contract builds + npm install -g wasm-opt + + # Add wasm target for Rust + rustup target add wasm32-unknown-unknown + ``` + +2. **Clone and Setup Repository** + ```bash + git clone https://github.com/nymtech/nym.git + cd nym/nym + + # Install JavaScript dependencies + yarn install + + # Build the project + make build + ``` + +3. **Database Setup (Optional, for API development)** + ```bash + # Install PostgreSQL + # Create database + createdb nym_api + + # Run migrations (from nym-api directory) + cd nym-api + sqlx migrate run + ``` + +### Quick Start + +```bash +# Run a mixnode locally +dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode mixnode --id my-mixnode + +# Run a gateway locally +dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode gateway --id my-gateway + +# Run the API server +dotenv -f envs/sandbox.env -- cargo run -p nym-api + +# Run a client +cargo run -p nym-client -- init --id my-client +cargo run -p nym-client -- run --id my-client +``` + +## CI/CD Pipeline + +The project uses GitHub Actions for CI/CD with several key workflows: + +1. **Build and Test**: + - `ci-build.yml`: Main build workflow for Rust components + - Tests are run on multiple platforms (Linux, Windows, macOS) + - Includes formatting check (rustfmt) and linting (clippy) + +2. **Release Process**: + - Binary artifacts are published on release tags + - Multiple platform builds are created + +3. **Documentation**: + - Documentation is automatically built and deployed + +## Database Structure + +The system uses SQLite databases with tables like: +- `mixnode_status`: Status information about mixnodes +- `gateway_status`: Status information about gateways +- `routes`: Route performance information (success/failure of specific paths) +- `monitor_run`: Information about monitoring test runs + +## Development Workflows + +### Running a Node + +To run the mixnode or gateway: + +```bash +# Run nym-node as a mixnode with specified identity +cargo run -p nym-node -- run --mode mixnode --id my-mixnode + +# Run nym-node as a gateway +cargo run -p nym-node -- run --mode gateway --id my-gateway +``` + +### Configuration + +Nodes can be configured with files in various locations: +- Command-line arguments +- Environment variables +- `.env` files specified with `--config-env-file` + +### Monitoring + +To monitor the health of your node: +- View logs for real-time information +- Use the node's HTTP API for status information +- Check the explorer for public node statistics + +## Common Libraries + +- `common/types`: Shared data types across all components +- `common/crypto`: Cryptographic primitives and wrappers +- `common/client-core`: Core client functionality +- `common/gateway-client`: Client-gateway communication +- `common/task`: Task management and concurrency utilities +- `common/nymsphinx`: Sphinx packet implementation for mixnet +- `common/topology`: Network topology management +- `common/credentials`: Credential system for privacy-preserving authentication +- `common/bandwidth-controller`: Bandwidth management and accounting + +## Code Conventions + +- Error handling: Use anyhow/thiserror for structured error handling +- Logging: Use the tracing framework for logging and diagnostics +- State management: Generally use Tokio/futures for async code +- Configuration: Use the config crate and env vars with defaults +- Database: Use sqlx for type-safe database queries +- Follow clippy recommendations and rustfmt formatting +- Use semantic commit messages: feat, fix, docs, refactor, test, chore + +## When Making Changes + +- Run `make test` before submitting PRs +- Follow Rust naming conventions +- Use `clippy` to check for common issues +- Update SQLx query caches when modifying DB queries: `cargo sqlx prepare` +- Consider backward compatibility for protocol changes +- Use lefthook pre-commit hooks for TypeScript formatting +- Run `cargo deny check` to verify dependency compliance +- Test against both sandbox and local environments when possible +- Update relevant documentation and CHANGELOG.md + +## Development Tools + +### Useful Cargo Commands + +```bash +# Check for outdated dependencies +cargo outdated + +# Analyze binary size +cargo bloat --release -p nym-node + +# Generate dependency graph +cargo tree -p nym-api + +# Run with instrumentation +cargo run --features profiling -p nym-node + +# Check for security advisories +cargo audit +``` + +### Database Tools + +```bash +# SQLx CLI for migrations +cargo install sqlx-cli + +# Create new migration +cd nym-api && sqlx migrate add + +# Prepare query metadata for offline compilation +cargo sqlx prepare --workspace + +# View database schema +./nym-api/enter_db.sh +``` + +### Development Scripts + +- `scripts/build_topology.py`: Generate network topology files +- `scripts/node_api_check.py`: Verify node API endpoints +- `scripts/network_tunnel_manager.sh`: Manage network tunnels +- `scripts/localnet_start.sh`: Start a local test network +- Various deployment scripts in `deployment/` for different environments + +## Debugging + +- Enable more verbose logging with the RUST_LOG environment variable: + ``` + RUST_LOG=debug,nym_node=trace cargo run -p nym-node -- run --mode mixnode + ``` +- Use the HTTP API endpoints for status information +- Check monitoring data in the database for network performance metrics +- For complex issues, use tracing tools to follow packet flow +- Enable backtraces: `RUST_BACKTRACE=full` +- For WASM debugging: Use browser developer tools with source maps + +## Deployment and Advanced Configurations + +### Deployment Structure + +The `deployment/` directory contains Ansible playbooks and configurations for various deployment scenarios: + +- **`aws/`**: AWS-specific deployment configurations +- **`mixnode/`**: Mixnode deployment playbooks +- **`gateway/`**: Gateway deployment playbooks +- **`validator/`**: Validator node deployment +- **`sandbox-v2/`**: Complete sandbox environment setup +- **`big-dipper-2/`**: Block explorer deployment + +### Sandbox V2 Deployment + +The sandbox-v2 deployment (`deployment/sandbox-v2/`) provides a complete test environment: + +```bash +# Key playbooks: +- deploy.yaml # Main deployment orchestrator +- deploy-mixnodes.yaml # Deploy mixnodes +- deploy-gateways.yaml # Deploy gateways +- deploy-validators.yaml # Deploy validator nodes +- deploy-nym-api.yaml # Deploy API services +``` + +### Custom Environment Setup + +To create a custom environment: + +1. Copy an existing env file: `cp envs/sandbox.env envs/custom.env` +2. Modify the network endpoints and contract addresses +3. Update the `NETWORK_NAME` to your identifier +4. Set appropriate mnemonics and keys (use fresh ones for production!) + +### Contract Addresses + +Contract addresses are network-specific and defined in environment files: +- Mixnet contract: Manages mixnode/gateway registry +- Vesting contract: Handles token vesting schedules +- Coconut contracts: Privacy-preserving credentials +- Name service: Human-readable address mapping +- Ecash contract: Electronic cash functionality + +### Local Network Setup + +For a completely local network: +```bash +# Start local chain +./scripts/localnet_start.sh + +# Deploy contracts +cd contracts +make deploy-local + +# Start nodes with local config +dotenv -f envs/local.env -- cargo run -p nym-node -- run --mode mixnode +``` + +## Common Issues and Troubleshooting + +### Database Issues + +- When modifying database queries, you must update SQLx query caches: + ```bash + cargo sqlx prepare + ``` +- If you see SQLx errors about missing query files, this is likely the cause +- For "database is locked" errors with SQLite, ensure only one process accesses the DB +- For PostgreSQL connection issues, verify DATABASE_URL and that the server is running + +### API Connection Issues + +- Check the environment variables pointing to the APIs (NYM_API, NYXD) +- Verify network connectivity and API health endpoints +- For authentication issues, check node keys and credentials +- Common endpoints to verify: + - API health: `$NYM_API/health` + - Chain status: `$NYXD/status` + - Contract info: `$NYXD/cosmwasm/wasm/v1/contract/$CONTRACT_ADDRESS` + +### Build Problems + +- Clean dependencies with `cargo clean` for a fresh build +- Check for compatible Rust version (1.80+ recommended) +- For smart contract builds, ensure wasm-opt is installed: `npm install -g wasm-opt` +- For cross-compilation issues, check target-specific dependencies +- WASM build issues: Ensure wasm32-unknown-unknown target is installed: + ```bash + rustup target add wasm32-unknown-unknown + ``` +- For "cannot find -lpq" errors, install PostgreSQL development files: + ```bash + # Ubuntu/Debian + sudo apt-get install libpq-dev + # macOS + brew install postgresql + ``` + +### Environment Issues + +- Contract address mismatches: Ensure you're using the correct environment file +- "Account sequence mismatch": The account nonce is out of sync, wait and retry +- Token decimal issues: Sandbox uses different decimal places than mainnet +- API version mismatches: Ensure your local API version matches the network +- "Insufficient funds": Get test tokens from faucet (sandbox) or check balance +- Gateway/mixnode bonding issues: Verify minimum stake requirements + +## Working with Routes and Monitoring + +1. Route monitoring metrics are stored in a `routes` table with: + - Layer node IDs (layer1, layer2, layer3, gw) + - Success flag (boolean) + - Timestamp + +2. To analyze routes: + - Check `NetworkAccount` and `AccountingRoute` in `nym-network-monitor/src/accounting.rs` + - View monitoring logic in `common/nymsphinx/chunking/monitoring.rs` + - Observe how routes are submitted to the database in the `submit_accounting_routes_to_db` function + +## Performance Optimization + +### Profiling and Benchmarking + +```bash +# Run benchmarks +cargo bench -p nym-node + +# Profile with perf (Linux) +cargo build --release --features profiling +perf record --call-graph=dwarf ./target/release/nym-node run --mode mixnode +perf report + +# Generate flamegraph +cargo install flamegraph +cargo flamegraph --bin nym-node -- run --mode mixnode +``` + +### Common Performance Considerations + +- Use bounded channels for backpressure +- Batch database operations where possible +- Monitor memory usage with `RUST_LOG=nym_node::metrics=debug` +- Use connection pooling for database connections +- Consider using `jemalloc` for better memory allocation performance \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f54cfddd60..ca6f517837 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,14 +4,14 @@ version = 3 [[package]] name = "accessory" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb3791c4beae5b827e93558ac83a88e63a841aad61759a05d9b577ef16030470" +checksum = "28e416a3ab45838bac2ab2d81b1088d738d7b2d2c5272a54d39366565a29bd80" dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -35,9 +35,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -91,15 +91,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -134,13 +133,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab99eae5ee58501ab236beb6f20f6ca39be615267b014899c89b2f0bc18a459" +checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" dependencies = [ + "cssparser", "html5ever", "maplit", - "once_cell", "tendril", "url", ] @@ -168,9 +167,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -183,44 +182,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arbitrary" @@ -382,6 +381,48 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.104", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -405,9 +446,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.18" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" dependencies = [ "brotli", "flate2", @@ -449,7 +490,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -460,7 +501,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -490,15 +531,10 @@ dependencies = [ ] [[package]] -name = "atty" -version = "0.2.14" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto-future" @@ -508,46 +544,18 @@ checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "autodoc" version = "0.1.0" dependencies = [ - "env_logger 0.11.7", + "env_logger", "log", ] -[[package]] -name = "axum" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core 0.3.4", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 0.1.2", - "tower 0.4.13", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.7.9" @@ -565,7 +573,41 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core 0.5.2", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "itoa", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -594,23 +636,6 @@ dependencies = [ "serde", ] -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-core" version = "0.4.5" @@ -632,6 +657,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-extra" version = "0.9.6" @@ -664,7 +709,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -678,7 +723,7 @@ dependencies = [ "auto-future", "axum 0.7.9", "bytes", - "bytesize", + "bytesize 1.3.3", "cookie", "http 1.3.1", "http-body-util", @@ -687,7 +732,37 @@ dependencies = [ "mime", "pretty_assertions", "reserve-port", - "rust-multipart-rfc7578_2", + "rust-multipart-rfc7578_2 0.6.1", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "axum-test" +version = "17.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum 0.8.4", + "bytes", + "bytesize 2.0.1", + "cookie", + "http 1.3.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2 0.8.0", "serde", "serde_json", "serde_urlencoded", @@ -699,9 +774,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -738,9 +813,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "base85rs" @@ -750,9 +825,9 @@ checksum = "87678d33a2af71f019ed11f52db246ca6c5557edee2cccbe689676d1ad9c6b5a" [[package]] name = "basic-toml" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ "serde", ] @@ -772,6 +847,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bip32" version = "0.5.3" @@ -784,16 +865,16 @@ dependencies = [ "rand_core 0.6.4", "ripemd", "secp256k1", - "sha2 0.10.8", + "sha2 0.10.9", "subtle 2.6.1", "zeroize", ] [[package]] name = "bip39" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" dependencies = [ "bitcoin_hashes", "rand 0.8.5", @@ -827,9 +908,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -868,10 +949,21 @@ dependencies = [ ] [[package]] -name = "blake3" -version = "1.7.0" +name = "blake2b_simd" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", @@ -914,7 +1006,7 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f6d7f06817e48ea4e17532fa61bc4e8b9a101437f0623f69d2ea54284f3a817" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "siphasher 1.0.1", ] @@ -942,9 +1034,9 @@ checksum = "3e31ea183f6ee62ac8b8a8cf7feddd766317adfb13ff469de57ce033efd6a790" [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -953,9 +1045,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -967,15 +1059,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", "tinyvec", ] [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-tools" @@ -1010,15 +1102,21 @@ dependencies = [ [[package]] name = "bytesize" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[package]] +name = "bytesize" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] @@ -1032,20 +1130,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo_metadata" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" -dependencies = [ - "camino", - "cargo-platform", - "semver 1.0.26", - "serde", - "serde_json", - "thiserror 1.0.69", -] - [[package]] name = "cargo_metadata" version = "0.18.1" @@ -1060,6 +1144,20 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.26", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "cast" version = "0.3.0" @@ -1074,9 +1172,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.2.14" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -1095,9 +1193,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -1141,9 +1239,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1194,9 +1292,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -1204,9 +1302,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -1216,9 +1314,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.47" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" +checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" dependencies = [ "clap", ] @@ -1235,27 +1333,38 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" @@ -1275,7 +1384,7 @@ checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" dependencies = [ "crossterm 0.28.1", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -1289,24 +1398,25 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", - "windows-sys 0.59.0", + "unicode-width 0.2.1", + "windows-sys 0.60.2", ] [[package]] name = "console-api" -version = "0.5.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" dependencies = [ - "prost 0.11.9", + "futures-core", + "prost", "prost-types", "tonic", "tracing-core", @@ -1314,16 +1424,18 @@ dependencies = [ [[package]] name = "console-subscriber" -version = "0.1.10" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" dependencies = [ "console-api", "crossbeam-channel", "crossbeam-utils", - "futures", + "futures-task", "hdrhistogram", - "humantime 2.2.0", + "humantime", + "hyper-util", + "prost", "prost-types", "serde", "serde_json", @@ -1416,7 +1528,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "462e1f6a8e005acc8835d32d60cbd7973ed65ea2a8d8473830e675f050956427" dependencies = [ - "prost 0.13.5", + "prost", "tendermint-proto", ] @@ -1467,7 +1579,7 @@ dependencies = [ "p256", "rand_core 0.6.4", "rayon", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1479,7 +1591,7 @@ checksum = "a782b93fae93e57ca8ad3e9e994e784583f5933aeaaa5c80a545c4b437be2047" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1489,7 +1601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6984ab21b47a096e17ae4c73cea2123a704d4b6686c39421247ad67020d76f95" dependencies = [ "cosmwasm-schema-derive", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -1503,7 +1615,7 @@ checksum = "e01c9214319017f6ebd8e299036e1f717fa9bb6724e758f7d6fb2477599d1a29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1522,10 +1634,10 @@ dependencies = [ "hex", "rand_core 0.6.4", "rmp-serde", - "schemars", + "schemars 0.8.22", "serde", "serde-json-wasm", - "sha2 0.10.8", + "sha2 0.10.9", "static_assertions", "thiserror 1.0.69", ] @@ -1541,9 +1653,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -1556,9 +1668,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1601,6 +1713,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1666,7 +1784,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "crossterm_winapi", "parking_lot", "rustix 0.38.44", @@ -1684,9 +1802,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1721,6 +1839,29 @@ dependencies = [ "subtle 1.0.0", ] +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.104", +] + [[package]] name = "csv" version = "1.3.1" @@ -1742,6 +1883,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctr" version = "0.9.2" @@ -1763,24 +1910,24 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.47" +version = "0.4.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" +checksum = "9e2d5c8f48d9c0c23250e52b55e82a6ab4fdba6650c931f5a0a57a43abda812b" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", - "socket2", - "windows-sys 0.52.0", + "socket2 0.5.10", + "windows-sys 0.59.0", ] [[package]] name = "curl-sys" -version = "0.4.80+curl-8.12.1" +version = "0.4.82+curl-8.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f7df2eac63200c3ab25bde3b2268ef2ee56af3d238e76d61f01c3c49bff734" +checksum = "c4d63638b5ec65f1a4ae945287b3fd035be4554bbaf211901159c9a2a74fb5be" dependencies = [ "cc", "libc", @@ -1788,7 +1935,7 @@ dependencies = [ "openssl-sys", "pkg-config", "vcpkg", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1816,7 +1963,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1842,11 +1989,31 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "cw-utils", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", ] +[[package]] +name = "cw-multi-test" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2" +dependencies = [ + "anyhow", + "bech32", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "itertools 0.14.0", + "prost", + "schemars 0.8.22", + "serde", + "sha2 0.10.9", + "thiserror 2.0.12", +] + [[package]] name = "cw-storage-plus" version = "2.0.0" @@ -1854,7 +2021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f13360e9007f51998d42b1bc6b7fa0141f74feae61ed5fd1e5b0a89eec7b5de1" dependencies = [ "cosmwasm-std", - "schemars", + "schemars 0.8.22", "serde", ] @@ -1866,7 +2033,7 @@ checksum = "07dfee7f12f802431a856984a32bce1cb7da1e6c006b5409e3981035ce562dec" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", ] @@ -1880,7 +2047,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "schemars", + "schemars 0.8.22", "semver 1.0.26", "serde", "thiserror 1.0.69", @@ -1895,7 +2062,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils", - "schemars", + "schemars 0.8.22", "serde", ] @@ -1909,7 +2076,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw20", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", ] @@ -1923,15 +2090,15 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "schemars", + "schemars 0.8.22", "serde", ] [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -1939,27 +2106,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -1978,9 +2145,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "defguard_wireguard_rs" @@ -2012,14 +2179,14 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -2028,9 +2195,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -2055,7 +2222,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -2084,7 +2251,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "unicode-xid", ] @@ -2096,7 +2263,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "unicode-xid", ] @@ -2165,7 +2332,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -2185,12 +2352,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dotenvy" version = "0.15.7" @@ -2198,10 +2359,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "dyn-clone" -version = "1.0.18" +name = "dtoa" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "easy-addr" @@ -2209,7 +2385,7 @@ version = "0.1.0" dependencies = [ "cosmwasm-std", "quote", - "syn 1.0.109", + "syn 2.0.104", ] [[package]] @@ -2235,10 +2411,15 @@ dependencies = [ "bincode", "bytecodec", "bytes", + "clap", "dashmap", "dirs", + "futures", + "nym-bin-common", + "nym-crypto", "nym-sdk", "serde", + "tempfile", "tokio", "tokio-stream", "tokio-util", @@ -2258,6 +2439,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.16", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -2273,15 +2464,15 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", "rand_core 0.6.4", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle 2.6.1", "zeroize", ] @@ -2297,15 +2488,15 @@ dependencies = [ "hashbrown 0.14.5", "hex", "rand_core 0.6.4", - "sha2 0.10.8", + "sha2 0.10.9", "zeroize", ] [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ "serde", ] @@ -2322,6 +2513,8 @@ dependencies = [ "ff", "generic-array 0.14.7", "group", + "hkdf", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -2354,7 +2547,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -2369,22 +2562,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.7.1" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "atty", - "humantime 1.3.0", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "env_logger" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -2393,15 +2573,6 @@ dependencies = [ "log", ] -[[package]] -name = "envy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" -dependencies = [ - "serde", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -2410,12 +2581,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2457,9 +2628,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener 5.4.0", "pin-project-lite", @@ -2467,7 +2638,7 @@ dependencies = [ [[package]] name = "extension-storage" -version = "1.3.0-rc.0" +version = "1.4.0-rc.0" dependencies = [ "bip39", "console_error_panic_hook", @@ -2499,14 +2670,14 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fancy_constructor" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac0fd7f4636276b4bd7b3148d0ba2c1c3fbede2b5214e47e7fedb70b02cde44" +checksum = "28a27643a5d05f3a22f5afd6e0d0e6e354f92d37907006f97b84b9cb79082198" dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -2560,9 +2731,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -2595,6 +2766,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2730,7 +2907,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -2771,15 +2948,16 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generator" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" dependencies = [ + "cc", "cfg-if", "libc", "log", "rustversion", - "windows 0.58.0", + "windows", ] [[package]] @@ -2805,41 +2983,29 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", - "windows-targets 0.52.6", -] - -[[package]] -name = "getset" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.98", ] [[package]] @@ -2947,9 +3113,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -2957,7 +3123,26 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.1", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2966,9 +3151,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -2983,7 +3168,7 @@ dependencies = [ "log", "pest", "pest_derive", - "quick-error 2.0.1", + "quick-error", "serde", "serde_json", ] @@ -3015,17 +3200,22 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.4", ] [[package]] @@ -3043,11 +3233,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "headers-core", "http 1.3.1", @@ -3065,23 +3255,11 @@ dependencies = [ "http 1.3.1", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" @@ -3091,24 +3269,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -3130,9 +3293,9 @@ checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" [[package]] name = "hickory-proto" -version = "0.24.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ "async-trait", "bytes", @@ -3142,45 +3305,45 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "h2", - "http 0.2.12", + "h2 0.4.11", + "http 1.3.1", "idna", "ipnet", "once_cell", - "rand 0.8.5", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", - "thiserror 1.0.69", + "rand 0.9.2", + "ring", + "rustls 0.23.29", + "thiserror 2.0.12", "tinyvec", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.2", "tracing", "url", - "webpki-roots 0.25.4", + "webpki-roots 0.26.11", ] [[package]] name = "hickory-resolver" -version = "0.24.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", - "lru-cache", + "moka", "once_cell", "parking_lot", - "rand 0.8.5", + "rand 0.9.2", "resolv-conf", - "rustls 0.21.12", + "rustls 0.23.29", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.2", "tracing", - "webpki-roots 0.25.4", + "webpki-roots 0.26.11", ] [[package]] @@ -3213,6 +3376,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.11" @@ -3222,29 +3409,15 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "hostname" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" -dependencies = [ - "libc", - "match_cfg", - "winapi", -] - [[package]] name = "html5ever" -version = "0.27.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 2.0.98", + "match_token", ] [[package]] @@ -3311,9 +3484,9 @@ checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpcodec" @@ -3337,15 +3510,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error 1.2.3", -] - [[package]] name = "humantime" version = "2.2.0" @@ -3358,7 +3522,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" dependencies = [ - "humantime 2.2.0", + "humantime", "serde", ] @@ -3372,14 +3536,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3395,6 +3559,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -3422,48 +3587,53 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.25", + "rustls 0.23.29", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 0.26.8", + "webpki-roots 1.0.2", ] [[package]] name = "hyper-timeout" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 0.14.32", + "hyper 1.6.0", + "hyper-util", "pin-project-lite", "tokio", - "tokio-io-timeout", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3471,16 +3641,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -3494,21 +3665,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -3517,31 +3689,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -3549,67 +3701,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -3629,9 +3768,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -3645,7 +3784,7 @@ checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -3685,9 +3824,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexed_db_futures" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94eebf0199c01a1560770d964efb1fb09183a3afc0b1c1397fac1bfdbfa7d6cf" +checksum = "69ff41758cbd104e91033bb53bc449bec7eea65652960c81eddf3fc146ecea19" dependencies = [ "accessory", "cfg-if", @@ -3714,7 +3853,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -3730,25 +3869,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "serde", ] [[package]] name = "indicatif" -version = "0.17.11" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", - "number_prefix", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width 0.2.1", + "unit-prefix", "vt100", "web-time", ] @@ -3775,9 +3914,9 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array 0.14.7", @@ -3816,13 +3955,24 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b12ebb6799019b044deaf431eadfe23245b259bba5a2c0796acec3943a3cdb" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" dependencies = [ "rustversion", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ip_network" version = "0.4.1" @@ -3835,7 +3985,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.10", "widestring", "windows-sys 0.48.0", "winreg", @@ -3857,12 +4007,22 @@ dependencies = [ ] [[package]] -name = "is-terminal" -version = "0.4.15" +name = "iri-string" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ - "hermit-abi 0.4.0", + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", "libc", "windows-sys 0.59.0", ] @@ -3918,15 +4078,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.4" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -3937,21 +4097,22 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.4" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] @@ -3965,6 +4126,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -3975,7 +4162,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2 0.10.8", + "sha2 0.10.9", "signature", ] @@ -3987,9 +4174,9 @@ checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -4053,32 +4240,32 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -4087,9 +4274,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.21" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" dependencies = [ "cc", "libc", @@ -4097,12 +4284,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -4111,9 +4292,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lioness" @@ -4129,26 +4310,20 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.27" @@ -4169,13 +4344,10 @@ dependencies = [ ] [[package]] -name = "lru-cache" +name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mac" @@ -4203,7 +4375,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4215,7 +4387,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4228,7 +4400,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -4239,23 +4411,25 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.12.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] [[package]] -name = "match_cfg" -version = "0.1.0" +name = "match_token" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "matchers" @@ -4272,6 +4446,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -4284,9 +4464,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -4331,9 +4511,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -4346,19 +4526,19 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.48.0", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -4401,6 +4581,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "mock_instant" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" + [[package]] name = "moka" version = "0.12.10" @@ -4470,7 +4656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55e5bda7ca0f9ac5e75b5debac3b75e29a8ac8e2171106a2c3bb466389a8dd83" dependencies = [ "anyhow", - "bitflags 2.8.0", + "bitflags 2.9.1", "byteorder", "libc", "log", @@ -4536,7 +4722,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "cfg-if", "libc", ] @@ -4547,7 +4733,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", @@ -4678,11 +4864,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -4695,26 +4881,18 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "nym-api" -version = "1.1.56" +version = "1.1.64" dependencies = [ "anyhow", "async-trait", "axum 0.7.9", - "axum-extra", - "axum-test", + "axum-test 16.4.1", "bincode", "bip39", "bs58", "cfg-if", - "chrono", "clap", "console-subscriber", "cosmwasm-std", @@ -4723,13 +4901,10 @@ dependencies = [ "cw3", "cw4", "dashmap", - "dirs", - "dotenv", + "dotenvy", "futures", - "getset", "humantime-serde", - "itertools 0.14.0", - "k256", + "moka", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", @@ -4743,12 +4918,11 @@ dependencies = [ "nym-crypto", "nym-dkg", "nym-ecash-contract-common", + "nym-ecash-signer-check", "nym-ecash-time", "nym-gateway-client", "nym-http-api-common", - "nym-inclusion-probability", "nym-mixnet-contract-common", - "nym-multisig-contract-common", "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", @@ -4760,25 +4934,25 @@ dependencies = [ "nym-topology", "nym-types", "nym-validator-client", - "nym-vesting-contract-common", "pin-project", "rand 0.8.5", "rand_chacha 0.3.1", - "reqwest 0.12.15", - "schemars", + "reqwest 0.12.22", + "schemars 0.8.22", "semver 1.0.26", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "sqlx", "tempfile", "tendermint", + "test-with", "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tokio-util", - "tower-http", + "tower-http 0.5.2", "tracing", "ts-rs", "url", @@ -4796,29 +4970,32 @@ dependencies = [ "cosmrs", "cosmwasm-std", "ecdsa", - "getset", "hex", "humantime-serde", + "nym-coconut-dkg-common", "nym-compact-ecash", "nym-config", "nym-contracts-common", "nym-credentials-interface", "nym-crypto", + "nym-ecash-signer-check-types", "nym-ecash-time", "nym-mixnet-contract-common", "nym-network-defaults", "nym-node-requests", + "nym-noise-keys", "nym-serde-helpers", "nym-ticketbooks-merkle", "rand_chacha 0.3.1", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "tendermint", "tendermint-rpc", "thiserror 2.0.12", "time", + "tracing", "ts-rs", "utoipa", ] @@ -4834,46 +5011,22 @@ dependencies = [ ] [[package]] -name = "nym-authenticator" +name = "nym-authenticator-client" version = "0.1.0" dependencies = [ - "anyhow", "bincode", - "bs58", - "bytes", - "clap", - "defguard_wireguard_rs", - "fastrand 2.3.0", "futures", - "ipnetwork", - "log", "nym-authenticator-requests", - "nym-bin-common", - "nym-client-core", - "nym-config", - "nym-credential-verification", "nym-credentials-interface", "nym-crypto", - "nym-gateway-requests", - "nym-gateway-storage", - "nym-id", - "nym-network-defaults", "nym-sdk", "nym-service-provider-requests-common", - "nym-service-providers-common", - "nym-sphinx", - "nym-task", - "nym-types", - "nym-wireguard", "nym-wireguard-types", - "rand 0.8.5", - "serde", - "serde_json", + "semver 1.0.26", "thiserror 2.0.12", "tokio", - "tokio-stream", "tokio-util", - "url", + "tracing", ] [[package]] @@ -4891,7 +5044,7 @@ dependencies = [ "nym-wireguard-types", "rand 0.8.5", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 2.0.12", "x25519-dalek", ] @@ -4928,8 +5081,7 @@ dependencies = [ "log", "opentelemetry", "opentelemetry-jaeger", - "pretty_env_logger", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "tracing", @@ -4940,24 +5092,9 @@ dependencies = [ "vergen", ] -[[package]] -name = "nym-bity-integration" -version = "0.1.0" -dependencies = [ - "anyhow", - "cosmrs", - "eyre", - "k256", - "nym-cli-commands", - "nym-validator-client", - "serde", - "serde_json", - "thiserror 2.0.12", -] - [[package]] name = "nym-cli" -version = "1.1.53" +version = "1.1.61" dependencies = [ "anyhow", "base64 0.22.1", @@ -4973,7 +5110,6 @@ dependencies = [ "nym-cli-commands", "nym-network-defaults", "nym-validator-client", - "pretty_env_logger", "serde", "serde_json", "tap", @@ -5033,14 +5169,14 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", - "toml 0.8.20", + "toml 0.8.23", "url", "zeroize", ] [[package]] name = "nym-client" -version = "1.1.53" +version = "1.1.61" dependencies = [ "bs58", "clap", @@ -5082,20 +5218,19 @@ dependencies = [ "async-trait", "base64 0.22.1", "bs58", + "cfg-if", "clap", "comfy-table", "futures", "gloo-timers", "http-body-util", - "humantime-serde", + "humantime", "hyper 1.6.0", "hyper-util", - "log", "nym-bandwidth-controller", "nym-client-core-config-types", "nym-client-core-gateways-storage", "nym-client-core-surb-storage", - "nym-config", "nym-credential-storage", "nym-credentials-interface", "nym-crypto", @@ -5104,7 +5239,6 @@ dependencies = [ "nym-gateway-requests", "nym-http-api-client", "nym-id", - "nym-metrics", "nym-mixnet-client", "nym-network-defaults", "nym-nonexhaustive-delayqueue", @@ -5118,7 +5252,7 @@ dependencies = [ "rand_chacha 0.3.1", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "si-scale", "tempfile", "thiserror 2.0.12", @@ -5126,6 +5260,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "tracing", "tungstenite 0.20.1", "url", "wasm-bindgen", @@ -5144,6 +5279,7 @@ dependencies = [ "nym-pemstore", "nym-sphinx-addressing", "nym-sphinx-params", + "nym-statistics-common", "serde", "thiserror 2.0.12", "url", @@ -5153,9 +5289,9 @@ dependencies = [ name = "nym-client-core-gateways-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "cosmrs", - "log", "nym-crypto", "nym-gateway-requests", "serde", @@ -5163,6 +5299,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", + "tracing", "url", "zeroize", ] @@ -5171,16 +5308,18 @@ dependencies = [ name = "nym-client-core-surb-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "dashmap", - "log", "nym-crypto", "nym-sphinx", "nym-task", "sqlx", + "sqlx-pool-guard", "thiserror 2.0.12", "time", "tokio", + "tracing", ] [[package]] @@ -5246,7 +5385,7 @@ dependencies = [ "rand 0.8.5", "rayon", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle 2.6.1", "thiserror 2.0.12", "zeroize", @@ -5262,7 +5401,7 @@ dependencies = [ "nym-network-defaults", "serde", "thiserror 2.0.12", - "toml 0.8.20", + "toml 0.8.23", "url", ] @@ -5270,11 +5409,12 @@ dependencies = [ name = "nym-contracts-common" version = "0.5.0" dependencies = [ + "anyhow", "bs58", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 2.0.12", @@ -5282,6 +5422,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common-testing" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", +] + [[package]] name = "nym-cpp-ffi" version = "0.1.2" @@ -5298,44 +5451,42 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", - "async-trait", "axum 0.7.9", "bip39", "bs58", "cfg-if", "clap", - "colored", - "dotenvy", "futures", - "humantime 2.2.0", + "humantime", "nym-bin-common", "nym-compact-ecash", "nym-config", + "nym-credential-proxy-lib", "nym-credential-proxy-requests", "nym-credentials", "nym-credentials-interface", "nym-crypto", "nym-ecash-contract-common", + "nym-ecash-signer-check", "nym-http-api-common", "nym-network-defaults", "nym-validator-client", "rand 0.8.5", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "sqlx", - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "tempfile", "thiserror 2.0.12", "time", "tokio", "tokio-util", - "tower 0.5.2", - "tower-http", + "tower-http 0.5.2", "tracing", "url", "utoipa", @@ -5344,6 +5495,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-credential-proxy-lib" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.9", + "bip39", + "bs58", + "futures", + "humantime", + "nym-compact-ecash", + "nym-credential-proxy-requests", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-signer-check", + "nym-network-defaults", + "nym-validator-client", + "rand 0.8.5", + "reqwest 0.12.22", + "serde", + "serde_json", + "sqlx", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.12", + "time", + "tokio", + "tokio-util", + "tracing", + "url", + "uuid", + "zeroize", +] + [[package]] name = "nym-credential-proxy-requests" version = "0.1.0" @@ -5354,8 +5542,8 @@ dependencies = [ "nym-http-api-client", "nym-http-api-common", "nym-serde-helpers", - "reqwest 0.12.15", - "schemars", + "reqwest 0.12.22", + "schemars 0.8.22", "serde", "serde_json", "time", @@ -5370,6 +5558,7 @@ dependencies = [ name = "nym-credential-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bincode", "log", @@ -5378,6 +5567,7 @@ dependencies = [ "nym-ecash-time", "serde", "sqlx", + "sqlx-pool-guard", "thiserror 2.0.12", "tokio", "zeroize", @@ -5405,9 +5595,11 @@ dependencies = [ name = "nym-credential-verification" version = "0.1.0" dependencies = [ + "async-trait", "bs58", "cosmwasm-std", "cw-utils", + "dyn-clone", "futures", "nym-api-requests", "nym-credentials", @@ -5458,7 +5650,8 @@ dependencies = [ "nym-network-defaults", "rand 0.8.5", "serde", - "strum 0.26.3", + "strum", + "strum_macros", "thiserror 2.0.12", "time", "utoipa", @@ -5471,6 +5664,7 @@ dependencies = [ "aead", "aes", "aes-gcm-siv", + "base64 0.22.1", "blake3", "bs58", "cipher", @@ -5480,13 +5674,14 @@ dependencies = [ "generic-array 0.14.7", "hkdf", "hmac", + "jwt-simple", "nym-pemstore", "nym-sphinx-types", "rand 0.8.5", "rand_chacha 0.3.1", "serde", "serde_bytes", - "sha2 0.10.8", + "sha2 0.10.9", "subtle-encoding", "thiserror 2.0.12", "x25519-dalek", @@ -5511,7 +5706,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde_derive", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 2.0.12", "zeroize", ] @@ -5530,6 +5725,36 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nym-ecash-signer-check" +version = "0.1.0" +dependencies = [ + "futures", + "nym-ecash-signer-check-types", + "nym-network-defaults", + "nym-validator-client", + "semver 1.0.26", + "thiserror 2.0.12", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "nym-ecash-signer-check-types" +version = "0.1.0" +dependencies = [ + "nym-coconut-dkg-common", + "nym-crypto", + "semver 1.0.26", + "serde", + "thiserror 2.0.12", + "time", + "tracing", + "url", + "utoipa", +] + [[package]] name = "nym-ecash-time" version = "0.1.0" @@ -5538,19 +5763,11 @@ dependencies = [ "time", ] -[[package]] -name = "nym-execute" -version = "0.1.0" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "nym-exit-policy" version = "0.1.0" dependencies = [ - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "thiserror 2.0.12", @@ -5566,6 +5783,7 @@ dependencies = [ "bs58", "lazy_static", "nym-bin-common", + "nym-crypto", "nym-sdk", "nym-sphinx-anonymous-replies", "tokio", @@ -5579,14 +5797,18 @@ version = "1.1.36" dependencies = [ "anyhow", "async-trait", + "bincode", "bip39", "bs58", "dashmap", "defguard_wireguard_rs", + "fastrand 2.3.0", "futures", "ipnetwork", + "mock_instant", "nym-api-requests", - "nym-authenticator", + "nym-authenticator-requests", + "nym-client-core", "nym-credential-verification", "nym-credentials", "nym-credentials-interface", @@ -5594,6 +5816,7 @@ dependencies = [ "nym-gateway-requests", "nym-gateway-stats-storage", "nym-gateway-storage", + "nym-id", "nym-ip-packet-router", "nym-mixnet-client", "nym-mixnode-common", @@ -5601,6 +5824,7 @@ dependencies = [ "nym-network-requester", "nym-node-metrics", "nym-sdk", + "nym-service-provider-requests-common", "nym-sphinx", "nym-statistics-common", "nym-task", @@ -5608,10 +5832,11 @@ dependencies = [ "nym-types", "nym-validator-client", "nym-wireguard", + "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", - "sha2 0.10.8", - "sqlx", + "serde", + "sha2 0.10.9", "thiserror 2.0.12", "time", "tokio", @@ -5628,7 +5853,7 @@ name = "nym-gateway-client" version = "0.1.0" dependencies = [ "futures", - "getrandom 0.2.15", + "getrandom 0.2.16", "gloo-utils 0.2.0", "nym-bandwidth-controller", "nym-credential-storage", @@ -5665,6 +5890,7 @@ dependencies = [ name = "nym-gateway-requests" version = "0.1.0" dependencies = [ + "anyhow", "bs58", "futures", "generic-array 0.14.7", @@ -5675,11 +5901,13 @@ dependencies = [ "nym-pemstore", "nym-serde-helpers", "nym-sphinx", + "nym-statistics-common", "nym-task", + "nym-test-utils", "rand 0.8.5", "serde", "serde_json", - "strum 0.26.3", + "strum", "subtle 2.6.1", "thiserror 2.0.12", "time", @@ -5694,12 +5922,12 @@ dependencies = [ name = "nym-gateway-stats-storage" version = "0.1.0" dependencies = [ - "nym-credentials-interface", + "anyhow", "nym-node-metrics", "nym-sphinx", "nym-statistics-common", "sqlx", - "strum 0.26.3", + "strum", "thiserror 2.0.12", "time", "tokio", @@ -5710,8 +5938,11 @@ dependencies = [ name = "nym-gateway-storage" version = "0.1.0" dependencies = [ + "anyhow", + "async-trait", "bincode", "defguard_wireguard_rs", + "dyn-clone", "nym-credentials-interface", "nym-gateway-requests", "nym-sphinx", @@ -5724,11 +5955,12 @@ dependencies = [ [[package]] name = "nym-go-ffi" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "lazy_static", "nym-bin-common", + "nym-crypto", "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", @@ -5745,7 +5977,7 @@ dependencies = [ "cosmwasm-schema", "cw-controllers", "cw4", - "schemars", + "schemars 0.8.22", "serde", ] @@ -5754,14 +5986,17 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "bytes", "encoding_rs", "hickory-resolver", "http 1.3.1", + "itertools 0.14.0", "mime", "nym-bin-common", + "nym-http-api-common", "once_cell", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "thiserror 2.0.12", @@ -5777,6 +6012,7 @@ version = "0.1.0" dependencies = [ "axum 0.7.9", "axum-client-ip", + "bincode", "bytes", "colored", "futures", @@ -5785,6 +6021,7 @@ dependencies = [ "serde_json", "serde_yaml", "subtle 2.6.1", + "time", "tower 0.5.2", "tracing", "utoipa", @@ -5826,6 +6063,20 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nym-ip-packet-client" +version = "0.1.0" +dependencies = [ + "bytes", + "futures", + "nym-ip-packet-requests", + "nym-sdk", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "nym-ip-packet-requests" version = "0.1.0" @@ -5876,7 +6127,7 @@ dependencies = [ "nym-wireguard", "nym-wireguard-types", "rand 0.8.5", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "thiserror 2.0.12", @@ -5914,8 +6165,11 @@ version = "0.1.0" dependencies = [ "dashmap", "futures", + "nym-crypto", + "nym-noise", "nym-sphinx", "nym-task", + "rand 0.8.5", "tokio", "tokio-stream", "tokio-util", @@ -5935,10 +6189,9 @@ dependencies = [ "humantime-serde", "nym-contracts-common", "rand_chacha 0.3.1", - "schemars", + "schemars 0.8.22", "semver 1.0.26", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -5983,7 +6236,7 @@ dependencies = [ "cw-utils", "cw3", "cw4", - "schemars", + "schemars 0.8.22", "serde", "thiserror 2.0.12", ] @@ -5992,11 +6245,11 @@ dependencies = [ name = "nym-network-defaults" version = "0.1.0" dependencies = [ - "cargo_metadata 0.18.1", + "cargo_metadata 0.19.2", "dotenvy", "log", "regex", - "schemars", + "schemars 0.8.22", "serde", "url", "utoipa", @@ -6025,7 +6278,7 @@ dependencies = [ "petgraph", "rand 0.8.5", "rand_chacha 0.3.1", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "tokio", @@ -6037,7 +6290,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.54" +version = "1.1.62" dependencies = [ "addr", "anyhow", @@ -6068,11 +6321,10 @@ dependencies = [ "nym-sphinx", "nym-task", "nym-types", - "pretty_env_logger", "publicsuffix", "rand 0.8.5", "regex", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "sqlx", @@ -6088,7 +6340,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.9.0" +version = "1.17.0" dependencies = [ "anyhow", "arc-swap", @@ -6099,11 +6351,13 @@ dependencies = [ "blake2 0.8.1", "bloomfilter", "bs58", - "cargo_metadata 0.18.1", + "cargo_metadata 0.19.2", "celes", + "cfg-if", "chacha", "clap", "colored", + "console-subscriber", "criterion", "csv", "cupid", @@ -6114,7 +6368,6 @@ dependencies = [ "indicatif", "ipnetwork", "lioness", - "nym-authenticator", "nym-bin-common", "nym-client-core-config-types", "nym-config", @@ -6129,6 +6382,8 @@ dependencies = [ "nym-network-requester", "nym-node-metrics", "nym-node-requests", + "nym-noise", + "nym-noise-keys", "nym-nonexhaustive-delayqueue", "nym-pemstore", "nym-sphinx-acknowledgements", @@ -6138,6 +6393,7 @@ dependencies = [ "nym-sphinx-params", "nym-sphinx-routing", "nym-sphinx-types", + "nym-statistics-common", "nym-task", "nym-topology", "nym-types", @@ -6146,16 +6402,17 @@ dependencies = [ "nym-wireguard", "nym-wireguard-types", "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "sysinfo", "thiserror 2.0.12", "time", "tokio", "tokio-util", - "toml 0.8.20", - "tower-http", + "toml 0.8.23", + "tower-http 0.5.2", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -6173,7 +6430,7 @@ dependencies = [ "futures", "nym-metrics", "nym-statistics-common", - "strum 0.26.3", + "strum", "time", "tokio", "tracing", @@ -6185,18 +6442,20 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime 2.2.0", + "humantime", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", + "nym-noise-keys", "nym-wireguard-types", "rand_chacha 0.3.1", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "strum 0.26.3", + "strum", + "strum_macros", "thiserror 2.0.12", "time", "tokio", @@ -6205,10 +6464,11 @@ dependencies = [ [[package]] name = "nym-node-status-agent" -version = "1.0.0-rc.2" +version = "1.0.4" dependencies = [ "anyhow", "clap", + "futures", "nym-bin-common", "nym-crypto", "nym-node-status-client", @@ -6221,23 +6481,26 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "2.1.0" +version = "3.3.2" dependencies = [ "ammonia", "anyhow", "axum 0.7.9", + "axum-test 17.3.0", "bip39", - "chrono", + "celes", "clap", "cosmwasm-std", - "envy", "futures-util", + "humantime", "itertools 0.14.0", "moka", "nym-bin-common", "nym-contracts-common", "nym-crypto", "nym-http-api-client", + "nym-http-api-common", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-node-metrics", "nym-node-requests", @@ -6249,18 +6512,19 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "regex", - "reqwest 0.12.15", + "reqwest 0.12.22", + "semver 1.0.26", "serde", "serde_json", "serde_json_path", "sqlx", - "strum 0.26.3", - "strum_macros 0.26.4", + "strum", + "strum_macros", "thiserror 2.0.12", "time", "tokio", "tokio-util", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-log 0.2.0", "tracing-subscriber", @@ -6271,16 +6535,15 @@ dependencies = [ [[package]] name = "nym-node-status-client" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "bincode", - "chrono", "nym-crypto", - "nym-http-api-client", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", + "time", "tracing", ] @@ -6324,6 +6587,39 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-noise" +version = "0.1.0" +dependencies = [ + "anyhow", + "arc-swap", + "bytes", + "futures", + "nym-crypto", + "nym-noise-keys", + "nym-test-utils", + "pin-project", + "rand_chacha 0.3.1", + "sha2 0.10.9", + "snow", + "strum", + "strum_macros", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "nym-noise-keys" +version = "0.1.0" +dependencies = [ + "nym-crypto", + "schemars 0.8.22", + "serde", + "utoipa", +] + [[package]] name = "nym-nonexhaustive-delayqueue" version = "0.1.0" @@ -6367,7 +6663,7 @@ dependencies = [ "chacha20poly1305", "criterion", "fastrand 2.3.0", - "getrandom 0.2.15", + "getrandom 0.2.16", "log", "rand 0.8.5", "rayon", @@ -6383,6 +6679,33 @@ version = "0.3.0" dependencies = [ "pem", "tracing", + "zeroize", +] + +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars 0.8.22", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "nym-pool-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "schemars 0.8.22", + "serde", + "thiserror 2.0.12", + "time", ] [[package]] @@ -6425,17 +6748,17 @@ dependencies = [ "nym-topology", "nym-validator-client", "parking_lot", - "pretty_env_logger", "rand 0.8.5", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "tap", "tempfile", "thiserror 2.0.12", + "time", "tokio", "tokio-stream", "tokio-util", - "toml 0.8.20", + "toml 0.8.23", "tracing", "tracing-subscriber", "url", @@ -6480,9 +6803,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-signers-monitor" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "humantime", + "itertools 0.14.0", + "nym-bin-common", + "nym-ecash-signer-check", + "nym-network-defaults", + "nym-task", + "nym-validator-client", + "time", + "tokio", + "tracing", + "url", + "zulip-client", +] + [[package]] name = "nym-socks5-client" -version = "1.1.53" +version = "1.1.61" dependencies = [ "bs58", "clap", @@ -6536,8 +6879,8 @@ dependencies = [ "nym-validator-client", "pin-project", "rand 0.8.5", - "reqwest 0.12.15", - "schemars", + "reqwest 0.12.22", + "schemars 0.8.22", "serde", "tap", "thiserror 2.0.12", @@ -6579,7 +6922,6 @@ dependencies = [ name = "nym-sphinx" version = "0.1.0" dependencies = [ - "log", "nym-crypto", "nym-metrics", "nym-mixnet-contract-common", @@ -6599,6 +6941,7 @@ dependencies = [ "rand_distr", "thiserror 2.0.12", "tokio", + "tracing", ] [[package]] @@ -6623,10 +6966,12 @@ dependencies = [ name = "nym-sphinx-addressing" version = "0.1.0" dependencies = [ + "bincode", "nym-crypto", "nym-sphinx-types", "rand 0.8.5", "serde", + "serde_json", "thiserror 2.0.12", ] @@ -6643,7 +6988,6 @@ dependencies = [ "nym-topology", "rand 0.8.5", "rand_chacha 0.3.1", - "serde", "thiserror 2.0.12", "tracing", "wasm-bindgen", @@ -6687,8 +7031,8 @@ dependencies = [ name = "nym-sphinx-forwarding" version = "0.1.0" dependencies = [ - "nym-outfox", "nym-sphinx-addressing", + "nym-sphinx-anonymous-replies", "nym-sphinx-params", "nym-sphinx-types", "thiserror 2.0.12", @@ -6738,6 +7082,37 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nym-statistics-api" +version = "0.1.5" +dependencies = [ + "anyhow", + "axum 0.7.9", + "axum-client-ip", + "axum-extra", + "celes", + "clap", + "nym-bin-common", + "nym-http-api-client", + "nym-http-api-common", + "nym-statistics-common", + "nym-task", + "nym-validator-client", + "serde", + "serde_json", + "sqlx", + "time", + "tokio", + "tokio-util", + "tower-http 0.5.2", + "tracing", + "tracing-subscriber", + "url", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", +] + [[package]] name = "nym-statistics-common" version = "0.1.0" @@ -6751,12 +7126,15 @@ dependencies = [ "nym-task", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "si-scale", + "strum", + "strum_macros", "sysinfo", "thiserror 2.0.12", "time", "tokio", + "utoipa", "wasmtimer", ] @@ -6767,7 +7145,7 @@ dependencies = [ "aes-gcm", "argon2", "generic-array 0.14.7", - "getrandom 0.2.15", + "getrandom 0.2.16", "rand 0.8.5", "serde", "serde_json", @@ -6791,6 +7169,16 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-test-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "rand_chacha 0.3.1", + "tokio", +] + [[package]] name = "nym-ticketbooks-merkle" version = "0.1.0" @@ -6800,10 +7188,10 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rs_merkle", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "time", "utoipa", ] @@ -6814,17 +7202,16 @@ version = "0.1.0" dependencies = [ "async-trait", "nym-api-requests", - "nym-config", "nym-crypto", "nym-mixnet-contract-common", "nym-sphinx-addressing", - "nym-sphinx-routing", "nym-sphinx-types", "rand 0.8.5", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", "thiserror 2.0.12", + "time", "tracing", "tsify", "wasm-bindgen", @@ -6859,12 +7246,13 @@ dependencies = [ "nym-mixnet-contract-common", "nym-validator-client", "nym-vesting-contract-common", - "reqwest 0.12.15", - "schemars", + "reqwest 0.12.22", + "schemars 0.8.22", "serde", "serde_json", - "sha2 0.10.8", - "strum 0.26.3", + "sha2 0.10.9", + "strum", + "strum_macros", "tempfile", "thiserror 2.0.12", "ts-rs", @@ -6873,6 +7261,22 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-upgrade-mode-check" +version = "0.1.0" +dependencies = [ + "anyhow", + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.12.22", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tracing", +] + [[package]] name = "nym-validator-client" version = "0.1.0" @@ -6904,13 +7308,14 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-performance-contract-common", "nym-serde-helpers", "nym-vesting-contract-common", - "prost 0.13.5", - "reqwest 0.12.15", + "prost", + "reqwest 0.12.22", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "tendermint-rpc", "thiserror 2.0.12", "time", @@ -6931,7 +7336,7 @@ dependencies = [ "clap", "cosmwasm-std", "futures", - "humantime 2.2.0", + "humantime", "humantime-serde", "nym-bin-common", "nym-coconut-dkg-common", @@ -6954,7 +7359,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "sha2 0.10.8", + "sha2 0.10.9", "sqlx", "thiserror 2.0.12", "time", @@ -6970,7 +7375,7 @@ version = "0.1.0" dependencies = [ "bytes", "futures", - "humantime 2.2.0", + "humantime", "nym-crypto", "nym-task", "nym-validator-client", @@ -7002,7 +7407,7 @@ name = "nym-vpn-api-lib-wasm" version = "0.1.0" dependencies = [ "bs58", - "getrandom 0.2.15", + "getrandom 0.2.16", "js-sys", "nym-bin-common", "nym-compact-ecash", @@ -7037,25 +7442,50 @@ dependencies = [ "nym-vesting-contract-common", "serde", "serde_json", - "strum 0.23.0", + "strum", + "strum_macros", "ts-rs", ] +[[package]] +name = "nym-wg-gateway-client" +version = "0.1.0" +dependencies = [ + "nym-authenticator-client", + "nym-authenticator-requests", + "nym-bandwidth-controller", + "nym-credentials-interface", + "nym-crypto", + "nym-node-requests", + "nym-pemstore", + "nym-sdk", + "nym-statistics-common", + "nym-validator-client", + "rand 0.8.5", + "thiserror 2.0.12", + "tracing", + "url", +] + [[package]] name = "nym-wireguard" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", "bincode", "chrono", "dashmap", "defguard_wireguard_rs", + "dyn-clone", "futures", "ip_network", "log", "nym-authenticator-requests", "nym-credential-verification", + "nym-credentials-interface", "nym-crypto", + "nym-gateway-requests", "nym-gateway-storage", "nym-network-defaults", "nym-node-metrics", @@ -7069,6 +7499,68 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +dependencies = [ + "async-trait", + "nym-http-api-client", + "nym-wireguard-private-metadata-shared", + "tracing", +] + +[[package]] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.7.9", + "futures", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-shared", + "tokio", + "tokio-util", + "tower-http 0.5.2", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +dependencies = [ + "axum 0.7.9", + "bincode", + "nym-credentials-interface", + "schemars 0.8.22", + "serde", + "thiserror 2.0.12", + "utoipa", +] + +[[package]] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +dependencies = [ + "async-trait", + "axum 0.7.9", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-client", + "nym-wireguard-private-metadata-server", + "nym-wireguard-private-metadata-shared", + "tokio", + "tower-http 0.5.2", + "utoipa", +] + [[package]] name = "nym-wireguard-types" version = "0.1.0" @@ -7086,7 +7578,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.18" +version = "0.1.26" dependencies = [ "anyhow", "bytes", @@ -7095,17 +7587,17 @@ dependencies = [ "flate2", "futures", "hex", - "humantime 2.2.0", + "humantime", "humantime-serde", "nix 0.27.1", "nym-async-file-watcher", "nym-bin-common", "nym-config", "nym-task", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "tar", "thiserror 2.0.12", "time", @@ -7129,15 +7621,15 @@ dependencies = [ "nym-task", "nym-validator-client", "nyxd-scraper", - "reqwest 0.12.15", - "schemars", + "reqwest 0.12.22", + "schemars 0.8.22", "serde", "sqlx", "thiserror 2.0.12", "time", "tokio", "tokio-util", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-subscriber", "utoipa", @@ -7149,14 +7641,15 @@ dependencies = [ name = "nyxd-scraper" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "const_format", "cosmrs", "eyre", "futures", - "humantime 2.2.0", + "humantime", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "sqlx", "tendermint", "tendermint-rpc", @@ -7169,6 +7662,25 @@ dependencies = [ "url", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -7183,12 +7695,22 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "oorandom" -version = "11.1.4" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "opaque-debug" @@ -7210,9 +7732,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -7340,7 +7862,19 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", ] [[package]] @@ -7360,9 +7894,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -7370,9 +7904,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -7400,9 +7934,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "peg" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" dependencies = [ "peg-macros", "peg-runtime", @@ -7410,9 +7944,9 @@ dependencies = [ [[package]] name = "peg-macros" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" dependencies = [ "peg-runtime", "proc-macro2", @@ -7421,9 +7955,9 @@ dependencies = [ [[package]] name = "peg-runtime" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" [[package]] name = "pem" @@ -7453,9 +7987,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -7464,9 +7998,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -7474,26 +8008,25 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.7.15" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -7503,7 +8036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.10.0", ] [[package]] @@ -7546,7 +8079,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -7575,7 +8108,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -7613,9 +8146,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plain" @@ -7692,9 +8225,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -7718,8 +8251,8 @@ dependencies = [ "hmac", "md-5", "memchr", - "rand 0.9.0", - "sha2 0.10.8", + "rand 0.9.2", + "sha2 0.10.9", "stringprep", ] @@ -7734,6 +8267,15 @@ dependencies = [ "postgres-protocol", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -7742,11 +8284,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -7765,16 +8307,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "pretty_env_logger" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" -dependencies = [ - "env_logger 0.7.1", - "log", -] - [[package]] name = "primeorder" version = "0.13.6" @@ -7803,23 +8335,32 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] -name = "prometheus" -version = "0.13.4" +name = "proc_pidinfo" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +checksum = "29492a7b48a00ab80202528e235d2f80a04ccff3747540b4ec6881f2f2bc42d1" +dependencies = [ + "libc", +] + +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" dependencies = [ "cfg-if", "fnv", @@ -7827,17 +8368,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 1.0.69", -] - -[[package]] -name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive 0.11.9", + "thiserror 2.0.12", ] [[package]] @@ -7847,20 +8378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive 0.13.5", -] - -[[package]] -name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", + "prost-derive", ] [[package]] @@ -7873,29 +8391,43 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "prost-types" -version = "0.11.9" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost 0.11.9", + "prost", ] [[package]] name = "protobuf" -version = "2.28.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] [[package]] name = "psl" -version = "2.1.86" +version = "2.1.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e02ed846877ce4044391085ca68b470b0d379cd18a9be0666161764d35448" +checksum = "45f621acfbd2ca5670eee9a95270747dfa9a29e63e1937d7e6a1ac6994331966" dependencies = [ "psl-types", ] @@ -7916,12 +8448,6 @@ dependencies = [ "psl-types", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -7930,9 +8456,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases", @@ -7940,8 +8466,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.25", - "socket2", + "rustls 0.23.29", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -7950,16 +8476,17 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.3.1", - "rand 0.9.0", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.25", + "rustls 0.23.29", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -7970,14 +8497,14 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -7991,6 +8518,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -8010,13 +8543,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.1", - "zerocopy 0.8.20", + "rand_core 0.9.3", ] [[package]] @@ -8036,7 +8568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.1", + "rand_core 0.9.3", ] [[package]] @@ -8045,17 +8577,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", - "zerocopy 0.8.20", + "getrandom 0.3.3", ] [[package]] @@ -8090,11 +8621,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] [[package]] @@ -8103,11 +8634,31 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "regex" version = "1.11.1" @@ -8163,7 +8714,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -8195,9 +8746,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "async-compression", "base64 0.22.1", @@ -8208,18 +8759,14 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper-rustls 0.27.7", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.25", - "rustls-pemfile 2.2.0", + "rustls 0.23.29", "rustls-pki-types", "serde", "serde_json", @@ -8227,38 +8774,32 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.2", - "tokio-socks", "tokio-util", "tower 0.5.2", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.8", - "windows-registry", + "webpki-roots 1.0.2", ] [[package]] name = "reserve-port" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "359fc315ed556eb0e42ce74e76f4b1cd807b50fa6307f3de4e51f92dbe86e2d5" +checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" dependencies = [ - "lazy_static", "thiserror 2.0.12", ] [[package]] name = "resolv-conf" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" -dependencies = [ - "hostname", - "quick-error 1.2.3", -] +checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" [[package]] name = "rfc6979" @@ -8272,57 +8813,18 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rinja" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5" -dependencies = [ - "itoa", - "rinja_derive", -] - -[[package]] -name = "rinja_derive" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b" -dependencies = [ - "basic-toml", - "memchr", - "mime", - "mime_guess", - "proc-macro2", - "quote", - "rinja_parser", - "rustc-hash", - "serde", - "syn 2.0.98", -] - -[[package]] -name = "rinja_parser" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610" -dependencies = [ - "memchr", - "nom", - "serde", -] - [[package]] name = "ripemd" version = "0.1.3" @@ -8360,14 +8862,14 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] name = "rsa" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest 0.10.7", @@ -8377,6 +8879,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle 2.6.1", @@ -8385,9 +8888,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.5.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -8396,24 +8899,24 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.5.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.98", + "syn 2.0.104", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.5.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", "walkdir", ] @@ -8434,10 +8937,25 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "rust-multipart-rfc7578_2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "mime", + "rand 0.9.2", + "thiserror 2.0.12", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -8469,7 +8987,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -8478,15 +8996,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.1" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.9.2", - "windows-sys 0.59.0", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", ] [[package]] @@ -8517,14 +9035,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.4", "subtle 2.6.1", "zeroize", ] @@ -8574,11 +9093,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] @@ -8604,9 +9124,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -8615,15 +9135,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -8657,6 +9177,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -8666,7 +9210,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.29.1", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -8692,13 +9236,13 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -8719,7 +9263,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -8748,9 +9292,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" dependencies = [ "cc", ] @@ -8761,7 +9305,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -8859,7 +9403,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -8870,7 +9414,7 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -8881,14 +9425,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -8943,14 +9487,14 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "serde_path_to_error" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ "itoa", "serde", @@ -8964,14 +9508,14 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -8990,15 +9534,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.1", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -9008,14 +9554,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -9024,7 +9570,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -9077,9 +9623,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -9109,9 +9655,9 @@ checksum = "b72e7cd0744e007e382ba320435f1ed1ecd709409b4ebd5cfbc843d77b25a8aa" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -9130,9 +9676,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -9167,12 +9713,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "sluice" @@ -9187,9 +9730,12 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smawk" @@ -9220,15 +9766,41 @@ dependencies = [ ] [[package]] -name = "socket2" -version = "0.5.8" +name = "snow" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2 0.10.6", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "rustc_version 0.4.1", + "sha2 0.10.9", + "subtle 2.6.1", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "sphinx-packet" version = "0.6.0" @@ -9249,7 +9821,7 @@ dependencies = [ "lioness", "rand 0.8.5", "rand_distr", - "sha2 0.10.8", + "sha2 0.10.9", "subtle 2.6.1", "x25519-dalek", "zeroize", @@ -9274,21 +9846,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -9299,96 +9861,89 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "ahash", - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "chrono", "crc", "crossbeam-queue", "either", - "event-listener 2.5.3", - "futures-channel", + "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.15.4", "hashlink", - "hex", - "indexmap 2.7.1", + "indexmap 2.10.0", "log", "memchr", "once_cell", - "paste", "percent-encoding", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls 0.23.29", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tracing", "url", - "webpki-roots 0.25.4", + "webpki-roots 0.26.11", ] [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.104", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", - "heck 0.4.1", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", "quote", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", - "tempfile", + "syn 2.0.104", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", - "base64 0.21.7", - "bitflags 2.8.0", + "base64 0.22.1", + "bitflags 2.9.1", "byteorder", "bytes", "chrono", @@ -9414,25 +9969,38 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "tracing", "whoami", ] +[[package]] +name = "sqlx-pool-guard" +version = "0.1.0" +dependencies = [ + "proc_pidinfo", + "sqlx", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "windows", +] + [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", - "base64 0.21.7", - "bitflags 2.8.0", + "base64 0.22.1", + "bitflags 2.9.1", "byteorder", "chrono", "crc", @@ -9440,7 +10008,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -9454,11 +10021,11 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.12", "time", "tracing", "whoami", @@ -9466,9 +10033,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -9482,11 +10049,12 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", + "thiserror 2.0.12", "time", "tracing", "url", - "urlencoding", ] [[package]] @@ -9513,9 +10081,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "string_cache" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", @@ -9526,9 +10094,9 @@ dependencies = [ [[package]] name = "string_cache_codegen" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244292f3441c89febe5b5bdfbb6863aeaf4f64da810ea3050fd927b27b8d92ce" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ "phf_generator", "phf_shared", @@ -9555,46 +10123,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.23.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.23.1", -] - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", + "strum_macros", ] [[package]] name = "strum_macros" -version = "0.23.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -9624,6 +10169,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.16", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "syn" version = "1.0.109" @@ -9637,9 +10195,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.98" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -9663,27 +10221,27 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "rayon", - "windows 0.57.0", + "objc2-core-foundation", + "objc2-io-kit", + "windows", ] [[package]] @@ -9732,22 +10290,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand 2.3.0", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.1", + "rustix 1.0.8", "windows-sys 0.59.0", ] [[package]] name = "tendermint" -version = "0.40.1" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9703e34d940c2a293804752555107f8dbe2b84ec4c6dd5203831235868105d2" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" dependencies = [ "bytes", "digest 0.10.7", @@ -9758,13 +10316,13 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.13.5", + "prost", "ripemd", "serde", "serde_bytes", "serde_json", "serde_repr", - "sha2 0.10.8", + "sha2 0.10.9", "signature", "subtle 2.6.1", "subtle-encoding", @@ -9775,27 +10333,27 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.40.1" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc3ea9a39b7ee34eefcff771cc067ecaa0c988c1c5ac08defd878471a06f76" +checksum = "069d1791f9b02a596abcd26eb72003b2e9906c6169a60fa82ffc080dd3a43fda" dependencies = [ "flex-error", "serde", "serde_json", "tendermint", - "toml 0.8.20", + "toml 0.8.23", "url", ] [[package]] name = "tendermint-proto" -version = "0.40.1" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9e1705aa0fa5ecb2c6aa7fb78c2313c4a31158ea5f02048bf318f849352eb" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" dependencies = [ "bytes", "flex-error", - "prost 0.13.5", + "prost", "serde", "serde_bytes", "subtle-encoding", @@ -9804,16 +10362,16 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.40.1" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a52aa504c63ec05519e31348d3f4ba2fe79493c588e2cad5323d5e81b161a" +checksum = "35e0569a4b4cc42ff00df5a665be2858a39ff79df4790b176f1cd0e169bc0fc2" dependencies = [ "async-trait", "async-tungstenite", "bytes", "flex-error", "futures", - "getrandom 0.2.15", + "getrandom 0.2.16", "peg", "pin-project", "rand 0.8.5", @@ -9856,6 +10414,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-with" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f370b9efbfbbc5f057cbce9888373eaeb146a3095bb8cc869b199c94d15559" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "regex", + "syn 2.0.104", +] + [[package]] name = "testnet-manager" version = "0.1.0" @@ -9867,6 +10438,7 @@ dependencies = [ "console", "cw-utils", "dkg-bypass-contract", + "humantime", "indicatif", "nym-bin-common", "nym-coconut-dkg-common", @@ -9879,6 +10451,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-pemstore", + "nym-performance-contract-common", "nym-validator-client", "nym-vesting-contract-common", "rand 0.8.5", @@ -9889,7 +10462,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", - "toml 0.8.20", + "toml 0.8.23", "tracing", "url", "zeroize", @@ -9897,9 +10470,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", ] @@ -9930,7 +10503,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -9941,17 +10514,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -10012,9 +10584,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -10032,9 +10604,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -10047,31 +10619,23 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", - "mio 1.0.3", + "mio 1.0.4", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite", - "tokio", + "windows-sys 0.59.0", ] [[package]] @@ -10082,7 +10646,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -10104,8 +10668,8 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.9.0", - "socket2", + "rand 0.9.2", + "socket2 0.5.10", "tokio", "tokio-util", "whoami", @@ -10138,19 +10702,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.25", - "tokio", -] - -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror 1.0.69", + "rustls 0.23.29", "tokio", ] @@ -10208,15 +10760,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.4", "pin-project-lite", "slab", "tokio", @@ -10233,9 +10785,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -10245,46 +10797,55 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] [[package]] -name = "tonic" -version = "0.9.2" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ + "async-stream", "async-trait", - "axum 0.6.20", - "base64 0.21.7", + "axum 0.7.9", + "base64 0.22.1", "bytes", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "h2 0.4.11", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", - "prost 0.11.9", + "prost", + "socket2 0.5.10", "tokio", "tokio-stream", "tower 0.4.13", @@ -10336,7 +10897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "async-compression", - "bitflags 2.8.0", + "bitflags 2.9.1", "bytes", "futures-core", "futures-util", @@ -10356,6 +10917,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -10382,20 +10961,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -10413,9 +10992,9 @@ dependencies = [ [[package]] name = "tracing-indicatif" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1" +checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200" dependencies = [ "indicatif", "tracing", @@ -10558,7 +11137,7 @@ checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "termcolor", ] @@ -10585,7 +11164,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -10656,15 +11235,15 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -10689,9 +11268,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -10699,56 +11278,54 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "uniffi" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe34585ac0275accf6c284d0080cc2840f3898c551cda869ec291b5a4218712c" +checksum = "b334fd69b3cf198b63616c096aabf9820ab21ed9b2aa1367ddd4b411068bf520" dependencies = [ "anyhow", "camino", - "cargo_metadata 0.15.4", + "cargo_metadata 0.19.2", "clap", "uniffi_bindgen", "uniffi_build", "uniffi_core", "uniffi_macros", + "uniffi_pipeline", ] [[package]] name = "uniffi_bindgen" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a792af1424cc8b3c43b44c1a6cb7935ed1fbe5584a74f70e8bab9799740266d" +checksum = "2ff0132b533483cf19abb30bba5c72c24d9f3e4d9a2ff71cb3e22e73899fd46e" dependencies = [ "anyhow", + "askama", "camino", - "cargo_metadata 0.15.4", + "cargo_metadata 0.19.2", "fs-err", "glob", "goblin", "heck 0.5.0", + "indexmap 2.10.0", "once_cell", - "paste", - "rinja", "serde", + "tempfile", "textwrap", "toml 0.5.11", + "uniffi_internal_macros", "uniffi_meta", + "uniffi_pipeline", "uniffi_udl", ] [[package]] name = "uniffi_build" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c4138211f2ae951018fcce6a978e1fcd1a47c3fd0bc0d5472a520520060db1" +checksum = "0d84d607076008df3c32dd2100ee4e727269f11d3faa35691af70d144598f666" dependencies = [ "anyhow", "camino", @@ -10757,9 +11334,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c18baace68a52666d33d12d73ca335ecf27a302202cefb53b1f974512bb72417" +checksum = "53e3b997192dc15ef1778c842001811ec7f241a093a693ac864e1fc938e64fa9" dependencies = [ "anyhow", "bytes", @@ -10769,19 +11346,22 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9902d4ed16c65e6c0222241024dd0bfeed07ea3deb7c470eb175e5f5ef406cd" +checksum = "f64bec2f3a33f2f08df8150e67fa45ba59a2ca740bf20c1beb010d4d791f9a1b" dependencies = [ + "anyhow", + "indexmap 2.10.0", + "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "uniffi_macros" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d82c82ef945c51082d8763635334b994e63e77650f09d0fae6d28dd08b1de83" +checksum = "5d8708716d2582e4f3d7e9f320290b5966eb951ca421d7630571183615453efc" dependencies = [ "camino", "fs-err", @@ -10789,27 +11369,41 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.98", + "syn 2.0.104", "toml 0.5.11", "uniffi_meta", ] [[package]] name = "uniffi_meta" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6027b971c2aa86350dd180aee9819729c7b99bacd381534511ff29d2c09cea" +checksum = "3d226fc167754ce548c5ece9828c8a06f03bf1eea525d2659ba6bd648bd8e2f3" dependencies = [ "anyhow", "siphasher 0.3.11", "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b925b6421df15cf4bedee27714022cd9626fb4d7eee0923522a608b274ba4371" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.10.0", + "tempfile", + "uniffi_internal_macros", ] [[package]] name = "uniffi_udl" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52300b7a4ab02dc159a038a13d5bfe27aefbad300d91b0b501b3dda094c1e0a2" +checksum = "9c42649b721df759d9d4692a376b82b62ce3028ec9fc466f4780fb8cdf728996" dependencies = [ "anyhow", "textwrap", @@ -10817,6 +11411,12 @@ dependencies = [ "weedle2", ] +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + [[package]] name = "universal-hash" version = "0.5.1" @@ -10863,12 +11463,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -10883,11 +11477,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.10.0", "serde", "serde_json", "utoipa-gen", @@ -10895,14 +11489,14 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.98", + "syn 2.0.104", "uuid", ] @@ -10941,7 +11535,7 @@ checksum = "268d76aaebb80eba79240b805972e52d7d410d4bcc52321b951318b0f440cd60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -10952,18 +11546,20 @@ checksum = "382673bda1d05c85b4550d32fd4192ccd4cffe9a908543a0795d1e7682b36246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "utoipauto-core", ] [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -10978,7 +11574,7 @@ dependencies = [ "nym-validator-client", "serde", "serde_json", - "strum 0.26.3", + "strum", "time", "tokio", "tracing", @@ -11077,15 +11673,15 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -11096,6 +11692,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -11118,7 +11723,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -11153,7 +11758,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -11188,7 +11793,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] @@ -11229,7 +11834,7 @@ name = "wasm-storage" version = "0.1.0" dependencies = [ "async-trait", - "getrandom 0.2.15", + "getrandom 0.2.16", "indexed_db_futures", "js-sys", "nym-store-cipher", @@ -11259,7 +11864,7 @@ version = "0.1.0" dependencies = [ "console_error_panic_hook", "futures", - "getrandom 0.2.15", + "getrandom 0.2.16", "gloo-net", "gloo-utils 0.2.0", "js-sys", @@ -11271,9 +11876,9 @@ dependencies = [ [[package]] name = "wasmtimer" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" +checksum = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446" dependencies = [ "futures", "js-sys", @@ -11303,6 +11908,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webpki-roots" version = "0.24.0" @@ -11320,9 +11937,18 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -11338,9 +11964,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ "redox_syscall", "wasite", @@ -11349,9 +11975,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -11386,161 +12012,102 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.57.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", ] [[package]] -name = "windows" -version = "0.58.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-core", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-core" -version = "0.57.0" +name = "windows-future" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result 0.3.1", - "windows-strings 0.3.1", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" +name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-targets 0.52.6", + "windows-core", + "windows-link", ] [[package]] name = "windows-result" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -11581,6 +12148,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -11629,9 +12205,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -11643,6 +12219,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -11825,9 +12410,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -11844,24 +12429,18 @@ dependencies = [ [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" @@ -11886,13 +12465,12 @@ dependencies = [ [[package]] name = "xattr" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" dependencies = [ "libc", - "linux-raw-sys 0.4.15", - "rustix 0.38.44", + "rustix 1.0.8", ] [[package]] @@ -11903,9 +12481,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -11915,75 +12493,54 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" -dependencies = [ - "zerocopy-derive 0.8.20", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", "synstructure", ] @@ -12004,14 +12561,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -12020,27 +12588,27 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.104", ] [[package]] name = "zip" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938cc23ac49778ac8340e366ddc422b2227ea176edb447e23fc0627608dddadd" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.7.1", + "indexmap 2.10.0", "memchr", "thiserror 2.0.12", "zopfli", @@ -12053,7 +12621,7 @@ dependencies = [ "anyhow", "async-trait", "bs58", - "getrandom 0.2.15", + "getrandom 0.2.16", "js-sys", "nym-bin-common", "nym-compact-ecash", @@ -12061,7 +12629,7 @@ dependencies = [ "nym-crypto", "nym-http-api-client", "rand 0.8.5", - "reqwest 0.12.15", + "reqwest 0.12.22", "serde", "thiserror 2.0.12", "tokio", @@ -12075,42 +12643,57 @@ dependencies = [ [[package]] name = "zopfli" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] [[package]] name = "zstd" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.2.1" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zulip-client" +version = "0.1.0" +dependencies = [ + "itertools 0.14.0", + "nym-bin-common", + "nym-http-api-client", + "reqwest 0.12.22", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tracing", + "url", + "zeroize", +] diff --git a/Cargo.toml b/Cargo.toml index 2c57ccc28b..4fa01436df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,12 +33,17 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-dkg", - "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr", + "common/cosmwasm-smart-contracts/contracts-common", + "common/cosmwasm-smart-contracts/contracts-common-testing", + "common/cosmwasm-smart-contracts/easy_addr", "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/multisig-contract", + "common/cosmwasm-smart-contracts/nym-performance-contract", + "common/cosmwasm-smart-contracts/nym-pool-contract", "common/cosmwasm-smart-contracts/vesting-contract", + "common/credential-proxy", "common/credential-storage", "common/credential-utils", "common/credential-verification", @@ -46,8 +51,9 @@ members = [ "common/credentials-interface", "common/crypto", "common/dkg", + "common/ecash-signer-check", + "common/ecash-signer-check-types", "common/ecash-time", - "common/execute", "common/exit-policy", "common/gateway-requests", "common/gateway-stats-storage", @@ -64,6 +70,8 @@ members = [ "common/nym-id", "common/nym-metrics", "common/nym_offline_compact_ecash", + "common/nymnoise", + "common/nymnoise/keys", "common/nymsphinx", "common/nymsphinx/acknowledgements", "common/nymsphinx/addressing", @@ -84,26 +92,32 @@ members = [ "common/socks5/requests", "common/statistics", "common/store-cipher", - "common/task", + "common/task", "common/test-utils", "common/ticketbooks-merkle", "common/topology", "common/tun", - "common/types", + "common/types", "common/upgrade-mode-check", "common/verloc", "common/wasm/client-core", "common/wasm/storage", "common/wasm/utils", "common/wireguard", + "common/wireguard-private-metadata/client", + "common/wireguard-private-metadata/server", + "common/wireguard-private-metadata/shared", + "common/wireguard-private-metadata/tests", "common/wireguard-types", + "common/zulip-client", "documentation/autodoc", "gateway", - "integrations/bity", "nym-api", "nym-api/nym-api-requests", + "nym-authenticator-client", "nym-browser-extension/storage", "nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy-requests", "nym-credential-proxy/vpn-api-lib-wasm", + "nym-ip-packet-client", "nym-network-monitor", "nym-node", "nym-node-status-api/nym-node-status-agent", @@ -111,18 +125,19 @@ members = [ "nym-node-status-api/nym-node-status-client", "nym-node/nym-node-metrics", "nym-node/nym-node-requests", - "nym-outfox", + "nym-outfox", "nym-signers-monitor", + "nym-statistics-api", "nym-validator-rewarder", + "nym-wg-gateway-client", "nyx-chain-watcher", "sdk/ffi/cpp", "sdk/ffi/go", "sdk/ffi/shared", "sdk/rust/nym-sdk", - "service-providers/authenticator", "service-providers/common", "service-providers/ip-packet-router", "service-providers/network-requester", - "tools/echo-server", + "sqlx-pool-guard", "tools/echo-server", "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", @@ -132,7 +147,7 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", - "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", + "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", "tools/nym-nr-query", @@ -153,20 +168,15 @@ default-members = [ "nym-node", "nym-node-status-api/nym-node-status-agent", "nym-node-status-api/nym-node-status-api", + "nym-statistics-api", "nym-validator-rewarder", "nyx-chain-watcher", - "service-providers/authenticator", "service-providers/ip-packet-router", "service-providers/network-requester", "tools/nymvisor", ] -exclude = [ - "explorer", - "contracts", - "nym-wallet", - "cpu-cycles", -] +exclude = ["explorer", "contracts", "nym-wallet", "cpu-cycles"] [workspace.package] authors = ["Nym Technologies SA"] @@ -175,7 +185,7 @@ homepage = "https://nymtech.net" documentation = "https://nymtech.net" edition = "2021" license = "Apache-2.0" -rust-version = "1.80" +rust-version = "1.81" readme = "README.md" [workspace.dependencies] @@ -185,7 +195,7 @@ aes = "0.8.1" aes-gcm = "0.10.1" aes-gcm-siv = "0.11.1" ammonia = "4" -anyhow = "1.0.97" +anyhow = "1.0.98" arc-swap = "1.7.1" argon2 = "0.5.0" async-trait = "0.1.88" @@ -204,20 +214,20 @@ bloomfilter = "3.0.1" bs58 = "0.5.1" bytecodec = "0.4.15" bytes = "1.10.1" -cargo_metadata = "0.18.1" +cargo_metadata = "0.19.2" celes = "2.6.0" cfg-if = "1.0.0" chacha20 = "0.9.0" chacha20poly1305 = "0.10.1" -chrono = "0.4.40" +chrono = "0.4.41" cipher = "0.4.3" -clap = "4.5.34" +clap = "4.5.38" clap_complete = "4.5" clap_complete_fig = "4.5" colored = "2.2" comfy-table = "7.1.4" -console = "0.15.11" -console-subscriber = "0.1.1" +console = "0.16.0" +console-subscriber = "0.4.1" console_error_panic_hook = "0.1" const-str = "0.5.6" const_format = "0.2.34" @@ -233,15 +243,16 @@ digest = "0.10.7" dirs = "5.0" doc-comment = "0.3" dotenvy = "0.15.6" +dyn-clone = "1.0.19" ecdsa = "0.16" ed25519-dalek = "2.1" encoding_rs = "0.8.35" -env_logger = "0.11.7" +env_logger = "0.11.8" envy = "0.4" etherparse = "0.13.0" eyre = "0.6.9" fastrand = "2.1.1" -flate2 = "1.1.0" +flate2 = "1.1.1" futures = "0.3.31" futures-util = "0.3" generic-array = "0.14.7" @@ -251,7 +262,7 @@ handlebars = "3.5.5" headers = "0.4.0" hex = "0.4.3" hex-literal = "0.3.3" -hickory-resolver = "0.24.4" +hickory-resolver = "0.25" hkdf = "0.12.3" hmac = "0.12.1" http = "1" @@ -262,11 +273,12 @@ humantime = "2.2.0" humantime-serde = "1.1.1" hyper = "1.6.0" hyper-util = "0.1" -indicatif = "0.17.11" +indicatif = "0.18.0" inquire = "0.6.2" ip_network = "0.4.1" ipnetwork = "0.20" itertools = "0.14.0" +jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] } k256 = "0.13" lazy_static = "1.5.0" ledger-transport = "0.10.0" @@ -286,8 +298,8 @@ pem = "0.8" petgraph = "0.6.5" pin-project = "1.1" pin-project-lite = "0.2.16" -pretty_env_logger = "0.4.0" publicsuffix = "2.3.0" +proc_pidinfo = "0.1.3" quote = "1" rand = "0.8.5" rand_chacha = "0.3" @@ -310,28 +322,30 @@ serde_json_path = "0.7.2" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" -sha2 = "0.10.8" +sha2 = "0.10.9" si-scale = "0.2.3" +snow = "0.9.6" sphinx-packet = "=0.6.0" -sqlx = "0.7.4" -strum = "0.26" -strum_macros = "0.26" +sqlx = "0.8.6" +strum = "0.27.2" +strum_macros = "0.27.2" subtle-encoding = "0.5" -syn = "1" -sysinfo = "0.33.0" +syn = "2" +sysinfo = "0.37.0" tap = "1.0.1" tar = "0.4.44" -tempfile = "3.19" +test-with = { version = "0.15.4", default-features = false } +tempfile = "3.20" thiserror = "2.0" time = "0.3.41" -tokio = "1.44" +tokio = "1.47" tokio-postgres = "0.7" tokio-stream = "0.1.17" tokio-test = "0.4.4" tokio-tun = "0.11.5" tokio-tungstenite = { version = "0.20.1" } -tokio-util = "0.7.14" -toml = "0.8.20" +tokio-util = "0.7.15" +toml = "0.8.22" tower = "0.5.2" tower-http = "0.5.2" tracing = "0.1.41" @@ -342,7 +356,7 @@ tracing-tree = "0.2.2" tracing-indicatif = "0.3.9" ts-rs = "10.1.0" tungstenite = { version = "0.20.1", default-features = false } -uniffi = "0.29.1" +uniffi = "0.29.2" uniffi_build = "0.29.0" url = "2.5" utoipa = "5.2" @@ -351,11 +365,10 @@ utoipauto = "0.2" uuid = "*" vergen = { version = "=8.3.1", default-features = false } walkdir = "2" -wasm-bindgen-test = "0.3.49" x25519-dalek = "2.0.0" zeroize = "1.7.0" -prometheus = { version = "0.13.0" } +prometheus = { version = "0.14.0" } # coconut/DKG related # unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork @@ -369,9 +382,6 @@ subtle = "2.5.0" # cosmwasm-related cosmwasm-schema = "=2.2.2" cosmwasm-std = "=2.2.2" -# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1 -# (and ideally we don't want to pull the same dependency twice) -serde-json-wasm = "=1.0.1" # same version as used by cosmwasm cw-utils = "=2.0.0" cw-storage-plus = "=2.0.0" @@ -379,26 +389,28 @@ cw2 = { version = "=2.0.0" } cw3 = { version = "=2.0.0" } cw4 = { version = "=2.0.0" } cw-controllers = { version = "=2.0.0" } +cw-multi-test = "=2.3.2" # cosmrs-related bip32 = { version = "0.5.3", default-features = false } cosmrs = { version = "0.21.1" } -tendermint = "0.40.0" -tendermint-rpc = "0.40.0" +tendermint = "0.40.4" +tendermint-rpc = "0.40.4" prost = { version = "0.13", default-features = false } # wasm-related dependencies gloo-utils = "0.2.0" gloo-net = "0.6.0" -indexed_db_futures = "0.6.1" +indexed_db_futures = "0.6.4" js-sys = "0.3.76" serde-wasm-bindgen = "0.6.5" tsify = "0.4.5" wasm-bindgen = "0.2.99" wasm-bindgen-futures = "0.4.49" +wasm-bindgen-test = "0.3.49" wasmtimer = "0.4.1" web-sys = "0.3.76" @@ -434,6 +446,9 @@ opt-level = 'z' # lto = true opt-level = 'z' +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } + [workspace.lints.clippy] unwrap_used = "deny" expect_used = "deny" diff --git a/Makefile b/Makefile index 799abcfd76..c69797c9e3 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,11 @@ help: @echo " clippy: run clippy for all workspaces" @echo " test: run clippy, unit tests, and formatting." @echo " test-all: like test, but also includes the expensive tests" - @echo " deb: build debian packages + @echo " deb: build debian packages" + @echo "" + @echo "Contract building targets:" + @echo " contracts: build contracts for development (includes wasm-opt)" + @echo " publish-contracts: build contracts using Docker optimizer (deterministic)" # ----------------------------------------------------------------------------- # Meta targets @@ -130,25 +134,69 @@ cargo-test: sdk-wasm-test clippy: sdk-wasm-lint # ----------------------------------------------------------------------------- -# Build contracts ready for deploy +# Build CosmWasm contracts (deterministic docker build) # ----------------------------------------------------------------------------- -CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg -CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS)) -CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release -contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts +WASM_CONTRACT_DIR := contracts/target/wasm32-unknown-unknown/release +# Find every direct contract folder that contains a Cargo.toml +CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u) + +CONTRACTS_OUT_DIR = contracts/artifacts + +# Build all contracts via the official CosmWasm optimizer image (one invocation per contract) +# See : https://github.com/CosmWasm/optimizer?tab=readme-ov-file#contracts-excluded-from-workspace +# The optimizer ships separate multi-arch images. ARM builds are *not* bit-for-bit identical to the +# canonical x86_64 build (see README notice in CosmWasm/optimizer). For reproducible artefacts we +# therefore always run the amd64 variant by default. +# Override with : +# $ COSMWASM_OPTIMIZER_IMAGE=cosmwasm/optimizer-arm64:0.17.0 make contracts-publish +# +COSMWASM_OPTIMIZER_IMAGE ?= cosmwasm/optimizer:0.17.0 +COSMWASM_OPTIMIZER_PLATFORM ?= linux/amd64 + +# Ensure clean build environment and run the optimizer +optimize-contracts: + @rm -rf artifacts 2>/dev/null || true + @echo "=== Ensuring clean build environment" + docker volume rm nym_contracts_cache 2>/dev/null || true + docker volume rm registry_cache 2>/dev/null || true + @for DIR in $(CONTRACT_DIRS); do \ + echo "=== Optimizing $${DIR}"; \ + docker run --rm --platform $(COSMWASM_OPTIMIZER_PLATFORM) \ + -v $(CURDIR):/code \ + --mount type=volume,source=nym_contracts_cache,target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + -e CARGO_BUILD_INCREMENTAL=false \ + -e RUSTFLAGS="-C target-cpu=generic -C debuginfo=0" \ + -e SOURCE_DATE_EPOCH=1 \ + $(COSMWASM_OPTIMIZER_IMAGE) $${DIR}; \ + done + @mkdir -p $(CONTRACTS_OUT_DIR) + @cp artifacts/*.wasm $(CONTRACTS_OUT_DIR)/ 2>/dev/null || true + + @cd $(CONTRACTS_OUT_DIR) && sha256sum *.wasm > checksums.txt + # Cleanup temporary artefacts directory + @rm -rf artifacts 2>/dev/null || true wasm-opt-contracts: - for contract in $(CONTRACTS_WASM); do \ - wasm-opt --signext-lowering -Os $(CONTRACTS_OUT_DIR)/$$contract -o $(CONTRACTS_OUT_DIR)/$$contract; \ + @for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \ + echo "Running wasm-opt on $$WASM"; \ + wasm-opt --signext-lowering -Os $$WASM -o $$WASM ; \ done cosmwasm-check-contracts: - for contract in $(CONTRACTS_WASM); do \ - cosmwasm-check $(CONTRACTS_OUT_DIR)/$$contract; \ + @for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \ + echo "Checking $$WASM"; \ + cosmwasm-check $$WASM ; \ done +# Default development build +contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts + +# Publishing build used by CI – deterministic Docker optimiser +publish-contracts: optimize-contracts cosmwasm-check-contracts + # Consider adding 's' to make plural consistent (beware: used in github workflow) contract-schema: $(MAKE) -C contracts schema diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a626cc874a..a27a046e0b 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.53" +version = "1.1.61" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" @@ -46,6 +46,7 @@ nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-bin-common = { path = "../../common/bin-common", features = [ "output_format", "clap", + "basic_tracing", ] } nym-client-core = { path = "../../common/client-core", features = [ "fs-credentials-storage", diff --git a/clients/native/examples/js-examples/websocket/package-lock.json b/clients/native/examples/js-examples/websocket/package-lock.json index d4b74ccd26..b25778cb52 100644 --- a/clients/native/examples/js-examples/websocket/package-lock.json +++ b/clients/native/examples/js-examples/websocket/package-lock.json @@ -2048,10 +2048,11 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz", - "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -6095,9 +6096,9 @@ } }, "http-proxy-middleware": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz", - "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "requires": { "@types/http-proxy": "^1.17.8", diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index 91eb21875b..6e7a41bc5d 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -25,6 +25,7 @@ pub mod old_config_v1_1_13; pub mod old_config_v1_1_20; pub mod old_config_v1_1_20_2; pub mod old_config_v1_1_33; +pub mod old_config_v1_1_54; mod persistence; mod template; diff --git a/clients/native/src/client/config/old_config_v1_1_33.rs b/clients/native/src/client/config/old_config_v1_1_33.rs index 02ed7218d1..75f36202d5 100644 --- a/clients/native/src/client/config/old_config_v1_1_33.rs +++ b/clients/native/src/client/config/old_config_v1_1_33.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::config::persistence::ClientPaths; -use crate::client::config::{default_config_filepath, Config, Socket, SocketType}; +use crate::client::config::{default_config_filepath, Socket, SocketType}; use crate::error::ClientError; use nym_bin_common::logging::LoggingSettings; use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; @@ -14,6 +14,8 @@ use std::io; use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; +use super::old_config_v1_1_54::ConfigV1_1_54; + #[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)] pub struct ClientPathsV1_1_33 { #[serde(flatten)] @@ -33,6 +35,21 @@ pub struct ConfigV1_1_33 { pub logging: LoggingSettings, } +impl TryFrom for ConfigV1_1_54 { + type Error = ClientError; + + fn try_from(value: ConfigV1_1_33) -> Result { + Ok(ConfigV1_1_54 { + base: value.base.into(), + socket: value.socket.into(), + storage_paths: ClientPaths { + common_paths: value.storage_paths.common_paths.upgrade_default()?, + }, + logging: value.logging, + }) + } +} + impl ConfigV1_1_33 { pub fn read_from_toml_file>(path: P) -> io::Result { read_config_from_toml_file(path) @@ -41,17 +58,6 @@ impl ConfigV1_1_33 { pub fn read_from_default_path>(id: P) -> io::Result { Self::read_from_toml_file(default_config_filepath(id)) } - - pub fn try_upgrade(self) -> Result { - Ok(Config { - base: self.base.into(), - socket: self.socket.into(), - storage_paths: ClientPaths { - common_paths: self.storage_paths.common_paths.upgrade_default()?, - }, - logging: self.logging, - }) - } } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone, Copy)] diff --git a/clients/native/src/client/config/old_config_v1_1_54.rs b/clients/native/src/client/config/old_config_v1_1_54.rs new file mode 100644 index 0000000000..b3eb36080a --- /dev/null +++ b/clients/native/src/client/config/old_config_v1_1_54.rs @@ -0,0 +1,41 @@ +use std::{io, path::Path}; + +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::old_config_v1_1_54::ConfigV1_1_54 as BaseConfigV1_1_54; +use nym_config::read_config_from_toml_file; +use serde::{Deserialize, Serialize}; + +use crate::error::ClientError; + +use super::{default_config_filepath, persistence::ClientPaths, Config, Socket}; + +#[derive(Debug, Deserialize, PartialEq, Serialize, Clone)] +pub struct ConfigV1_1_54 { + #[serde(flatten)] + pub base: BaseConfigV1_1_54, + + pub socket: Socket, + + pub storage_paths: ClientPaths, + + pub logging: LoggingSettings, +} + +impl ConfigV1_1_54 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + base: self.base.into(), + socket: self.socket, + storage_paths: self.storage_paths, + logging: self.logging, + }) + } +} diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index 922f95b63c..1c5dfcf0b8 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -92,10 +92,6 @@ host = '{{ socket.host }}' [debug] -[debug.traffic] -average_packet_delay = '{{ debug.traffic.average_packet_delay }}' -message_sending_average_delay = '{{ debug.traffic.message_sending_average_delay }}' - [debug.acknowledgements] average_ack_delay = '{{ debug.acknowledgements.average_ack_delay }}' diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index d32e3e835f..ce36a5eb84 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -111,7 +111,7 @@ impl SocketClient { let dkg_query_client = if self.config.base.client.disabled_credentials_mode { None } else { - Some(default_query_dkg_client_from_config(&self.config.base)) + Some(default_query_dkg_client_from_config(&self.config.base)?) }; let storage = self.initialise_storage().await?; diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 4be73f95ca..5ab7a7b989 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -5,6 +5,7 @@ use crate::client::config::old_config_v1_1_13::OldConfigV1_1_13; use crate::client::config::old_config_v1_1_20::ConfigV1_1_20; use crate::client::config::old_config_v1_1_20_2::ConfigV1_1_20_2; use crate::client::config::old_config_v1_1_33::ConfigV1_1_33; +use crate::client::config::old_config_v1_1_54::ConfigV1_1_54; use crate::client::config::{BaseClientConfig, Config}; use crate::commands::ecash::Ecash; use crate::error::ClientError; @@ -177,7 +178,8 @@ async fn try_upgrade_v1_1_13_config(id: &str) -> Result { let updated_step2: ConfigV1_1_20_2 = updated_step1.into(); let (updated_step3, gateway_config) = updated_step2.upgrade()?; let old_paths = updated_step3.storage_paths.clone(); - let updated = updated_step3.try_upgrade()?; + let updated_step4: ConfigV1_1_54 = updated_step3.try_into()?; + let updated = updated_step4.try_upgrade()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, @@ -205,7 +207,8 @@ async fn try_upgrade_v1_1_20_config(id: &str) -> Result { let updated_step1: ConfigV1_1_20_2 = old_config.into(); let (updated_step2, gateway_config) = updated_step1.upgrade()?; let old_paths = updated_step2.storage_paths.clone(); - let updated = updated_step2.try_upgrade()?; + let updated_step3: ConfigV1_1_54 = updated_step2.try_into()?; + let updated = updated_step3.try_upgrade()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, @@ -229,7 +232,8 @@ async fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { let (updated_step1, gateway_config) = old_config.upgrade()?; let old_paths = updated_step1.storage_paths.clone(); - let updated = updated_step1.try_upgrade()?; + let updated_step2: ConfigV1_1_54 = updated_step1.try_into()?; + let updated = updated_step2.try_upgrade()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, @@ -252,7 +256,8 @@ async fn try_upgrade_v1_1_33_config(id: &str) -> Result { info!("It is going to get updated to the current specification."); let old_paths = old_config.storage_paths.clone(); - let updated = old_config.try_upgrade()?; + let updated_step1: ConfigV1_1_54 = old_config.try_into()?; + let updated = updated_step1.try_upgrade()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, @@ -265,6 +270,22 @@ async fn try_upgrade_v1_1_33_config(id: &str) -> Result { Ok(true) } +async fn try_upgrade_v1_1_54_config(id: &str) -> Result { + // explicitly load it as v1.1.54 (which is incompatible with the current one, i.e. +1.1.55) + let Ok(old_config) = ConfigV1_1_54::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.54 config template."); + info!("It is going to get updated to the current specification."); + + let updated = old_config.try_upgrade()?; + + updated.save_to_default_location()?; + Ok(true) +} + async fn try_upgrade_config(id: &str) -> Result<(), ClientError> { if try_upgrade_v1_1_13_config(id).await? { return Ok(()); @@ -278,6 +299,9 @@ async fn try_upgrade_config(id: &str) -> Result<(), ClientError> { if try_upgrade_v1_1_33_config(id).await? { return Ok(()); } + if try_upgrade_v1_1_54_config(id).await? { + return Ok(()); + } Ok(()) } diff --git a/clients/native/src/main.rs b/clients/native/src/main.rs index abeb07a647..d00f0b5e51 100644 --- a/clients/native/src/main.rs +++ b/clients/native/src/main.rs @@ -4,7 +4,7 @@ use std::error::Error; use clap::{crate_name, crate_version, Parser}; -use nym_bin_common::logging::{maybe_print_banner, setup_logging}; +use nym_bin_common::logging::{maybe_print_banner, setup_tracing_logger}; use nym_network_defaults::setup_env; pub mod client; @@ -20,7 +20,7 @@ async fn main() -> Result<(), Box> { if !args.no_banner { maybe_print_banner(crate_name!(), crate_version!()); } - setup_logging(); + setup_tracing_logger(); if let Err(err) = commands::execute(args).await { log::error!("{err}"); diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 2df449385d..761e2b9531 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -318,7 +318,7 @@ impl Handler { async fn handle_text_message(&mut self, msg: String) -> Option { debug!("Handling text message request"); - trace!("Content: {:?}", msg); + trace!("Content: {msg:?}"); self.received_response_type = ReceivedResponseType::Text; let client_request = ClientRequest::try_from_text(msg); diff --git a/clients/native/src/websocket/listener.rs b/clients/native/src/websocket/listener.rs index cc36198d40..a1b430a930 100644 --- a/clients/native/src/websocket/listener.rs +++ b/clients/native/src/websocket/listener.rs @@ -68,9 +68,9 @@ impl Listener { new_conn = tcp_listener.accept() => { match new_conn { Ok((mut socket, remote_addr)) => { - debug!("Received connection from {:?}", remote_addr); + debug!("Received connection from {remote_addr:?}"); if self.state.is_connected() { - warn!("Tried to open a duplicate websocket connection. The request came from {}", remote_addr); + warn!("Tried to open a duplicate websocket connection. The request came from {remote_addr}"); // if we've already got a connection, don't allow another one // while we only ever want to accept a single connection, we don't want // to leave clients hanging (and also allow for reconnection if it somehow diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 94ed60ecf0..8f69f57566 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.53" +version = "1.1.61" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" @@ -27,6 +27,7 @@ zeroize = { workspace = true } nym-bin-common = { path = "../../common/bin-common", features = [ "output_format", "clap", + "basic_tracing", ] } nym-client-core = { path = "../../common/client-core", features = [ "fs-credentials-storage", diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 0d16638164..7e821a4a60 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -7,6 +7,7 @@ use crate::config::old_config_v1_1_20::ConfigV1_1_20; use crate::config::old_config_v1_1_20_2::ConfigV1_1_20_2; use crate::config::old_config_v1_1_30::ConfigV1_1_30; use crate::config::old_config_v1_1_33::ConfigV1_1_33; +use crate::config::old_config_v1_1_54::ConfigV1_1_54; use crate::config::{BaseClientConfig, Config}; use crate::error::Socks5ClientError; use clap::CommandFactory; @@ -204,15 +205,16 @@ async fn try_upgrade_v1_1_13_config(id: &str) -> Result let old_paths = updated_step3.storage_paths.clone(); let updated_step4: ConfigV1_1_33 = updated_step3.into(); - let updated = updated_step4.try_upgrade()?; + let updated_step5: ConfigV1_1_54 = updated_step4.try_into()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, - &updated.storage_paths.common_paths, + &updated_step5.storage_paths.common_paths, Some(gateway_config), ) .await?; + let updated = updated_step5.try_upgrade()?; updated.save_to_default_location()?; Ok(true) } @@ -234,15 +236,16 @@ async fn try_upgrade_v1_1_20_config(id: &str) -> Result let old_paths = updated_step2.storage_paths.clone(); let updated_step3: ConfigV1_1_33 = updated_step2.into(); - let updated = updated_step3.try_upgrade()?; + let updated_step4: ConfigV1_1_54 = updated_step3.try_into()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, - &updated.storage_paths.common_paths, + &updated_step4.storage_paths.common_paths, Some(gateway_config), ) .await?; + let updated = updated_step4.try_upgrade()?; updated.save_to_default_location()?; Ok(true) } @@ -261,15 +264,17 @@ async fn try_upgrade_v1_1_20_2_config(id: &str) -> Result Result let old_paths = old_config.storage_paths.clone(); let updated_step1: ConfigV1_1_33 = old_config.into(); - let updated = updated_step1.try_upgrade()?; + let updated_step2: ConfigV1_1_54 = updated_step1.try_into()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, - &updated.storage_paths.common_paths, + &updated_step2.storage_paths.common_paths, None, ) .await?; + let updated = updated_step2.try_upgrade()?; updated.save_to_default_location()?; Ok(true) } @@ -312,15 +318,32 @@ async fn try_upgrade_v1_1_33_config(id: &str) -> Result let old_paths = old_config.storage_paths.clone(); - let updated = old_config.try_upgrade()?; + let updated_step1: ConfigV1_1_54 = old_config.try_into()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, - &updated.storage_paths.common_paths, + &updated_step1.storage_paths.common_paths, None, ) .await?; + let updated = updated_step1.try_upgrade()?; + updated.save_to_default_location()?; + Ok(true) +} + +async fn try_upgrade_v1_1_54_config(id: &str) -> Result { + // explicitly load it as v1.1.54 (which is incompatible with the current one, i.e. +1.1.55) + let Ok(old_config) = ConfigV1_1_54::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.54 config template."); + info!("It is going to get updated to the current specification."); + + let updated = old_config.try_upgrade()?; + updated.save_to_default_location()?; Ok(true) } @@ -341,6 +364,9 @@ async fn try_upgrade_config(id: &str) -> Result<(), Socks5ClientError> { if try_upgrade_v1_1_33_config(id).await? { return Ok(()); } + if try_upgrade_v1_1_54_config(id).await? { + return Ok(()); + } Ok(()) } diff --git a/clients/socks5/src/config/mod.rs b/clients/socks5/src/config/mod.rs index 5f2f07d64d..62726d15bd 100644 --- a/clients/socks5/src/config/mod.rs +++ b/clients/socks5/src/config/mod.rs @@ -25,6 +25,7 @@ pub mod old_config_v1_1_20; pub mod old_config_v1_1_20_2; pub mod old_config_v1_1_30; pub mod old_config_v1_1_33; +pub mod old_config_v1_1_54; mod persistence; mod template; diff --git a/clients/socks5/src/config/old_config_v1_1_33.rs b/clients/socks5/src/config/old_config_v1_1_33.rs index fb14259bf4..c139de9f6d 100644 --- a/clients/socks5/src/config/old_config_v1_1_33.rs +++ b/clients/socks5/src/config/old_config_v1_1_33.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::{default_config_filepath, Config, SocksClientPaths}; +use crate::config::{default_config_filepath, SocksClientPaths}; use crate::error::Socks5ClientError; use nym_bin_common::logging::LoggingSettings; use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; use std::io; use std::path::Path; +use super::old_config_v1_1_54::ConfigV1_1_54; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct SocksClientPathsV1_1_33 { #[serde(flatten)] @@ -28,6 +30,20 @@ pub struct ConfigV1_1_33 { pub logging: LoggingSettings, } +impl TryFrom for ConfigV1_1_54 { + type Error = Socks5ClientError; + + fn try_from(value: ConfigV1_1_33) -> Result { + Ok(ConfigV1_1_54 { + core: value.core.into(), + storage_paths: SocksClientPaths { + common_paths: value.storage_paths.common_paths.upgrade_default()?, + }, + logging: value.logging, + }) + } +} + impl ConfigV1_1_33 { pub fn read_from_toml_file>(path: P) -> io::Result { read_config_from_toml_file(path) @@ -36,14 +52,4 @@ impl ConfigV1_1_33 { pub fn read_from_default_path>(id: P) -> io::Result { Self::read_from_toml_file(default_config_filepath(id)) } - - pub fn try_upgrade(self) -> Result { - Ok(Config { - core: self.core.into(), - storage_paths: SocksClientPaths { - common_paths: self.storage_paths.common_paths.upgrade_default()?, - }, - logging: self.logging, - }) - } } diff --git a/clients/socks5/src/config/old_config_v1_1_54.rs b/clients/socks5/src/config/old_config_v1_1_54.rs new file mode 100644 index 0000000000..ebe7166fa5 --- /dev/null +++ b/clients/socks5/src/config/old_config_v1_1_54.rs @@ -0,0 +1,39 @@ +use std::{io, path::Path}; + +use nym_bin_common::logging::LoggingSettings; +use nym_config::read_config_from_toml_file; +use nym_socks5_client_core::config::old_config_v1_1_54::ConfigV1_1_54 as CoreConfigV1_1_54; +use serde::{Deserialize, Serialize}; + +use crate::config::Config; +use crate::error::Socks5ClientError; + +use super::{default_config_filepath, SocksClientPaths}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_54 { + pub core: CoreConfigV1_1_54, + + pub storage_paths: SocksClientPaths, + + pub logging: LoggingSettings, +} + +impl ConfigV1_1_54 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + core: self.core.into(), + storage_paths: self.storage_paths, + logging: self.logging, + }) + } +} diff --git a/clients/socks5/src/config/template.rs b/clients/socks5/src/config/template.rs index 2a9a2791bd..f170834342 100644 --- a/clients/socks5/src/config/template.rs +++ b/clients/socks5/src/config/template.rs @@ -98,10 +98,6 @@ send_anonymously = {{ core.socks5.send_anonymously }} [core.debug] -[core.debug.traffic] -average_packet_delay = '{{ core.debug.traffic.average_packet_delay }}' -message_sending_average_delay = '{{ core.debug.traffic.message_sending_average_delay }}' - [core.debug.acknowledgements] average_ack_delay = '{{ core.debug.acknowledgements.average_ack_delay }}' diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index 8ad4ddf954..36161d3dcd 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -4,7 +4,7 @@ use std::error::Error; use clap::{crate_name, crate_version, Parser}; -use nym_bin_common::logging::{maybe_print_banner, setup_logging}; +use nym_bin_common::logging::{maybe_print_banner, setup_tracing_logger}; use nym_network_defaults::setup_env; mod commands; @@ -19,7 +19,7 @@ async fn main() -> Result<(), Box> { if !args.no_banner { maybe_print_banner(crate_name!(), crate_version!()); } - setup_logging(); + setup_tracing_logger(); if let Err(err) = commands::execute(args).await { log::error!("{err}"); diff --git a/common/async-file-watcher/src/lib.rs b/common/async-file-watcher/src/lib.rs index 62bb895bcd..0fdeb7da3a 100644 --- a/common/async-file-watcher/src/lib.rs +++ b/common/async-file-watcher/src/lib.rs @@ -137,7 +137,7 @@ impl AsyncFileWatcher { log::error!("the file watcher receiver has been dropped!"); } } else { - log::debug!("will not propagate information about {:?}", event); + log::debug!("will not propagate information about {event:?}"); } } Err(err) => { diff --git a/common/authenticator-requests/src/v1/registration.rs b/common/authenticator-requests/src/v1/registration.rs index da44cf97bf..03cfc30c22 100644 --- a/common/authenticator-requests/src/v1/registration.rs +++ b/common/authenticator-requests/src/v1/registration.rs @@ -108,7 +108,7 @@ impl GatewayClient { #[cfg(feature = "verify")] pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { // use gateways key as a ref to an x25519_dalek key - let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key); + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); // TODO: change that to use our nym_crypto::hmac module instead #[allow(clippy::expect_used)] diff --git a/common/authenticator-requests/src/v2/registration.rs b/common/authenticator-requests/src/v2/registration.rs index cdc9d2180c..436fde5346 100644 --- a/common/authenticator-requests/src/v2/registration.rs +++ b/common/authenticator-requests/src/v2/registration.rs @@ -117,7 +117,7 @@ impl GatewayClient { #[cfg(feature = "verify")] pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { // use gateways key as a ref to an x25519_dalek key - let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key); + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); // TODO: change that to use our nym_crypto::hmac module instead #[allow(clippy::expect_used)] diff --git a/common/authenticator-requests/src/v3/registration.rs b/common/authenticator-requests/src/v3/registration.rs index 8d51a970f1..21a50882ca 100644 --- a/common/authenticator-requests/src/v3/registration.rs +++ b/common/authenticator-requests/src/v3/registration.rs @@ -117,7 +117,7 @@ impl GatewayClient { #[cfg(feature = "verify")] pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { // use gateways key as a ref to an x25519_dalek key - let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key); + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); // TODO: change that to use our nym_crypto::hmac module instead #[allow(clippy::expect_used)] diff --git a/common/authenticator-requests/src/v4/registration.rs b/common/authenticator-requests/src/v4/registration.rs index 09359a1a68..28aadf9c6d 100644 --- a/common/authenticator-requests/src/v4/registration.rs +++ b/common/authenticator-requests/src/v4/registration.rs @@ -169,7 +169,7 @@ impl GatewayClient { #[cfg(feature = "verify")] pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { // use gateways key as a ref to an x25519_dalek key - let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key); + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); // TODO: change that to use our nym_crypto::hmac module instead #[allow(clippy::expect_used)] diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs index 09359a1a68..3c51776ca0 100644 --- a/common/authenticator-requests/src/v5/registration.rs +++ b/common/authenticator-requests/src/v5/registration.rs @@ -28,8 +28,6 @@ pub type HmacSha256 = Hmac; pub type Nonce = u64; pub type Taken = Option; -pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct IpPair { pub ipv4: Ipv4Addr, @@ -169,7 +167,7 @@ impl GatewayClient { #[cfg(feature = "verify")] pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { // use gateways key as a ref to an x25519_dalek key - let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key); + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); // TODO: change that to use our nym_crypto::hmac module instead #[allow(clippy::expect_used)] diff --git a/common/bandwidth-controller/src/event.rs b/common/bandwidth-controller/src/event.rs index ee968dfd93..e5f78228dd 100644 --- a/common/bandwidth-controller/src/event.rs +++ b/common/bandwidth-controller/src/event.rs @@ -11,7 +11,7 @@ impl std::fmt::Display for BandwidthStatusMessage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BandwidthStatusMessage::RemainingBandwidth(b) => { - write!(f, "remaining bandwidth: {}", b) + write!(f, "remaining bandwidth: {b}") } BandwidthStatusMessage::NoBandwidth => write!(f, "no bandwidth left"), } diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 2016849eb0..4502238c91 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -207,7 +207,7 @@ where ::StorageError: Send + Sync + 'static, { if let Some(stored) = storage - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await .map_err(BandwidthControllerError::credential_storage_error)? { @@ -220,7 +220,7 @@ where ecash_apis, |api| async move { api.api_client - .global_expiration_date_signatures(Some(expiration_date)) + .global_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) .await }, format!("aggregated coin index signatures for date {expiration_date}"), diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index 51cfa31b19..aa75898ec4 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -13,7 +13,6 @@ clap_complete = { workspace = true, optional = true } clap_complete_fig = { workspace = true, optional = true } const-str = { workspace = true } log = { workspace = true } -pretty_env_logger = { workspace = true } schemars = { workspace = true, features = ["preserve_order"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } diff --git a/common/bin-common/src/logging/mod.rs b/common/bin-common/src/logging/mod.rs index ffbe6a6cb0..b5a9c68e40 100644 --- a/common/bin-common/src/logging/mod.rs +++ b/common/bin-common/src/logging/mod.rs @@ -21,29 +21,6 @@ pub struct LoggingSettings { // well, we need to implement something here at some point... } -// I'd argue we should start transitioning from `log` to `tracing` -pub fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("handlebars", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .init(); -} - // don't call init so that we could attach additional layers #[cfg(feature = "basic_tracing")] pub fn build_tracing_logger() -> impl tracing_subscriber::layer::SubscriberExt { diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index f56c591217..74663fe807 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -13,10 +13,10 @@ async-trait = { workspace = true } base64 = { workspace = true } bs58 = { workspace = true } clap = { workspace = true, optional = true } +cfg-if = { workspace = true } comfy-table = { workspace = true, optional = true } futures = { workspace = true } -humantime-serde = { workspace = true } -log = { workspace = true } +humantime = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -25,25 +25,23 @@ sha2 = { workspace = true } si-scale = { workspace = true } thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } -tokio = { workspace = true, features = ["macros"] } time = { workspace = true } +tokio = { workspace = true, features = ["sync", "macros"] } +tracing = { workspace = true } zeroize = { workspace = true } # internal nym-id = { path = "../nym-id" } nym-bandwidth-controller = { path = "../bandwidth-controller" } -nym-config = { path = "../config" } nym-crypto = { path = "../crypto" } nym-gateway-client = { path = "../client-libs/gateway-client" } nym-gateway-requests = { path = "../gateway-requests" } nym-http-api-client = { path = "../http-api-client" } -nym-metrics = { path = "../nym-metrics" } nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-sphinx = { path = "../nymsphinx" } nym-statistics-common = { path = "../statistics" } nym-pemstore = { path = "../pemstore" } nym-topology = { path = "../topology", features = ["persistence"] } -nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } nym-credentials-interface = { path = "../credentials-interface" } @@ -56,6 +54,9 @@ nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } nym-ecash-time = { path = "../ecash-time" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies] +nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } + ### For serving prometheus metrics [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] workspace = true @@ -123,3 +124,6 @@ fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"] fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] wasm = ["nym-gateway-client/wasm"] metrics-server = [] + +[lints] +workspace = true diff --git a/common/client-core/config-types/Cargo.toml b/common/client-core/config-types/Cargo.toml index 272768106a..31184905f5 100644 --- a/common/client-core/config-types/Cargo.toml +++ b/common/client-core/config-types/Cargo.toml @@ -19,6 +19,7 @@ nym-pemstore = { path = "../../pemstore", optional = true } # those are pulling so many deps T.T nym-sphinx-params = { path = "../../nymsphinx/params" } nym-sphinx-addressing = { path = "../../nymsphinx/addressing" } +nym-statistics-common = { path = "../../statistics" } [features] diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 1d8b20d003..b41eca332a 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -5,6 +5,7 @@ use nym_config::defaults::NymNetworkDetails; use nym_config::serde_helpers::{de_maybe_stringified, ser_maybe_stringified}; use nym_sphinx_addressing::Recipient; use nym_sphinx_params::{PacketSize, PacketType}; +use nym_statistics_common::types::SessionType; use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; @@ -22,7 +23,7 @@ const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; const DEFAULT_ACK_WAIT_ADDITION: Duration = Duration::from_millis(1_500); const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(200); const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(20); -const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(50); +const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15); const DEFAULT_TOPOLOGY_REFRESH_RATE: Duration = Duration::from_secs(5 * 60); // every 5min const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_000); @@ -56,9 +57,7 @@ const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; const DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD: Duration = Duration::from_secs(10); const DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD: Duration = Duration::from_secs(5 * 60); - -// 12 hours -const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60); +const DEFAULT_MAXIMUM_REPLY_SURB_REREQUESTS: usize = 5; // 24 hours const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60); @@ -375,14 +374,12 @@ pub struct Traffic { /// sent packet is going to be delayed at any given mix node. /// So for a packet going through three mix nodes, on average, it will take three times this value /// until the packet reaches its destination. - #[serde(with = "humantime_serde")] pub average_packet_delay: Duration, /// The parameter of Poisson distribution determining how long, on average, /// it is going to take another 'real traffic stream' message to be sent. /// If no real packets are available and cover traffic is enabled, /// a loop cover message is sent instead in order to preserve the rate. - #[serde(with = "humantime_serde")] pub message_sending_average_delay: Duration, /// Controls whether the main packet stream constantly produces packets according to the predefined @@ -414,6 +411,15 @@ pub struct Traffic { pub use_legacy_sphinx_format: bool, pub packet_type: PacketType, + + /// Indicates whether to mix hops or not. If mix hops are enabled, traffic + /// will be routed as usual, to the entry gateway, through three mix nodes, egressing + /// through the exit gateway. If mix hops are disabled, traffic will be routed directly + /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops + /// requires use of the updated SURB packet format. + pub disable_mix_hops: bool, } impl Traffic { @@ -444,6 +450,7 @@ impl Default for Traffic { // we should use the legacy format until sufficient number of nodes understand the // improved encoding use_legacy_sphinx_format: true, + disable_mix_hops: false, } } } @@ -619,10 +626,9 @@ pub struct ReplySurbs { #[serde(with = "humantime_serde")] pub maximum_reply_surb_drop_waiting_period: Duration, - /// Defines maximum amount of time given reply surb is going to be valid for. - /// This is going to be superseded by key rotation once implemented. - #[serde(with = "humantime_serde")] - pub maximum_reply_surb_age: Duration, + /// Defines maximum number of times the client is going to re-request reply surbs + /// for clearing pending messages before giving up after making no progress. + pub maximum_reply_surbs_rerequests: usize, /// Defines maximum amount of time given reply key is going to be valid for. /// This is going to be superseded by key rotation once implemented. @@ -632,9 +638,6 @@ pub struct ReplySurbs { /// Specifies the number of mixnet hops the packet should go through. If not specified, then /// the default value is used. pub surb_mix_hops: Option, - - /// Specifies if we should reset all the sender tags on startup - pub fresh_sender_tags: bool, } impl Default for ReplySurbs { @@ -649,10 +652,9 @@ impl Default for ReplySurbs { maximum_reply_surb_rerequest_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD, maximum_reply_surb_drop_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD, - maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE, + maximum_reply_surbs_rerequests: DEFAULT_MAXIMUM_REPLY_SURB_REREQUESTS, maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, surb_mix_hops: None, - fresh_sender_tags: false, } } } @@ -711,6 +713,9 @@ pub struct DebugConfig { /// Defines all configuration options related to the forget me flag. pub forget_me: ForgetMe, + + /// Defines all configuration options related to the remember me flag. + pub remember_me: RememberMe, } impl DebugConfig { @@ -734,6 +739,7 @@ impl Default for DebugConfig { reply_surbs: Default::default(), stats_reporting: Default::default(), forget_me: Default::default(), + remember_me: Default::default(), } } } @@ -799,3 +805,57 @@ impl ForgetMe { } } } + +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize, Copy)] +pub struct RememberMe { + /// Signal that this client should be accounted for in the stats + stats: bool, + + /// Type of the session to remember, if it should be remembered + session_type: SessionType, +} + +impl RememberMe { + pub fn new_vpn() -> Self { + Self { + stats: true, + session_type: SessionType::Vpn, + } + } + + pub fn new_mixnet() -> Self { + Self { + stats: true, + session_type: SessionType::Mixnet, + } + } + + pub fn new_native() -> Self { + Self { + stats: true, + session_type: SessionType::Native, + } + } + + pub fn new(stats: bool, session_type: SessionType) -> Self { + Self { + stats, + session_type, + } + } + + pub fn new_none() -> Self { + Self { + stats: false, + session_type: SessionType::Unknown, + } + } + + pub fn session_type(&self) -> SessionType { + self.session_type + } + + pub fn stats(&self) -> bool { + self.stats + } +} diff --git a/common/client-core/config-types/src/old/mod.rs b/common/client-core/config-types/src/old/mod.rs index 80b49b41be..2ce1ebdda4 100644 --- a/common/client-core/config-types/src/old/mod.rs +++ b/common/client-core/config-types/src/old/mod.rs @@ -6,6 +6,7 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; // aliases for backwards compatibility pub use v1 as old_config_v1_1_13; @@ -13,3 +14,4 @@ pub use v2 as old_config_v1_1_20; pub use v3 as old_config_v1_1_20_2; pub use v4 as old_config_v1_1_30; pub use v5 as old_config_v1_1_33; +pub use v6 as old_config_v1_1_54; diff --git a/common/client-core/config-types/src/old/v5.rs b/common/client-core/config-types/src/old/v5.rs index 117e9dcf32..caebd2f4f3 100644 --- a/common/client-core/config-types/src/old/v5.rs +++ b/common/client-core/config-types/src/old/v5.rs @@ -1,16 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ - Acknowledgements, Client, Config, CoverTraffic, DebugConfig, GatewayConnection, ReplySurbs, - Topology, Traffic, -}; use nym_sphinx_addressing::Recipient; use nym_sphinx_params::{PacketSize, PacketType}; use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; +use super::v6::*; + // 'DEBUG' const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; @@ -87,18 +85,18 @@ pub struct ConfigV5 { pub debug: DebugConfigV5, } -impl From for Config { +impl From for ConfigV6 { fn from(value: ConfigV5) -> Self { - Config { - client: Client { + ConfigV6 { + client: ClientV6 { version: value.client.version, id: value.client.id, disabled_credentials_mode: value.client.disabled_credentials_mode, nyxd_urls: value.client.nyxd_urls, nym_api_urls: value.client.nym_api_urls, }, - debug: DebugConfig { - traffic: Traffic { + debug: DebugConfigV6 { + traffic: TrafficV6 { average_packet_delay: value.debug.traffic.average_packet_delay, message_sending_average_delay: value .debug @@ -113,7 +111,7 @@ impl From for Config { packet_type: value.debug.traffic.packet_type, ..Default::default() }, - cover_traffic: CoverTraffic { + cover_traffic: CoverTrafficV6 { loop_cover_traffic_average_delay: value .debug .cover_traffic @@ -127,18 +125,18 @@ impl From for Config { .cover_traffic .disable_loop_cover_traffic_stream, }, - gateway_connection: GatewayConnection { + gateway_connection: GatewayConnectionV6 { gateway_response_timeout: value .debug .gateway_connection .gateway_response_timeout, }, - acknowledgements: Acknowledgements { + acknowledgements: AcknowledgementsV6 { average_ack_delay: value.debug.acknowledgements.average_ack_delay, ack_wait_multiplier: value.debug.acknowledgements.ack_wait_multiplier, ack_wait_addition: value.debug.acknowledgements.ack_wait_addition, }, - topology: Topology { + topology: TopologyV6 { topology_refresh_rate: value.debug.topology.topology_refresh_rate, topology_resolution_timeout: value.debug.topology.topology_resolution_timeout, disable_refreshing: value.debug.topology.disable_refreshing, @@ -148,7 +146,7 @@ impl From for Config { .max_startup_gateway_waiting_period, ..Default::default() }, - reply_surbs: ReplySurbs { + reply_surbs: ReplySurbsV6 { minimum_reply_surb_storage_threshold: value .debug .reply_surbs diff --git a/common/client-core/config-types/src/old/v6.rs b/common/client-core/config-types/src/old/v6.rs new file mode 100644 index 0000000000..66bf388114 --- /dev/null +++ b/common/client-core/config-types/src/old/v6.rs @@ -0,0 +1,622 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + Acknowledgements, Client, Config, CoverTraffic, DebugConfig, ForgetMe, GatewayConnection, + RememberMe, ReplySurbs, StatsReporting, Topology, Traffic, +}; +use nym_config::serde_helpers::{de_maybe_stringified, ser_maybe_stringified}; +use nym_sphinx_addressing::Recipient; +use nym_sphinx_params::{PacketSize, PacketType}; +use nym_statistics_common::types::SessionType; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use url::Url; + +// 'DEBUG' +const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; + +const DEFAULT_ACK_WAIT_ADDITION: Duration = Duration::from_millis(1_500); +const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(200); +const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(20); +const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15); +const DEFAULT_TOPOLOGY_REFRESH_RATE: Duration = Duration::from_secs(5 * 60); // every 5min +const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_000); + +// the same values as our current (10.06.24) blacklist +const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50; +const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50; + +const DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead + +// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause +// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the +// bandwidth bridging protocol, we can come back to a smaller timeout value +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); + +const DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO: f64 = 0.70; + +// reply-surbs related: + +// define when to request +// clients/client-core/src/client/replies/reply_storage/surb_storage.rs +const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200; +const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; + +// define how much to request at once +// clients/client-core/src/client/replies/reply_controller.rs +const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 50; + +const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; + +const DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD: Duration = Duration::from_secs(10); +const DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD: Duration = Duration::from_secs(5 * 60); + +// 12 hours +const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60); + +// 24 hours +const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60); + +// stats reporting related + +/// Time interval between reporting statistics to the given provider if it exists +const STATS_REPORT_INTERVAL_SECS: Duration = Duration::from_secs(300); + +// aliases for backwards compatibility +pub type ConfigV1_1_54 = ConfigV6; +pub type ClientV1_1_54 = ClientV6; +pub type DebugConfigV1_1_54 = DebugConfigV6; + +pub type TrafficV1_1_54 = TrafficV6; +pub type CoverTrafficV1_1_54 = CoverTrafficV6; +pub type GatewayConnectionV1_1_54 = GatewayConnectionV6; +pub type AcknowledgementsV1_1_54 = AcknowledgementsV6; +pub type TopologyV1_1_54 = TopologyV6; +pub type ReplySurbsV1_1_54 = ReplySurbsV6; + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV6 { + pub client: ClientV6, + + #[serde(default)] + pub debug: DebugConfigV6, +} + +impl From for Config { + fn from(value: ConfigV6) -> Self { + Config { + client: Client { + version: value.client.version, + id: value.client.id, + disabled_credentials_mode: value.client.disabled_credentials_mode, + nyxd_urls: value.client.nyxd_urls, + nym_api_urls: value.client.nym_api_urls, + }, + debug: DebugConfig { + traffic: Traffic { + average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, + message_sending_average_delay: value + .debug + .traffic + .message_sending_average_delay, + disable_main_poisson_packet_distribution: value + .debug + .traffic + .disable_main_poisson_packet_distribution, + primary_packet_size: value.debug.traffic.primary_packet_size, + secondary_packet_size: value.debug.traffic.secondary_packet_size, + packet_type: value.debug.traffic.packet_type, + deterministic_route_selection: value + .debug + .traffic + .deterministic_route_selection, + maximum_number_of_retransmissions: value + .debug + .traffic + .maximum_number_of_retransmissions, + use_legacy_sphinx_format: value.debug.traffic.use_legacy_sphinx_format, + disable_mix_hops: value.debug.traffic.disable_mix_hops, + }, + cover_traffic: CoverTraffic { + loop_cover_traffic_average_delay: value + .debug + .cover_traffic + .loop_cover_traffic_average_delay, + cover_traffic_primary_size_ratio: value + .debug + .cover_traffic + .cover_traffic_primary_size_ratio, + disable_loop_cover_traffic_stream: value + .debug + .cover_traffic + .disable_loop_cover_traffic_stream, + }, + gateway_connection: GatewayConnection { + gateway_response_timeout: value + .debug + .gateway_connection + .gateway_response_timeout, + }, + acknowledgements: Acknowledgements { + average_ack_delay: value.debug.acknowledgements.average_ack_delay, + ack_wait_multiplier: value.debug.acknowledgements.ack_wait_multiplier, + ack_wait_addition: value.debug.acknowledgements.ack_wait_addition, + }, + topology: Topology { + topology_refresh_rate: value.debug.topology.topology_refresh_rate, + topology_resolution_timeout: value.debug.topology.topology_resolution_timeout, + disable_refreshing: value.debug.topology.disable_refreshing, + max_startup_gateway_waiting_period: value + .debug + .topology + .max_startup_gateway_waiting_period, + minimum_mixnode_performance: value.debug.topology.minimum_mixnode_performance, + minimum_gateway_performance: value.debug.topology.minimum_gateway_performance, + use_extended_topology: value.debug.topology.use_extended_topology, + ignore_egress_epoch_role: value.debug.topology.ignore_egress_epoch_role, + ignore_ingress_epoch_role: value.debug.topology.ignore_ingress_epoch_role, + }, + reply_surbs: ReplySurbs { + minimum_reply_surb_storage_threshold: value + .debug + .reply_surbs + .minimum_reply_surb_storage_threshold, + maximum_reply_surb_storage_threshold: value + .debug + .reply_surbs + .maximum_reply_surb_storage_threshold, + minimum_reply_surb_request_size: value + .debug + .reply_surbs + .minimum_reply_surb_request_size, + maximum_reply_surb_request_size: value + .debug + .reply_surbs + .maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: value + .debug + .reply_surbs + .maximum_allowed_reply_surb_request_size, + maximum_reply_surb_rerequest_waiting_period: value + .debug + .reply_surbs + .maximum_reply_surb_rerequest_waiting_period, + maximum_reply_surb_drop_waiting_period: value + .debug + .reply_surbs + .maximum_reply_surb_drop_waiting_period, + maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age, + surb_mix_hops: value.debug.reply_surbs.surb_mix_hops, + minimum_reply_surb_threshold_buffer: value + .debug + .reply_surbs + .minimum_reply_surb_threshold_buffer, + ..Default::default() + }, + stats_reporting: StatsReporting { + enabled: value.debug.stats_reporting.enabled, + provider_address: value.debug.stats_reporting.provider_address, + reporting_interval: value.debug.stats_reporting.reporting_interval, + }, + forget_me: ForgetMe { + client: value.debug.forget_me.client, + stats: value.debug.forget_me.stats, + }, + remember_me: RememberMe { + stats: value.debug.remember_me.stats, + session_type: value.debug.remember_me.session_type.into(), + }, + }, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +// note: the deny_unknown_fields is VITAL here to allow upgrades from v1.1.20_2 +#[serde(deny_unknown_fields)] +pub struct ClientV6 { + /// Version of the client for which this configuration was created. + pub version: String, + + /// ID specifies the human readable ID of this particular client. + pub id: String, + + /// Indicates whether this client is running in a disabled credentials mode, thus attempting + /// to claim bandwidth without presenting bandwidth credentials. + // TODO: this should be moved to `debug.gateway_connection` + #[serde(default)] + pub disabled_credentials_mode: bool, + + /// Addresses to nyxd validators via which the client can communicate with the chain. + #[serde(alias = "validator_urls")] + pub nyxd_urls: Vec, + + /// Addresses to APIs running on validator from which the client gets the view of the network. + #[serde(alias = "validator_api_urls")] + pub nym_api_urls: Vec, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct TrafficV6 { + /// The parameter of Poisson distribution determining how long, on average, + /// sent packet is going to be delayed at any given mix node. + /// So for a packet going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + #[serde(with = "humantime_serde")] + pub average_packet_delay: Duration, + + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take another 'real traffic stream' message to be sent. + /// If no real packets are available and cover traffic is enabled, + /// a loop cover message is sent instead in order to preserve the rate. + #[serde(with = "humantime_serde")] + pub message_sending_average_delay: Duration, + + /// Controls whether the main packet stream constantly produces packets according to the predefined + /// poisson distribution. + pub disable_main_poisson_packet_distribution: bool, + + /// Specify whether route selection should be determined by the packet header. + pub deterministic_route_selection: bool, + + /// Specify how many times particular packet can be retransmitted + /// None - no limit + pub maximum_number_of_retransmissions: Option, + + /// Specifies the packet size used for sent messages. + /// Do not override it unless you understand the consequences of that change. + pub primary_packet_size: PacketSize, + + /// Specifies the optional auxiliary packet size for optimizing message streams. + /// Note that its use decreases overall anonymity. + /// Do not set it unless you understand the consequences of that change. + pub secondary_packet_size: Option, + + /// Specify whether any constructed sphinx packets should use the legacy format, + /// where the payload keys are explicitly attached rather than using the seeds + /// this affects any forward packets, acks and reply surbs + /// this flag should remain disabled until sufficient number of nodes on the network has upgraded + /// and support updated format. + /// in the case of reply surbs, the recipient must also understand the new encoding + pub use_legacy_sphinx_format: bool, + + pub packet_type: PacketType, + + /// Indicates whether to mix hops or not. If mix hops are enabled, traffic + /// will be routed as usual, to the entry gateway, through three mix nodes, egressing + /// through the exit gateway. If mix hops are disabled, traffic will be routed directly + /// from the entry gateway to the exit gateway, bypassing the mix nodes. + pub disable_mix_hops: bool, +} + +impl Default for TrafficV6 { + fn default() -> Self { + TrafficV6 { + average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, + message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, + disable_main_poisson_packet_distribution: false, + deterministic_route_selection: false, + maximum_number_of_retransmissions: None, + primary_packet_size: PacketSize::RegularPacket, + secondary_packet_size: None, + packet_type: PacketType::Mix, + + // we should use the legacy format until sufficient number of nodes understand the + // improved encoding + use_legacy_sphinx_format: true, + disable_mix_hops: false, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoverTrafficV6 { + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take for another loop cover traffic message to be sent. + #[serde(with = "humantime_serde")] + pub loop_cover_traffic_average_delay: Duration, + + /// Specifies the ratio of `primary_packet_size` to `secondary_packet_size` used in cover traffic. + /// Only applicable if `secondary_packet_size` is enabled. + pub cover_traffic_primary_size_ratio: f64, + + /// Controls whether the dedicated loop cover traffic stream should be enabled. + /// (and sending packets, on average, every [Self::loop_cover_traffic_average_delay]) + pub disable_loop_cover_traffic_stream: bool, +} + +impl Default for CoverTrafficV6 { + fn default() -> Self { + CoverTrafficV6 { + loop_cover_traffic_average_delay: DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY, + cover_traffic_primary_size_ratio: DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO, + disable_loop_cover_traffic_stream: false, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct GatewayConnectionV6 { + /// How long we're willing to wait for a response to a message sent to the gateway, + /// before giving up on it. + #[serde(with = "humantime_serde")] + pub gateway_response_timeout: Duration, +} + +impl Default for GatewayConnectionV6 { + fn default() -> Self { + GatewayConnectionV6 { + gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct AcknowledgementsV6 { + /// The parameter of Poisson distribution determining how long, on average, + /// sent acknowledgement is going to be delayed at any given mix node. + /// So for an ack going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + #[serde(with = "humantime_serde")] + pub average_ack_delay: Duration, + + /// Value multiplied with the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 1. + pub ack_wait_multiplier: f64, + + /// Value added to the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 0. + #[serde(with = "humantime_serde")] + pub ack_wait_addition: Duration, +} + +impl Default for AcknowledgementsV6 { + fn default() -> Self { + AcknowledgementsV6 { + average_ack_delay: DEFAULT_AVERAGE_PACKET_DELAY, + ack_wait_multiplier: DEFAULT_ACK_WAIT_MULTIPLIER, + ack_wait_addition: DEFAULT_ACK_WAIT_ADDITION, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct TopologyV6 { + /// The uniform delay every which clients are querying the directory server + /// to try to obtain a compatible network topology to send sphinx packets through. + #[serde(with = "humantime_serde")] + pub topology_refresh_rate: Duration, + + /// During topology refresh, test packets are sent through every single possible network + /// path. This timeout determines waiting period until it is decided that the packet + /// did not reach its destination. + #[serde(with = "humantime_serde")] + pub topology_resolution_timeout: Duration, + + /// Specifies whether the client should not refresh the network topology after obtaining + /// the first valid instance. + /// Supersedes `topology_refresh_rate_ms`. + pub disable_refreshing: bool, + + /// Defines how long the client is going to wait on startup for its gateway to come online, + /// before abandoning the procedure. + #[serde(with = "humantime_serde")] + pub max_startup_gateway_waiting_period: Duration, + + /// Specifies a minimum performance of a mixnode that is used on route construction. + /// This setting is only applicable when `NymApi` topology is used. + pub minimum_mixnode_performance: u8, + + /// Specifies a minimum performance of a gateway that is used on route construction. + /// This setting is only applicable when `NymApi` topology is used. + pub minimum_gateway_performance: u8, + + /// Specifies whether this client should attempt to retrieve all available network nodes + /// as opposed to just active mixnodes/gateways. + pub use_extended_topology: bool, + + /// Specifies whether this client should ignore the current epoch role of the target egress node + /// when constructing the final hop packets. + pub ignore_egress_epoch_role: bool, + + /// Specifies whether this client should ignore the current epoch role of the ingress node + /// when attempting to establish new connection + pub ignore_ingress_epoch_role: bool, +} + +impl Default for TopologyV6 { + fn default() -> Self { + TopologyV6 { + topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, + topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, + disable_refreshing: false, + max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD, + minimum_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE, + minimum_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE, + use_extended_topology: false, + + ignore_egress_epoch_role: true, + ignore_ingress_epoch_role: true, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct ReplySurbsV6 { + /// Defines the minimum number of reply surbs the client wants to keep in its storage at all times. + /// It can only allow to go below that value if its to request additional reply surbs. + pub minimum_reply_surb_storage_threshold: usize, + + /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. + pub maximum_reply_surb_storage_threshold: usize, + + /// Defines the soft threshold ontop of the minimum reply surb storage threshold for when the client + /// should proactively request additional reply surbs. + pub minimum_reply_surb_threshold_buffer: usize, + + /// Defines the minimum number of reply surbs the client would request. + pub minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + pub maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + pub maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_rerequest_waiting_period: Duration, + + /// Defines maximum amount of time the client is going to wait for reply surbs before + /// deciding it's never going to get them and would drop all pending messages + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_drop_waiting_period: Duration, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_age: Duration, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_key_age: Duration, + + /// Specifies the number of mixnet hops the packet should go through. If not specified, then + /// the default value is used. + pub surb_mix_hops: Option, + + /// Specifies if we should reset all the sender tags on startup + pub fresh_sender_tags: bool, +} + +impl Default for ReplySurbsV6 { + fn default() -> Self { + ReplySurbsV6 { + minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD, + maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD, + minimum_reply_surb_threshold_buffer: DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER, + minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE, + maximum_reply_surb_rerequest_waiting_period: + DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD, + maximum_reply_surb_drop_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD, + maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE, + maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, + surb_mix_hops: None, + fresh_sender_tags: false, + } + } +} + +#[derive(Debug, Default, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct DebugConfigV6 { + /// Defines all configuration options related to traffic streams. + pub traffic: TrafficV6, + + /// Defines all configuration options related to cover traffic stream(s). + pub cover_traffic: CoverTrafficV6, + + /// Defines all configuration options related to the gateway connection. + pub gateway_connection: GatewayConnectionV6, + + /// Defines all configuration options related to acknowledgements, such as delays or wait timeouts. + pub acknowledgements: AcknowledgementsV6, + + /// Defines all configuration options related topology, such as refresh rates or timeouts. + pub topology: TopologyV6, + + /// Defines all configuration options related to reply SURBs. + pub reply_surbs: ReplySurbsV6, + + /// Defines all configuration options related to stats reporting. + pub stats_reporting: StatsReportingV6, + + /// Defines all configuration options related to the forget me flag. + pub forget_me: ForgetMeV6, + + /// Defines all configuration options related to the remember me flag. + pub remember_me: RememberMeV6, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct StatsReportingV6 { + /// Is stats reporting enabled + pub enabled: bool, + + /// Address of the stats collector. If this is none, no reporting will happen, regardless of `enabled` + #[serde( + serialize_with = "ser_maybe_stringified", + deserialize_with = "de_maybe_stringified" + )] + pub provider_address: Option, + + /// With what frequence will statistics be sent + #[serde(with = "humantime_serde")] + pub reporting_interval: Duration, +} + +impl Default for StatsReportingV6 { + fn default() -> Self { + StatsReportingV6 { + enabled: true, + provider_address: None, + reporting_interval: STATS_REPORT_INTERVAL_SECS, + } + } +} + +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize, Copy)] +pub struct ForgetMeV6 { + client: bool, + stats: bool, +} + +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize, Copy)] +pub struct RememberMeV6 { + /// Signal that this client should be accounted for in the stats + stats: bool, + + /// Type of the session to remember, if it should be remembered + session_type: SessionTypeV6, +} + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize, Default, Debug)] +pub enum SessionTypeV6 { + Vpn, + Mixnet, + Wasm, + Native, + Socks5, + #[default] + Unknown, +} + +impl From for SessionType { + fn from(value: SessionTypeV6) -> Self { + match value { + SessionTypeV6::Vpn => Self::Vpn, + SessionTypeV6::Mixnet => Self::Mixnet, + SessionTypeV6::Wasm => Self::Wasm, + SessionTypeV6::Native => Self::Native, + SessionTypeV6::Socks5 => Self::Socks5, + SessionTypeV6::Unknown => Self::Unknown, + } + } +} diff --git a/common/client-core/gateways-storage/Cargo.toml b/common/client-core/gateways-storage/Cargo.toml index e91c889ed6..938a6db5dc 100644 --- a/common/client-core/gateways-storage/Cargo.toml +++ b/common/client-core/gateways-storage/Cargo.toml @@ -3,17 +3,18 @@ name = "nym-client-core-gateways-storage" version = "0.1.0" edition = "2021" license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] async-trait.workspace = true cosmrs.workspace = true -log.workspace = true serde = { workspace = true, features = ["derive"] } thiserror.workspace = true time.workspace = true tokio = { workspace = true, features = ["sync"] } +tracing.workspace = true url.workspace = true zeroize = { workspace = true, features = ["zeroize_derive"] } @@ -26,6 +27,7 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] optional = true [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", diff --git a/common/client-core/gateways-storage/build.rs b/common/client-core/gateways-storage/build.rs index f8b85cbb4a..e8fc127e07 100644 --- a/common/client-core/gateways-storage/build.rs +++ b/common/client-core/gateways-storage/build.rs @@ -2,23 +2,30 @@ // SPDX-License-Identifier: Apache-2.0 #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { #[cfg(feature = "fs-gateways-storage")] { + use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/gateways-storage-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./fs_gateways_migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -28,4 +35,6 @@ async fn main() { // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); } + + Ok(()) } diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs index bdcaa0fdb5..59b7bef89d 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::BadGateway; -use std::io; -use std::path::PathBuf; +use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] @@ -19,7 +18,6 @@ pub enum StorageError { #[error("failed to perform sqlx migration: {source}")] MigrationError { - #[source] #[from] source: sqlx::migrate::MigrateError, }, @@ -32,7 +30,6 @@ pub enum StorageError { #[error("failed to run the SQL query: {source}")] QueryError { - #[source] #[from] source: sqlx::error::Error, }, diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs index d2d1943101..b28dc6e77e 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs @@ -7,12 +7,12 @@ use crate::{ RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails, }, }; -use log::{debug, error}; use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; use std::path::Path; +use tracing::{debug, error}; #[derive(Debug, Clone)] pub struct StorageManager { @@ -87,7 +87,7 @@ impl StorageManager { sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == Some(1)) + .map(|result| result.exists == 1) } pub(crate) async fn maybe_get_registered_gateway( diff --git a/common/client-core/src/cli_helpers/client_add_gateway.rs b/common/client-core/src/cli_helpers/client_add_gateway.rs index 8811aff1d3..5662a25a3d 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -12,12 +12,12 @@ use crate::{ error::ClientCoreError, init::types::{GatewaySelectionSpecification, GatewaySetup}, }; -use log::info; use nym_client_core_gateways_storage::GatewayDetails; use nym_crypto::asymmetric::ed25519; use nym_topology::NymTopology; use nym_validator_client::UserAgent; use std::path::PathBuf; +use tracing::info; #[cfg_attr(feature = "cli", derive(clap::Args))] #[derive(Debug, Clone)] @@ -81,14 +81,14 @@ where // Attempt to use a user-provided gateway, if possible let user_chosen_gateway_id = common_args.gateway_id; - log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); + tracing::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); let selection_spec = GatewaySelectionSpecification::new( user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), common_args.force_tls_gateway, ); - log::debug!("Gateway selection specification: {selection_spec:?}"); + tracing::debug!("Gateway selection specification: {selection_spec:?}"); let registered_gateways = get_all_registered_identities(&details_store).await?; diff --git a/common/client-core/src/cli_helpers/client_import_coin_index_signatures.rs b/common/client-core/src/cli_helpers/client_import_coin_index_signatures.rs index 5625250ce3..ce282ef9d1 100644 --- a/common/client-core/src/cli_helpers/client_import_coin_index_signatures.rs +++ b/common/client-core/src/cli_helpers/client_import_coin_index_signatures.rs @@ -58,6 +58,7 @@ where Some(data) => data, None => { // SAFETY: one of those arguments must have been set + #[allow(clippy::unwrap_used)] fs::read(common_args.signatures_path.unwrap())? } }; diff --git a/common/client-core/src/cli_helpers/client_import_credential.rs b/common/client-core/src/cli_helpers/client_import_credential.rs index 77c6dc16f4..30799167ce 100644 --- a/common/client-core/src/cli_helpers/client_import_credential.rs +++ b/common/client-core/src/cli_helpers/client_import_credential.rs @@ -64,6 +64,7 @@ where Some(data) => data, None => { // SAFETY: one of those arguments must have been set + #[allow(clippy::unwrap_used)] fs::read(common_args.credential_path.unwrap())? } }; diff --git a/common/client-core/src/cli_helpers/client_import_expiration_date_signatures.rs b/common/client-core/src/cli_helpers/client_import_expiration_date_signatures.rs index 4dc875e75c..f3752587ab 100644 --- a/common/client-core/src/cli_helpers/client_import_expiration_date_signatures.rs +++ b/common/client-core/src/cli_helpers/client_import_expiration_date_signatures.rs @@ -58,6 +58,7 @@ where Some(data) => data, None => { // SAFETY: one of those arguments must have been set + #[allow(clippy::unwrap_used)] fs::read(common_args.signatures_path.unwrap())? } }; diff --git a/common/client-core/src/cli_helpers/client_import_master_verification_key.rs b/common/client-core/src/cli_helpers/client_import_master_verification_key.rs index b44334b49e..677018226b 100644 --- a/common/client-core/src/cli_helpers/client_import_master_verification_key.rs +++ b/common/client-core/src/cli_helpers/client_import_master_verification_key.rs @@ -58,6 +58,7 @@ where Some(data) => data, None => { // SAFETY: one of those arguments must have been set + #[allow(clippy::unwrap_used)] fs::read(common_args.key_path.unwrap())? } }; diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index 03f41ed077..a59d73e261 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -12,7 +12,6 @@ use crate::{ }, init::types::{GatewaySelectionSpecification, GatewaySetup, InitResults}, }; -use log::info; use nym_client_core_gateways_storage::GatewayDetails; use nym_crypto::asymmetric::ed25519; use nym_sphinx::addressing::Recipient; @@ -20,6 +19,7 @@ use nym_topology::NymTopology; use nym_validator_client::UserAgent; use rand::rngs::OsRng; use std::path::PathBuf; +use tracing::info; // we can suppress this warning (as suggested by linter itself) since we're only using it in our own code #[allow(async_fn_in_trait)] @@ -130,23 +130,23 @@ where // Attempt to use a user-provided gateway, if possible let user_chosen_gateway_id = common_args.gateway; - log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); + tracing::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); let selection_spec = GatewaySelectionSpecification::new( user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), common_args.force_tls_gateway, ); - log::debug!("Gateway selection specification: {selection_spec:?}"); + tracing::debug!("Gateway selection specification: {selection_spec:?}"); // Load and potentially override config - log::debug!("Init arguments: {init_args:#?}"); + tracing::debug!("Init arguments: {init_args:#?}"); let config = C::construct_config(&init_args); - log::debug!("Constructed config: {config:#?}"); + tracing::debug!("Constructed config: {config:#?}"); let paths = config.common_paths(); let core = config.core_config(); - log::info!( + tracing::info!( "Using nym-api: {}", core.client .nym_api_urls diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 90031fd0fe..36cb427780 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -18,6 +18,7 @@ use crate::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, }; use crate::client::replies::reply_controller; +use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig; use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender}; use crate::client::replies::reply_storage::{ CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys, @@ -34,9 +35,8 @@ use crate::init::{ }; use crate::{config, spawn_future}; use futures::channel::mpsc; -use log::*; use nym_bandwidth_controller::BandwidthController; -use nym_client_core_config_types::ForgetMe; +use nym_client_core_config_types::{ForgetMe, RememberMe}; use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore}; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -56,13 +56,18 @@ use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, use nym_task::{TaskClient, TaskHandle}; use nym_topology::provider_trait::TopologyProvider; use nym_topology::HardcodedTopologyProvider; -use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent}; +use nym_validator_client::nym_api::NymApiClientExt; +use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent}; +use rand::prelude::SliceRandom; use rand::rngs::OsRng; +use rand::thread_rng; use std::fmt::Debug; use std::os::raw::c_int as RawFd; use std::path::Path; use std::sync::Arc; +use time::OffsetDateTime; use tokio::sync::mpsc::Sender; +use tracing::*; use url::Url; #[cfg(all( @@ -130,9 +135,11 @@ pub enum ClientInputStatus { } impl ClientInputStatus { + #[allow(clippy::panic)] pub fn register_producer(&mut self) -> ClientInput { match std::mem::replace(self, ClientInputStatus::Connected) { ClientInputStatus::AwaitingProducer { client_input } => client_input, + // critical failure implying misuse of software ClientInputStatus::Connected => panic!("producer was already registered before"), } } @@ -144,9 +151,11 @@ pub enum ClientOutputStatus { } impl ClientOutputStatus { + #[allow(clippy::panic)] pub fn register_consumer(&mut self) -> ClientOutput { match std::mem::replace(self, ClientOutputStatus::Connected) { ClientOutputStatus::AwaitingConsumer { client_output } => client_output, + // critical failure implying misuse of software ClientOutputStatus::Connected => panic!("consumer was already registered before"), } } @@ -238,6 +247,12 @@ where self } + #[must_use] + pub fn with_remember_me(mut self, remember_me: &RememberMe) -> Self { + self.config.debug.remember_me = *remember_me; + self + } + #[must_use] pub fn with_gateway_setup(mut self, setup: GatewaySetup) -> Self { self.setup_method = setup; @@ -332,6 +347,7 @@ where #[allow(clippy::too_many_arguments)] fn start_real_traffic_controller( controller_config: real_messages_control::Config, + key_rotation_config: KeyRotationConfig, topology_accessor: TopologyAccessor, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, @@ -349,6 +365,7 @@ where RealMessagesController::new( controller_config, + key_rotation_config, ack_receiver, input_receiver, mix_sender, @@ -447,10 +464,10 @@ where }; let gateway_failure = |err| { - log::error!("Could not authenticate and start up the gateway connection - {err}"); + tracing::error!("Could not authenticate and start up the gateway connection - {err}"); ClientCoreError::GatewayClientError { gateway_id: details.gateway_id.to_base58_string(), - source: err, + source: Box::new(err), } }; @@ -549,14 +566,14 @@ where custom_provider: Option>, config_topology: config::Topology, nym_api_urls: Vec, - user_agent: Option, + nym_api_client: NymApiClient, ) -> Box { // if no custom provider was ... provided ..., create one using nym-api custom_provider.unwrap_or_else(|| { Box::new(NymApiTopologyProvider::new( config_topology, nym_api_urls, - user_agent, + nym_api_client, )) }) } @@ -592,7 +609,7 @@ where topology_refresher.try_refresh().await; if let Err(err) = topology_refresher.ensure_topology_is_routable().await { - log::error!( + tracing::error!( "The current network topology seem to be insufficient to route any packets through \ - check if enough nodes and a gateway are online - source: {err}" ); @@ -668,27 +685,40 @@ where // TODO: rename it as it implies the data is persistent whilst one can use InMemBackend async fn setup_persistent_reply_storage( backend: S::ReplyStore, + key_rotation_config: KeyRotationConfig, shutdown: TaskClient, ) -> Result where ::StorageError: Sync + Send, S::ReplyStore: Send + Sync, { - log::trace!("Setup persistent reply storage"); + tracing::trace!("Setup persistent reply storage"); + let now = OffsetDateTime::now_utc(); + let expected_current_key_rotation_start = + key_rotation_config.expected_current_key_rotation_start(now); + // time of the start of one epoch BEFORE the CURRENT rotation has begun + // this indicates the starting time of when packets with the current keys might have been constructed + // (i.e. any surbs OLDER than that MUST BE invalid) + let prior_epoch_start = + expected_current_key_rotation_start - key_rotation_config.epoch_duration; + let persistent_storage = PersistentReplyStorage::new(backend); let mem_store = persistent_storage - .load_state_from_backend() + .load_state_from_backend(prior_epoch_start) .await .map_err(|err| ClientCoreError::SurbStorageError { source: Box::new(err), })?; let store_clone = mem_store.clone(); - spawn_future(async move { - persistent_storage - .flush_on_shutdown(store_clone, shutdown) - .await - }); + spawn_future!( + async move { + persistent_storage + .flush_on_shutdown(store_clone, shutdown) + .await + }, + "PersistentReplyStorage::flush_on_shutdown" + ); Ok(mem_store) } @@ -709,7 +739,7 @@ where let mut rng = OsRng; let keys = if let Some(derivation_material) = derivation_material { ClientKeys::from_master_key(&mut rng, &derivation_material) - .map_err(|_| ClientCoreError::HkdfDerivationError {})? + .map_err(|_| ClientCoreError::HkdfDerivationError)? } else { ClientKeys::generate_new(&mut rng) }; @@ -719,6 +749,23 @@ where setup_gateway(setup_method, key_store, details_store).await } + fn construct_nym_api_client(config: &Config, user_agent: Option) -> NymApiClient { + let mut nym_api_urls = config.get_nym_api_endpoints(); + nym_api_urls.shuffle(&mut thread_rng()); + + if let Some(user_agent) = user_agent { + NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent) + } else { + NymApiClient::new(nym_api_urls[0].clone()) + } + } + + async fn determine_key_rotation_state( + client: &NymApiClient, + ) -> Result { + Ok(client.nym_api.get_key_rotation_info().await?.into()) + } + pub async fn start_base(mut self) -> Result where S::ReplyStore: Send + Sync, @@ -783,11 +830,14 @@ where .dkg_query_client .map(|client| BandwidthController::new(credential_store, client)); + let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone()); + let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?; + let topology_provider = Self::setup_topology_provider( self.custom_topology_provider.take(), self.config.debug.topology, self.config.get_nym_api_endpoints(), - self.user_agent.clone(), + nym_api_client, ); let stats_reporter = Self::start_statistics_control( @@ -832,6 +882,7 @@ where let reply_storage = Self::setup_persistent_reply_storage( reply_storage_backend, + key_rotation_config, shutdown.fork("persistent_reply_storage"), ) .await?; @@ -872,6 +923,7 @@ where Self::start_real_traffic_controller( controller_config, + key_rotation_config, shared_topology_accessor.clone(), ack_receiver, input_receiver, @@ -930,6 +982,7 @@ where task_handle: shutdown, client_request_sender, forget_me: self.config.debug.forget_me, + remember_me: self.config.debug.remember_me, }) } } @@ -944,4 +997,5 @@ pub struct BaseClient { pub client_request_sender: ClientRequestSender, pub task_handle: TaskHandle, pub forget_me: ForgetMe, + pub remember_me: RememberMe, } diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index e7ef621c9d..365aabd0e8 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -1,32 +1,30 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::{ - fs_backend, CombinedReplyStorage, ReplyStorageBackend, +use crate::{ + client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend}, + config, + config::Config, + error::ClientCoreError, }; -use crate::config; -use crate::config::Config; -use crate::error::ClientCoreError; -use log::{error, info, trace}; use nym_bandwidth_controller::BandwidthController; use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_credential_storage::storage::Storage as CredentialStorage; -use nym_validator_client::nyxd; -use nym_validator_client::QueryHttpRpcNyxdClient; -use std::path::Path; -use std::{fs, io}; +use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; +use std::{io, path::Path}; use time::OffsetDateTime; +use tracing::{error, info, trace}; use url::Url; async fn setup_fresh_backend>( db_path: P, surb_config: &config::ReplySurbs, ) -> Result { - info!("creating fresh surb database"); + info!("Creating fresh surb database"); let mut storage_backend = match fs_backend::Backend::init(db_path).await { Ok(backend) => backend, Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); + error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"); return Err(ClientCoreError::SurbStorageError { source: Box::new(err), }); @@ -40,14 +38,15 @@ async fn setup_fresh_backend>( surb_config.minimum_reply_surb_storage_threshold, surb_config.maximum_reply_surb_storage_threshold, ); - storage_backend - .init_fresh(&mem_store) - .await - .map_err(|err| ClientCoreError::SurbStorageError { - source: Box::new(err), - })?; - - Ok(storage_backend) + match storage_backend.init_fresh(&mem_store).await { + Ok(()) => Ok(storage_backend), + Err(err) => { + storage_backend.shutdown().await; + Err(ClientCoreError::SurbStorageError { + source: Box::new(err), + }) + } + } } // fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend { @@ -58,12 +57,11 @@ async fn setup_fresh_backend>( // ) // } -fn archive_corrupted_database>(db_path: P) -> io::Result<()> { +async fn archive_corrupted_database>(db_path: P) -> io::Result<()> { let db_path = db_path.as_ref(); debug_assert!(db_path.exists()); let now = OffsetDateTime::now_utc().unix_timestamp(); - let suffix = format!("_{now}.corrupted"); let new_extension = @@ -72,11 +70,15 @@ fn archive_corrupted_database>(db_path: P) -> io::Result<()> { } else { suffix }; + let renamed = db_path.with_extension(new_extension); - let mut renamed = db_path.to_owned(); - renamed.set_extension(new_extension); - - fs::rename(db_path, renamed) + tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| { + error!( + "Failed to rename corrupt database file: {} to {}", + db_path.display(), + renamed.display() + ); + }) } pub async fn setup_fs_reply_surb_backend>( @@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend>( // the existing one let db_path = db_path.as_ref(); if db_path.exists() { - info!("loading existing surb database"); - match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await { + info!("Loading existing surb database"); + match fs_backend::Backend::try_load(db_path).await { Ok(backend) => Ok(backend), Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); - - archive_corrupted_database(db_path)?; + error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); + archive_corrupted_database(db_path).await?; setup_fresh_backend(db_path, surb_config).await } } @@ -113,41 +114,32 @@ pub async fn setup_fs_gateways_storage>( }) } -pub fn create_bandwidth_controller( - config: &Config, - storage: St, -) -> BandwidthController { - let nyxd_url = config - .get_validator_endpoints() - .pop() - .expect("No nyxd validator endpoint provided"); - - create_bandwidth_controller_with_urls(nyxd_url, storage) -} - pub fn create_bandwidth_controller_with_urls( nyxd_url: Url, storage: St, -) -> BandwidthController { - let client = default_query_dkg_client(nyxd_url); +) -> Result, ClientCoreError> { + let client = default_query_dkg_client(nyxd_url)?; - BandwidthController::new(storage, client) + Ok(BandwidthController::new(storage, client)) } -pub fn default_query_dkg_client_from_config(config: &Config) -> QueryHttpRpcNyxdClient { +pub fn default_query_dkg_client_from_config( + config: &Config, +) -> Result { let nyxd_url = config .get_validator_endpoints() .pop() - .expect("No nyxd validator endpoint provided"); + .ok_or(ClientCoreError::RpcClientMissingUrl)?; default_query_dkg_client(nyxd_url) } -pub fn default_query_dkg_client(nyxd_url: Url) -> QueryHttpRpcNyxdClient { +pub fn default_query_dkg_client(nyxd_url: Url) -> Result { let details = nym_network_defaults::NymNetworkDetails::new_from_env(); let client_config = nyxd::Config::try_from_nym_network_details(&details) - .expect("failed to construct validator client config"); + .map_err(|source| ClientCoreError::InvalidNetworkDetails { source })?; // overwrite env configuration with config URLs + QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str()) - .expect("Could not construct query client") + .map_err(|source| ClientCoreError::RpcClientCreationFailure { source }) } diff --git a/common/client-core/src/client/cover_traffic_stream.rs b/common/client-core/src/client/cover_traffic_stream.rs index 99b2894d85..d635fc20c6 100644 --- a/common/client-core/src/client/cover_traffic_stream.rs +++ b/common/client-core/src/client/cover_traffic_stream.rs @@ -6,7 +6,6 @@ use crate::client::topology_control::TopologyAccessor; use crate::{config, spawn_future}; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; -use log::*; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::cover::generate_loop_cover_packet; @@ -19,6 +18,7 @@ use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::error::TrySendError; +use tracing::*; #[cfg(not(target_arch = "wasm32"))] use tokio::time::{sleep, Sleep}; @@ -210,10 +210,10 @@ impl LoopCoverTrafficStream { TrySendError::Full(_) => { // This isn't a problem, if the channel is full means we're already sending the // max amount of messages downstream can handle. - log::debug!("Failed to send cover message - channel full"); + tracing::debug!("Failed to send cover message - channel full"); } TrySendError::Closed(_) => { - log::warn!("Failed to send cover message - channel closed"); + tracing::warn!("Failed to send cover message - channel closed"); } } } else { @@ -235,6 +235,7 @@ impl LoopCoverTrafficStream { tokio::task::yield_now().await; } + #[allow(clippy::panic)] pub fn start(mut self) { if self.cover_traffic.disable_loop_cover_traffic_stream { // we should have never got here in the first place - the task should have never been created to begin with @@ -251,27 +252,30 @@ impl LoopCoverTrafficStream { let mut shutdown = self.task_client.fork("select"); - spawn_future(async move { - debug!("Started LoopCoverTrafficStream with graceful shutdown support"); + spawn_future!( + async move { + debug!("Started LoopCoverTrafficStream with graceful shutdown support"); - while !shutdown.is_shutdown() { - tokio::select! { - biased; - _ = shutdown.recv() => { - log::trace!("LoopCoverTrafficStream: Received shutdown"); - } - next = self.next() => { - if next.is_some() { - self.on_new_message().await; - } else { - log::trace!("LoopCoverTrafficStream: Stopping since channel closed"); - break; + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + tracing::trace!("LoopCoverTrafficStream: Received shutdown"); + } + next = self.next() => { + if next.is_some() { + self.on_new_message().await; + } else { + tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed"); + break; + } } } } - } - shutdown.recv_timeout().await; - log::debug!("LoopCoverTrafficStream: Exiting"); - }) + shutdown.recv_timeout().await; + tracing::debug!("LoopCoverTrafficStream: Exiting"); + }, + "LoopCoverTrafficStream" + ) } } diff --git a/common/client-core/src/client/inbound_messages.rs b/common/client-core/src/client/inbound_messages.rs index ad5e4b0196..b74c9195be 100644 --- a/common/client-core/src/client/inbound_messages.rs +++ b/common/client-core/src/client/inbound_messages.rs @@ -135,7 +135,9 @@ impl InputMessage { recipient_tag, data, lane, - max_retransmissions: None, + // \/ set it to SOME sane default so that if we run out of surbs and constantly + // fail to request more, we wouldn't be stuck in limbo + max_retransmissions: Some(10), }; if let Some(packet_type) = packet_type { InputMessage::new_wrapper(message, packet_type) diff --git a/common/client-core/src/client/mix_traffic/mod.rs b/common/client-core/src/client/mix_traffic/mod.rs index 1114de9d89..7b705a7a3f 100644 --- a/common/client-core/src/client/mix_traffic/mod.rs +++ b/common/client-core/src/client/mix_traffic/mod.rs @@ -4,10 +4,10 @@ use crate::client::mix_traffic::transceiver::GatewayTransceiver; use crate::error::ClientCoreError; use crate::spawn_future; -use log::*; use nym_gateway_requests::ClientRequest; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; +use tracing::*; use transceiver::ErasedGatewayError; pub type BatchMixMessageSender = tokio::sync::mpsc::Sender>; @@ -96,72 +96,94 @@ impl MixTrafficController { mut mix_packets: Vec, ) -> Result<(), ErasedGatewayError> { debug_assert!(!mix_packets.is_empty()); - - let result = if mix_packets.len() == 1 { + let send_future = if mix_packets.len() == 1 { + // SAFETY: we just checked we have one packet + #[allow(clippy::unwrap_used)] let mix_packet = mix_packets.pop().unwrap(); - self.gateway_transceiver.send_mix_packet(mix_packet).await + self.gateway_transceiver.send_mix_packet(mix_packet) } else { - self.gateway_transceiver - .batch_send_mix_packets(mix_packets) - .await + self.gateway_transceiver.batch_send_mix_packets(mix_packets) }; - if result.is_err() { - self.consecutive_gateway_failure_count += 1; - } else { - trace!("We *might* have managed to forward sphinx packet(s) to the gateway!"); - self.consecutive_gateway_failure_count = 0; - } + tokio::select! { + biased; + _ = self.task_client.recv() => { + trace!("received shutdown while handling messages"); + Ok(()) + } + result = send_future => { + if result.is_err() { + self.consecutive_gateway_failure_count += 1; + } else { + trace!("We *might* have managed to forward sphinx packet(s) to the gateway!"); + self.consecutive_gateway_failure_count = 0; + } - result + result + } + } + } + + async fn on_client_request(&mut self, client_request: ClientRequest) { + tokio::select! { + biased; + _ = self.task_client.recv() => { + trace!("received shutdown while handling client request"); + } + result = self.gateway_transceiver.send_client_request(client_request) => { + if let Err(err) = result { + error!("Failed to send client request: {err}") + } + } + } } pub fn start(mut self) { - spawn_future(async move { - debug!("Started MixTrafficController with graceful shutdown support"); - - while !self.task_client.is_shutdown() { - tokio::select! { - mix_packets = self.mix_rx.recv() => match mix_packets { - Some(mix_packets) => { - if let Err(err) = self.on_messages(mix_packets).await { - error!("Failed to send sphinx packet(s) to the gateway: {err}"); - if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { - // Disconnect from the gateway. If we should try to re-connect - // is handled at a higher layer. - error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead"); - // Do we need to handle the embedded mixnet client case - // separately? - self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages)); - break; - } - } - }, - None => { - log::trace!("MixTrafficController: Stopping since channel closed"); + spawn_future!( + async move { + debug!("Started MixTrafficController with graceful shutdown support"); + while !self.task_client.is_shutdown() { + tokio::select! { + biased; + _ = self.task_client.recv() => { + tracing::trace!("MixTrafficController: Received shutdown"); break; } - }, - client_request = self.client_rx.recv() => match client_request { - Some(client_request) => { - match self.gateway_transceiver.send_client_request(client_request).await { - Ok(_) => (), - Err(e) => error!("Failed to send client request: {}", e), - }; + mix_packets = self.mix_rx.recv() => match mix_packets { + Some(mix_packets) => { + if let Err(err) = self.on_messages(mix_packets).await { + error!("Failed to send sphinx packet(s) to the gateway: {err}"); + if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { + // Disconnect from the gateway. If we should try to re-connect + // is handled at a higher layer. + error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead"); + // Do we need to handle the embedded mixnet client case + // separately? + self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages)); + break; + } + } + }, + None => { + tracing::trace!("MixTrafficController: Stopping since channel closed"); + break; + } + }, + client_request = self.client_rx.recv() => match client_request { + Some(client_request) => { + self.on_client_request(client_request).await; + }, + None => { + tracing::trace!("MixTrafficController, client request channel closed"); + break + } }, - None => { - log::trace!("MixTrafficController, client request channel closed"); - } - }, - _ = self.task_client.recv() => { - log::trace!("MixTrafficController: Received shutdown"); - break; } } - } - self.task_client.recv_timeout().await; - - log::debug!("MixTrafficController: Exiting"); - }); + self.task_client.recv_timeout().await; + tracing::debug!("MixTrafficController: Exiting"); + }, + "MixTrafficController" + ); } } diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs index 6ec0e68ae6..f749700ab2 100644 --- a/common/client-core/src/client/mix_traffic/transceiver.rs +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -use log::{debug, error}; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::error::GatewayClientError; @@ -14,6 +13,7 @@ use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use std::fmt::Debug; use std::os::raw::c_int as RawFd; use thiserror::Error; +use tracing::{debug, error}; #[cfg(not(target_arch = "wasm32"))] use futures::channel::oneshot; @@ -27,7 +27,7 @@ fn erase_err(err: E) -> ErasedGate ErasedGatewayError(Box::new(err)) } -/// This combines combines the functionalities of being able to send and receive mix packets. +/// This combines the functionalities of being able to send and receive mix packets. #[async_trait] pub trait GatewayTransceiver: GatewaySender + GatewayReceiver { fn gateway_identity(&self) -> ed25519::PublicKey; @@ -87,7 +87,7 @@ impl GatewayTransceiver for Box { message: ClientRequest, ) -> Result<(), GatewayClientError> { let _ = (**self).send_client_request(message.clone()).await?; - log::debug!("Sent client request: {:?}", message); + tracing::debug!("Sent client request: {:?}", message); Ok(()) } } @@ -269,6 +269,8 @@ pub struct MockGateway { } impl Default for MockGateway { + // test code + #[allow(clippy::unwrap_used)] fn default() -> Self { MockGateway { dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7" diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 4b5d483ee5..7c7000c3b3 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -5,7 +5,6 @@ use super::action_controller::{AckActionSender, Action}; use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; use futures::StreamExt; -use log::*; use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::{ acknowledgements::{identifier::recover_identifier, AckKey}, @@ -13,6 +12,7 @@ use nym_sphinx::{ }; use nym_task::TaskClient; use std::sync::Arc; +use tracing::*; /// Module responsible for listening for any data resembling acknowledgements from the network /// and firing actions to remove them from the 'Pending' state. @@ -65,7 +65,7 @@ impl AcknowledgementListener { return; } - trace!("Received {} from the mix network", frag_id); + trace!("Received {frag_id} from the mix network"); self.stats_tx .report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into()); if let Err(err) = self @@ -93,16 +93,16 @@ impl AcknowledgementListener { acks = self.ack_receiver.next() => match acks { Some(acks) => self.handle_ack_receiver_item(acks).await, None => { - log::trace!("AcknowledgementListener: Stopping since channel closed"); + tracing::trace!("AcknowledgementListener: Stopping since channel closed"); break; } }, _ = self.task_client.recv() => { - log::trace!("AcknowledgementListener: Received shutdown"); + tracing::trace!("AcknowledgementListener: Received shutdown"); } } } self.task_client.recv_timeout().await; - log::debug!("AcknowledgementListener: Exiting"); + tracing::debug!("AcknowledgementListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 6235b7c477..350b400e46 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -5,7 +5,6 @@ use super::PendingAcknowledgement; use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender; use futures::channel::mpsc; use futures::StreamExt; -use log::*; use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey}; use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_sphinx::Delay as SphinxDelay; @@ -13,6 +12,7 @@ use nym_task::TaskClient; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use tracing::*; pub(crate) type AckActionSender = mpsc::UnboundedSender; pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver; @@ -126,7 +126,7 @@ impl ActionController { fn handle_insert(&mut self, pending_acks: Vec) { for pending_ack in pending_acks { let frag_id = pending_ack.message_chunk.fragment_identifier(); - trace!("{} is inserted", frag_id); + trace!("{frag_id} is inserted"); if self .pending_acks_data @@ -161,22 +161,16 @@ impl ActionController { let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout); *queue_key = Some(new_queue_key) } else { - debug!( - "Tried to START TIMER on pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to START TIMER on pending ack that is already gone! - {frag_id}"); } } fn handle_remove(&mut self, frag_id: FragmentIdentifier) { - trace!("{} is getting removed", frag_id); + trace!("{frag_id} is getting removed"); match self.pending_acks_data.remove(&frag_id) { None => { - debug!( - "Tried to REMOVE pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to REMOVE pending ack that is already gone! - {frag_id}"); } Some((_, queue_key)) => { if let Some(queue_key) = queue_key { @@ -188,10 +182,7 @@ impl ActionController { } else { // I'm not 100% sure if having a `None` key is even possible here // (REMOVE would have to be called before START TIMER), - debug!( - "Tried to REMOVE pending ack without TIMER active - {}", - frag_id - ); + debug!("Tried to REMOVE pending ack without TIMER active - {frag_id}"); } } } @@ -200,27 +191,26 @@ impl ActionController { // initiated basically as a first step of retransmission. At first data has its delay updated // (as new sphinx packet was created with new expected delivery time) fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { - trace!("{} is updating its delay", frag_id); + trace!("{frag_id} is updating its delay"); // TODO: is it possible to solve this without either locking or temporarily removing the value? if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) { - // this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) + // SAFETY: this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) // or `ReplyController` (for 'reply' packets) which held the other potential // reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference // was dropped hence this unwrap is safe. + #[allow(clippy::unwrap_used)] let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap(); inner_data.update_retransmitted(delay); self.pending_acks_data .insert(frag_id, (Arc::new(inner_data), queue_key)); } else { - debug!( - "Tried to UPDATE TIMER on pending ack that is already gone! - {}", - frag_id - ); + debug!("Tried to UPDATE TIMER on pending ack that is already gone! - {frag_id}"); } } // note: when the entry expires it's automatically removed from pending_acks_timers + #[allow(clippy::panic)] fn handle_expired_ack_timer(&mut self, expired_ack: Expired) { let frag_id = expired_ack.into_inner(); @@ -241,7 +231,7 @@ impl ActionController { .unbounded_send(Arc::downgrade(pending_ack_data)) { if !self.task_client.is_shutdown_poll() { - log::error!("Failed to send pending ack for retransmission: {err}"); + tracing::error!("Failed to send pending ack for retransmission: {err}"); } } } else { @@ -269,7 +259,7 @@ impl ActionController { action = self.incoming_actions.next() => match action { Some(action) => self.process_action(action), None => { - log::trace!( + tracing::trace!( "ActionController: Stopping since incoming actions channel closed" ); break; @@ -278,17 +268,17 @@ impl ActionController { expired_ack = self.pending_acks_timers.next() => match expired_ack { Some(expired_ack) => self.handle_expired_ack_timer(expired_ack), None => { - log::trace!("ActionController: Stopping since ack channel closed"); + tracing::trace!("ActionController: Stopping since ack channel closed"); break; } }, _ = self.task_client.recv() => { - log::trace!("ActionController: Received shutdown"); + tracing::trace!("ActionController: Received shutdown"); break; } } } self.task_client.recv_timeout().await; - log::debug!("ActionController: Exiting"); + tracing::debug!("ActionController: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 49da4bce9c..f47f050b3b 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -5,7 +5,6 @@ use crate::client::inbound_messages::{InputMessage, InputMessageReceiver}; use crate::client::real_messages_control::message_handler::MessageHandler; use crate::client::real_messages_control::real_traffic_stream::RealMessage; use crate::client::replies::reply_controller::ReplyControllerSender; -use log::*; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::forwarding::packet::MixPacket; @@ -13,6 +12,7 @@ use nym_sphinx::params::PacketType; use nym_task::connections::TransmissionLane; use nym_task::TaskClient; use rand::{CryptoRng, Rng}; +use tracing::*; /// Module responsible for dealing with the received messages: splitting them, creating acknowledgements, /// putting everything into sphinx packets, etc. @@ -120,6 +120,7 @@ where } } + #[allow(clippy::panic)] async fn on_input_message(&mut self, msg: InputMessage) { match msg { InputMessage::Regular { @@ -213,7 +214,9 @@ where self.handle_premade_packets(msgs, lane).await } // MessageWrappers can't be nested - InputMessage::MessageWrapper { .. } => unimplemented!(), + InputMessage::MessageWrapper { .. } => { + panic!("attempted to use nested MessageWrapper") + } }, }; } @@ -223,21 +226,24 @@ where while !self.task_client.is_shutdown() { tokio::select! { + biased; + _ = self.task_client.recv() => { + tracing::trace!("InputMessageListener: Received shutdown"); + break; + } input_msg = self.input_receiver.recv() => match input_msg { Some(input_msg) => { self.on_input_message(input_msg).await; }, None => { - log::trace!("InputMessageListener: Stopping since channel closed"); + tracing::trace!("InputMessageListener: Stopping since channel closed"); break; } }, - _ = self.task_client.recv() => { - log::trace!("InputMessageListener: Received shutdown"); - } + } } self.task_client.recv_timeout().await; - log::debug!("InputMessageListener: Exiting"); + tracing::debug!("InputMessageListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index c3f63b07ad..ea332ebdaf 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -13,7 +13,6 @@ use crate::client::replies::reply_controller::ReplyControllerSender; use crate::spawn_future; use action_controller::AckActionReceiver; use futures::channel::mpsc; -use log::*; use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::params::{PacketSize, PacketType}; @@ -30,6 +29,7 @@ use std::{ sync::{Arc, Weak}, time::Duration, }; +use tracing::*; pub(crate) use action_controller::{AckActionSender, Action}; @@ -298,29 +298,44 @@ where let mut sent_notification_listener = self.sent_notification_listener; let mut action_controller = self.action_controller; - spawn_future(async move { - acknowledgement_listener.run().await; - debug!("The acknowledgement listener has finished execution!"); - }); + spawn_future!( + async move { + acknowledgement_listener.run().await; + debug!("The acknowledgement listener has finished execution!"); + }, + "AcknowledgementController::AcknowledgementListener" + ); - spawn_future(async move { - input_message_listener.run().await; - debug!("The input listener has finished execution!"); - }); + spawn_future!( + async move { + input_message_listener.run().await; + debug!("The input listener has finished execution!"); + }, + "AcknowledgementController::InputMessageListener" + ); - spawn_future(async move { - retransmission_request_listener.run(packet_type).await; - debug!("The retransmission request listener has finished execution!"); - }); + spawn_future!( + async move { + retransmission_request_listener.run(packet_type).await; + debug!("The retransmission request listener has finished execution!"); + }, + "AcknowledgementController::RetransmissionRequestListener" + ); - spawn_future(async move { - sent_notification_listener.run().await; - debug!("The sent notification listener has finished execution!"); - }); + spawn_future!( + async move { + sent_notification_listener.run().await; + debug!("The sent notification listener has finished execution!"); + }, + "AcknowledgementController::SentNotificationListener" + ); - spawn_future(async move { - action_controller.run().await; - debug!("The controller has finished execution!"); - }); + spawn_future!( + async move { + action_controller.run().await; + debug!("The controller has finished execution!"); + }, + "AcknowledgementController::ActionController" + ); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 2f7b5d976e..0b066d0cc0 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -10,13 +10,13 @@ use crate::client::real_messages_control::message_handler::{MessageHandler, Prep use crate::client::real_messages_control::real_traffic_stream::RealMessage; use crate::client::replies::reply_controller::ReplyControllerSender; use futures::StreamExt; -use log::*; use nym_sphinx::chunking::fragment::Fragment; use nym_sphinx::preparer::PreparedFragment; use nym_sphinx::{addressing::clients::Recipient, params::PacketType}; use nym_task::{connections::TransmissionLane, TaskClient}; use rand::{CryptoRng, Rng}; use std::sync::{Arc, Weak}; +use tracing::*; // responsible for packet retransmission upon fired timer pub(super) struct RetransmissionRequestListener { @@ -179,19 +179,22 @@ where while !self.task_client.is_shutdown() { tokio::select! { + biased; + _ = self.task_client.recv() => { + tracing::trace!("RetransmissionRequestListener: Received shutdown"); + break; + } timed_out_ack = self.request_receiver.next() => match timed_out_ack { Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await, None => { - log::trace!("RetransmissionRequestListener: Stopping since channel closed"); + tracing::trace!("RetransmissionRequestListener: Stopping since channel closed"); break; } }, - _ = self.task_client.recv() => { - log::trace!("RetransmissionRequestListener: Received shutdown"); - } + } } self.task_client.recv_timeout().await; - log::debug!("RetransmissionRequestListener: Exiting"); + tracing::debug!("RetransmissionRequestListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs index 0b563c375d..9ee9cdf461 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs @@ -4,9 +4,9 @@ use super::action_controller::{AckActionSender, Action}; use super::SentPacketNotificationReceiver; use futures::StreamExt; -use log::*; use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; use nym_task::TaskClient; +use tracing::*; /// Module responsible for starting up retransmission timers. /// It is required because when we send our packet to the `real traffic stream` controlled @@ -56,17 +56,17 @@ impl SentNotificationListener { self.on_sent_message(frag_id).await; } None => { - log::trace!("SentNotificationListener: Stopping since channel closed"); + tracing::trace!("SentNotificationListener: Stopping since channel closed"); break; } }, _ = self.task_client.recv() => { - log::trace!("SentNotificationListener: Received shutdown"); + tracing::trace!("SentNotificationListener: Received shutdown"); break; } } } assert!(self.task_client.is_shutdown_poll()); - log::debug!("SentNotificationListener: Exiting"); + tracing::debug!("SentNotificationListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 13cf1c0717..1dca40cf7e 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -9,11 +9,11 @@ use crate::client::real_messages_control::{AckActionSender, Action}; use crate::client::replies::reply_controller::MaxRetransmissions; use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit}; -use log::{debug, error, info, trace, warn}; +use nym_client_core_surb_storage::RetrievedReplySurb; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage}; -use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey}; +use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier}; use nym_sphinx::message::NymMessage; use nym_sphinx::params::{PacketSize, PacketType}; @@ -27,6 +27,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use thiserror::Error; +use tracing::{debug, error, info, trace, warn}; // TODO: move that error elsewhere since it seems to be contaminating different files #[derive(Debug, Error)] @@ -34,6 +35,9 @@ pub enum PreparationError { #[error(transparent)] NymTopologyError(#[from] NymTopologyError), + #[error("message wasn't split into any fragments!")] + EmptyFragments, + #[error("message too long for a single SURB, splitting into {fragments} fragments.")] MessageTooLongForSingleSurb { fragments: usize }, @@ -44,7 +48,7 @@ pub enum PreparationError { } impl PreparationError { - fn return_surbs(self, returned_surbs: Vec) -> SurbWrappedPreparationError { + fn return_surbs(self, returned_surbs: Vec) -> SurbWrappedPreparationError { SurbWrappedPreparationError { source: self, returned_surbs: Some(returned_surbs), @@ -58,7 +62,7 @@ pub struct SurbWrappedPreparationError { #[source] source: PreparationError, - returned_surbs: Option>, + returned_surbs: Option>, } impl From for SurbWrappedPreparationError @@ -80,7 +84,7 @@ impl SurbWrappedPreparationError { target: &AnonymousSenderTag, ) -> PreparationError { if let Some(reply_surbs) = self.returned_surbs { - surb_storage.insert_surbs(target, reply_surbs) + surb_storage.re_insert_reply_surbs(target, reply_surbs) } self.source } @@ -98,6 +102,15 @@ pub(crate) struct Config { /// Specify whether route selection should be determined by the packet header. deterministic_route_selection: bool, + /// Indicates whether to mix hops or not. If mix hops are enabled, traffic + /// will be routed as usual, to the entry gateway, through three mix nodes, egressing + /// through the exit gateway. If mix hops are disabled, traffic will be routed directly + /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops + /// requires use of the updated SURB packet format. + disable_mix_hops: bool, + /// Average delay a data packet is going to get delay at a single mixnode. average_packet_delay: Duration, @@ -133,6 +146,7 @@ impl Config { primary_packet_size: PacketSize::default(), secondary_packet_size: None, use_legacy_sphinx_format: use_legacy_reply_surb_format, + disable_mix_hops: false, } } @@ -147,6 +161,16 @@ impl Config { self.secondary_packet_size = packet_size; self } + + /// Configure whether messages senders using this config should use mix hops or not when sending messages. + /// + /// This overrides the `use_legacy_sphinx_format` setting as disabled mix hops + /// requires use of the updated SURB packet format. + pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self { + self.disable_mix_hops = disable_mix_hops; + self.use_legacy_sphinx_format = false; + self + } } #[derive(Clone)] @@ -193,6 +217,7 @@ where config.average_packet_delay, config.average_ack_delay, config.use_legacy_sphinx_format, + config.disable_mix_hops, ); MessageHandler { config, @@ -207,6 +232,10 @@ where } } + pub(crate) fn topology_access_handle(&self) -> &TopologyAccessor { + &self.topology_access + } + fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag { if let Some(existing) = self.tag_storage.try_get_existing(recipient) { trace!("we already had sender tag for {recipient}"); @@ -254,10 +283,10 @@ where } } - async fn generate_reply_surbs_with_keys( + async fn generate_reply_surbs( &mut self, amount: usize, - ) -> Result<(Vec, Vec), PreparationError> { + ) -> Result, PreparationError> { let topology_permit = self.topology_access.get_read_permit().await; let topology = self.get_topology(&topology_permit)?; @@ -267,24 +296,19 @@ where topology, )?; - let reply_keys = reply_surbs - .iter() - .map(|s| *s.encryption_key()) - .collect::>(); - - Ok((reply_surbs, reply_keys)) + Ok(reply_surbs) } pub(crate) async fn try_send_single_surb_message( &mut self, target: AnonymousSenderTag, message: ReplyMessage, - reply_surb: ReplySurb, + reply_surb: RetrievedReplySurb, is_extra_surb_request: bool, ) -> Result<(), SurbWrappedPreparationError> { let msg = NymMessage::new_reply(message); let packet_size = self.optimal_packet_size(&msg); - debug!("Using {packet_size} packets for {msg}"); + trace!("Using {packet_size} packets for {msg}"); let mut fragment = self .message_preparer @@ -299,6 +323,16 @@ where }); } + if fragment.is_empty() { + error!("CRITICAL FAILURE: our split message didn't result in any sendable fragments"); + return Err(SurbWrappedPreparationError { + source: PreparationError::EmptyFragments, + returned_surbs: Some(vec![reply_surb]), + }); + } + + // SAFETY: we just checked we have one fragment + #[allow(clippy::unwrap_used)] let chunk = fragment.pop().unwrap(); let chunk_clone = chunk.clone(); let prepared_fragment = self @@ -310,7 +344,10 @@ where Some(chunk.fragment_identifier()), ); let delay = prepared_fragment.total_delay; - let max_retransmissions = None; + + // we have to set a maximum number of retransmissions in case we fail to retrieve + // surbs for a long period of time; we don't want to be stuck constantly resending the data + let max_retransmissions = Some(10); let pending_ack = PendingAcknowledgement::new_anonymous( chunk, delay, @@ -333,7 +370,7 @@ where pub(crate) async fn try_request_additional_reply_surbs( &mut self, from: AnonymousSenderTag, - reply_surb: ReplySurb, + reply_surb: RetrievedReplySurb, amount: u32, ) -> Result<(), SurbWrappedPreparationError> { debug!("requesting {amount} reply SURBs from {from}"); @@ -348,7 +385,7 @@ where pub(crate) fn split_reply_message(&mut self, message: Vec) -> Vec { let msg = NymMessage::new_reply(ReplyMessage::new_data_message(message)); let packet_size = self.optimal_packet_size(&msg); - debug!("Using {packet_size} packets for {msg}"); + trace!("Using {packet_size} packets for {msg}"); self.message_preparer .pad_and_split_message(msg, packet_size) @@ -373,11 +410,9 @@ where &mut self, target: AnonymousSenderTag, fragments: Vec, - reply_surbs: Vec, + reply_surbs: impl IntoIterator, lane: TransmissionLane, ) -> Result<(), SurbWrappedPreparationError> { - // TODO: technically this is performing an unnecessary cloning, but in the grand scheme of things - // is it really that bad? self.try_send_reply_chunks( target, fragments.into_iter().map(|f| (lane, f)).collect(), @@ -390,7 +425,7 @@ where &mut self, target: AnonymousSenderTag, fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>, - reply_surbs: Vec, + reply_surbs: impl IntoIterator, ) -> Result<(), SurbWrappedPreparationError> { let prepared_fragments = self .prepare_reply_chunks_for_sending( @@ -481,7 +516,7 @@ where } else { self.optimal_packet_size(&message) }; - debug!("Using {packet_size} packets for {message}"); + trace!("Using {packet_size} packets for {message}"); let fragments = self .message_preparer .pad_and_split_message(message, packet_size); @@ -513,6 +548,7 @@ where pending_acks.push(pending_ack); } + drop(topology_permit); self.insert_pending_acks(pending_acks); self.forward_messages(real_messages, lane).await; @@ -527,8 +563,12 @@ where ) -> Result<(), PreparationError> { debug!("Sending additional reply SURBs with packet type {packet_type}"); let sender_tag = self.get_or_create_sender_tag(&recipient); - let (reply_surbs, reply_keys) = - self.generate_reply_surbs_with_keys(amount as usize).await?; + let reply_surbs = self.generate_reply_surbs(amount as usize).await?; + + let reply_keys = reply_surbs + .iter() + .map(|s| *s.encryption_key()) + .collect::>(); let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs( self.config.use_legacy_sphinx_format, @@ -548,7 +588,7 @@ where ) .await?; - log::trace!("storing {} reply keys", reply_keys.len()); + tracing::trace!("storing {} reply keys", reply_keys.len()); self.reply_key_storage.insert_multiple(reply_keys); Ok(()) @@ -565,9 +605,12 @@ where ) -> Result<(), SurbWrappedPreparationError> { debug!("Sending message with reply SURBs with packet type {packet_type}"); let sender_tag = self.get_or_create_sender_tag(&recipient); - let (reply_surbs, reply_keys) = self - .generate_reply_surbs_with_keys(num_reply_surbs as usize) - .await?; + let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?; + + let reply_keys = reply_surbs + .iter() + .map(|s| *s.encryption_key()) + .collect::>(); let message = NymMessage::new_repliable(RepliableMessage::new_data( self.config.use_legacy_sphinx_format, @@ -585,7 +628,7 @@ where ) .await?; - log::trace!("storing {} reply keys", reply_keys.len()); + tracing::trace!("storing {} reply keys", reply_keys.len()); self.reply_key_storage.insert_multiple(reply_keys); Ok(()) @@ -615,20 +658,12 @@ where pub(crate) async fn prepare_reply_chunks_for_sending( &mut self, fragments: Vec, - reply_surbs: Vec, + reply_surbs: impl IntoIterator, ) -> Result, SurbWrappedPreparationError> { - debug_assert_eq!( - fragments.len(), - reply_surbs.len(), - "attempted to send {} fragments with {} reply surbs", - fragments.len(), - reply_surbs.len() - ); - let topology_permit = self.topology_access.get_read_permit().await; let topology = match self.get_topology(&topology_permit) { Ok(topology) => topology, - Err(err) => return Err(err.return_surbs(reply_surbs)), + Err(err) => return Err(err.return_surbs(reply_surbs.into_iter().collect())), }; Ok(fragments @@ -636,12 +671,13 @@ where .zip(reply_surbs.into_iter()) .map(|(fragment, reply_surb)| { // unwrap here is fine as we know we have a valid topology + #[allow(clippy::unwrap_used)] self.message_preparer .prepare_reply_chunk_for_sending( fragment, topology, &self.config.ack_key, - reply_surb, + reply_surb.into(), PacketType::Mix, ) .unwrap() @@ -651,7 +687,7 @@ where pub(crate) async fn try_prepare_single_reply_chunk_for_sending( &mut self, - reply_surb: ReplySurb, + reply_surb: RetrievedReplySurb, chunk: Fragment, ) -> Result { let topology_permit = self.topology_access.get_read_permit().await; @@ -664,7 +700,7 @@ where chunk, topology, &self.config.ack_key, - reply_surb, + reply_surb.into(), PacketType::Mix, )?; @@ -695,17 +731,21 @@ where // tells real message sender (with the poisson timer) to send this to the mix network pub(crate) async fn forward_messages( - &self, + &mut self, messages: Vec, transmission_lane: TransmissionLane, ) { - if let Err(err) = self - .real_message_sender - .send((messages, transmission_lane)) - .await - { - if !self.task_client.is_shutdown_poll() { - error!("Failed to forward messages to the real message sender: {err}"); + tokio::select! { + biased; + _ = self.task_client.recv() => { + trace!("received shutdown while attempting to forward mixnet messages"); + } + sending_res = self.real_message_sender.send((messages, transmission_lane)) => { + if sending_res.is_err() { + error!( + "failed to forward mixnet messages due to closed channel (outside of shutdown!)" + ); + } } } } diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index cd9515062a..b169a9bff8 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -24,7 +24,6 @@ use crate::{ spawn_future, }; use futures::channel::mpsc; -use log::*; use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; @@ -34,7 +33,9 @@ use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths}; use nym_task::TaskClient; use rand::{rngs::OsRng, CryptoRng, Rng}; use std::sync::Arc; +use tracing::*; +use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig; pub(crate) use acknowledgement_control::{AckActionSender, Action}; pub(crate) mod acknowledgement_control; @@ -85,12 +86,6 @@ impl<'a> From<&'a Config> for real_traffic_stream::Config { } } -impl<'a> From<&'a Config> for reply_controller::Config { - fn from(cfg: &'a Config) -> Self { - reply_controller::Config::new(cfg.reply_surbs) - } -} - impl<'a> From<&'a Config> for message_handler::Config { fn from(cfg: &'a Config) -> Self { message_handler::Config::new( @@ -103,6 +98,7 @@ impl<'a> From<&'a Config> for message_handler::Config { ) .with_custom_primary_packet_size(cfg.traffic.primary_packet_size) .with_custom_secondary_packet_size(cfg.traffic.secondary_packet_size) + .disable_mix_hops(cfg.traffic.disable_mix_hops) } } @@ -138,6 +134,7 @@ impl RealMessagesController { #[allow(clippy::too_many_arguments)] pub(crate) fn new( config: Config, + key_rotation_config: KeyRotationConfig, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, @@ -168,7 +165,8 @@ impl RealMessagesController { // create all configs for the components let ack_control_config = (&config).into(); let out_queue_config = (&config).into(); - let reply_controller_config = (&config).into(); + let reply_controller_config = + reply_controller::Config::new(config.reply_surbs, key_rotation_config); let message_handler_config = (&config).into(); // create the actual components @@ -226,14 +224,20 @@ impl RealMessagesController { let ack_control = self.ack_control; let mut reply_control = self.reply_control; - spawn_future(async move { - out_queue_control.run().await; - debug!("The out queue controller has finished execution!"); - }); - spawn_future(async move { - reply_control.run().await; - debug!("The reply controller has finished execution!"); - }); + spawn_future!( + async move { + out_queue_control.run().await; + debug!("The out queue controller has finished execution!"); + }, + "RealMessagesController::OutQueueControl)" + ); + spawn_future!( + async move { + reply_control.run().await; + debug!("The reply controller has finished execution!"); + }, + "RealMessagesController::ReplyController" + ); ack_control.start(packet_type); } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index edc313990f..7cbf3b6ad6 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -9,7 +9,6 @@ use crate::client::transmission_buffer::TransmissionBuffer; use crate::config; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; -use log::*; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::chunking::fragment::FragmentIdentifier; @@ -27,6 +26,7 @@ use rand::{CryptoRng, Rng}; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; +use tracing::*; #[cfg(not(target_arch = "wasm32"))] use tokio::time::{sleep, Sleep}; @@ -202,7 +202,7 @@ where // well technically the message was not sent just yet, but now it's up to internal // queues and client load rather than the required delay. So realistically we can treat // whatever is about to happen as negligible additional delay. - trace!("{} is about to get sent to the mixnet", frag_id); + trace!("{frag_id} is about to get sent to the mixnet"); if let Err(err) = self.sent_notifier.unbounded_send(frag_id) { error!("Failed to notify about sent message: {err}"); } @@ -249,6 +249,8 @@ where } }; + // SAFETY: our topology must be valid at this point + #[allow(clippy::expect_used)] ( generate_loop_cover_packet( &mut self.rng, @@ -278,17 +280,33 @@ where } }; - if let Err(err) = self.mix_tx.send(vec![next_message]).await { - if !self.task_client.is_shutdown_poll() { - log::error!("Failed to send: {err}"); + let sending_res = tokio::select! { + biased; + _ = self.task_client.recv() => { + trace!("received shutdown signal while attempting to send mix message"); + return + } + sending_res = self.mix_tx.send(vec![next_message]) => { + sending_res + } + }; + + match sending_res { + Err(_) => { + if !self.task_client.is_shutdown_poll() { + tracing::error!( + "failed to send mixnet packet due to closed channel (outside of shutdown!)" + ); + } + } + Ok(_) => { + let event = if fragment_id.is_some() { + PacketStatisticsEvent::RealPacketSent(packet_size) + } else { + PacketStatisticsEvent::CoverPacketSent(packet_size) + }; + self.stats_tx.report(event.into()); } - } else { - let event = if fragment_id.is_some() { - PacketStatisticsEvent::RealPacketSent(packet_size) - } else { - PacketStatisticsEvent::CoverPacketSent(packet_size) - }; - self.stats_tx.report(event.into()); } // notify ack controller about sending our message only after we actually managed to push it @@ -313,7 +331,7 @@ where } fn on_close_connection(&mut self, connection_id: ConnectionId) { - log::debug!("Removing lane for connection: {connection_id}"); + tracing::debug!("Removing lane for connection: {connection_id}"); self.transmission_buffer .remove(&TransmissionLane::ConnectionId(connection_id)); } @@ -325,7 +343,7 @@ where fn adjust_current_average_message_sending_delay(&mut self) { let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity(); - log::trace!( + tracing::trace!( "used_slots: {used_slots}, current_multiplier: {}", self.sending_delay_controller.current_multiplier() ); @@ -334,7 +352,7 @@ where .sending_delay_controller .is_backpressure_currently_detected(used_slots) { - log::trace!("Backpressure detected"); + tracing::trace!("Backpressure detected"); self.sending_delay_controller.record_backpressure_detected(); } @@ -436,9 +454,11 @@ where Poll::Ready(None) => Poll::Ready(None), Poll::Ready(Some((real_messages, conn_id))) => { - log::trace!("handling real_messages: size: {}", real_messages.len()); + tracing::trace!("handling real_messages: size: {}", real_messages.len()); self.transmission_buffer.store(&conn_id, real_messages); + // SAFETY: we just stored the message + #[allow(clippy::expect_used)] let real_next = self.pop_next_message().expect("Just stored one"); Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) @@ -483,10 +503,12 @@ where Poll::Ready(None) => Poll::Ready(None), Poll::Ready(Some((real_messages, conn_id))) => { - log::trace!("handling real_messages: size: {}", real_messages.len()); + tracing::trace!("handling real_messages: size: {}", real_messages.len()); // First store what we got for the given connection id self.transmission_buffer.store(&conn_id, real_messages); + // SAFETY: we just stored the message + #[allow(clippy::expect_used)] let real_next = self.pop_next_message().expect("we just added one"); Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) @@ -538,11 +560,11 @@ where }; if packets > 1000 { - log::warn!("{status_str}"); + tracing::warn!("{status_str}"); } else if packets > 0 { - log::info!("{status_str}"); + tracing::info!("{status_str}"); } else { - log::debug!("{status_str}"); + tracing::debug!("{status_str}"); } // Send status message to whoever is listening (possibly UI) @@ -566,7 +588,7 @@ where tokio::select! { biased; _ = shutdown.recv() => { - log::trace!("OutQueueControl: Received shutdown"); + tracing::trace!("OutQueueControl: Received shutdown"); break; } _ = status_timer.tick() => { @@ -575,7 +597,7 @@ where next_message = self.next() => if let Some(next_message) = next_message { self.on_message(next_message).await; } else { - log::trace!("OutQueueControl: Stopping since channel closed"); + tracing::trace!("OutQueueControl: Stopping since channel closed"); break; } } @@ -589,18 +611,18 @@ where tokio::select! { biased; _ = shutdown.recv() => { - log::trace!("OutQueueControl: Received shutdown"); + tracing::trace!("OutQueueControl: Received shutdown"); } next_message = self.next() => if let Some(next_message) = next_message { self.on_message(next_message).await; } else { - log::trace!("OutQueueControl: Stopping since channel closed"); + tracing::trace!("OutQueueControl: Stopping since channel closed"); break; } } } } - log::debug!("OutQueueControl: Exiting"); + tracing::debug!("OutQueueControl: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index d038ac624e..7c147bed8d 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -98,12 +98,12 @@ impl SendingDelayController { self.current_multiplier = (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); self.time_when_changed = get_time_now(); - log::debug!( + tracing::debug!( "Increasing sending delay multiplier to: {}", self.current_multiplier ); } else { - log::warn!("Trying to increase delay multipler higher than allowed"); + tracing::warn!("Trying to increase delay multipler higher than allowed"); } } @@ -112,7 +112,7 @@ impl SendingDelayController { self.current_multiplier = (self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound); self.time_when_changed = get_time_now(); - log::debug!( + tracing::debug!( "Decreasing sending delay multiplier to: {}", self.current_multiplier ); @@ -164,11 +164,11 @@ impl SendingDelayController { self.current_multiplier() ); if self.current_multiplier() > 0 { - log::debug!("{}", status_str); + tracing::debug!("{status_str}"); } else if self.current_multiplier() > 1 { - log::info!("{}", status_str); + tracing::info!("{status_str}"); } else if self.current_multiplier() > 2 { - log::warn!("{}", status_str); + tracing::warn!("{status_str}"); } self.time_when_logged_about_elevated_multiplier = now; } diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index 380e519460..834d8b9be2 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::helpers::get_time_now; use crate::client::replies::{ reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys, }; @@ -8,7 +9,6 @@ use crate::spawn_future; use futures::channel::mpsc; use futures::lock::Mutex; use futures::StreamExt; -use log::*; use nym_crypto::asymmetric::x25519; use nym_crypto::Digest; use nym_gateway_client::MixnetMessageReceiver; @@ -23,7 +23,8 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C use nym_task::TaskClient; use std::collections::HashSet; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Duration; +use tracing::*; // The interval at which we check for stale buffers const STALE_BUFFER_CHECK_INTERVAL: Duration = Duration::from_secs(10); @@ -54,7 +55,7 @@ struct ReceivedMessagesBufferInner { stats_tx: ClientStatsSender, // Periodically check for stale buffers to clean up - last_stale_check: Instant, + last_stale_check: crate::client::helpers::Instant, } impl ReceivedMessagesBufferInner { @@ -154,7 +155,7 @@ impl ReceivedMessagesBufferInner { } fn cleanup_stale_buffers(&mut self) { - let now = Instant::now(); + let now = get_time_now(); if now - self.last_stale_check > STALE_BUFFER_CHECK_INTERVAL { self.last_stale_check = now; self.message_receiver @@ -190,7 +191,7 @@ impl ReceivedMessagesBuffer { message_sender: None, recently_reconstructed: HashSet::new(), stats_tx, - last_stale_check: Instant::now(), + last_stale_check: get_time_now(), })), reply_key_storage, reply_controller_sender, @@ -198,6 +199,7 @@ impl ReceivedMessagesBuffer { } } + #[allow(clippy::panic)] async fn disconnect_sender(&mut self) { let mut guard = self.inner.lock().await; if guard.message_sender.is_none() { @@ -208,6 +210,7 @@ impl ReceivedMessagesBuffer { guard.message_sender = None; } + #[allow(clippy::panic)] async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) { let mut guard = self.inner.lock().await; if guard.message_sender.is_some() { @@ -221,10 +224,7 @@ impl ReceivedMessagesBuffer { let stored_messages = std::mem::take(&mut guard.messages); if !stored_messages.is_empty() { if let Err(err) = sender.unbounded_send(stored_messages) { - error!( - "The sender channel we just received is already invalidated - {:?}", - err - ); + error!("The sender channel we just received is already invalidated - {err:?}"); // put the values back to the buffer // the returned error has two fields: err: SendError and val: T, // where val is the value that was failed to get sent; @@ -310,13 +310,15 @@ impl ReceivedMessagesBuffer { } }; - if let Err(err) = self.reply_controller_sender.send_additional_surbs( - msg.sender_tag, - reply_surbs, - from_surb_request, - ) { - if !self.task_client.is_shutdown_poll() { - error!("{err}"); + if !reply_surbs.is_empty() { + if let Err(err) = self.reply_controller_sender.send_additional_surbs( + msg.sender_tag, + reply_surbs, + from_surb_request, + ) { + if !self.task_client.is_shutdown_poll() { + error!("{err}"); + } } } } @@ -500,20 +502,20 @@ impl RequestReceiver { tokio::select! { biased; _ = self.task_client.recv() => { - log::trace!("RequestReceiver: Received shutdown"); + tracing::trace!("RequestReceiver: Received shutdown"); } request = self.query_receiver.next() => { if let Some(message) = request { self.handle_message(message).await } else { - log::trace!("RequestReceiver: Stopping since channel closed"); + tracing::trace!("RequestReceiver: Stopping since channel closed"); break; } }, } } self.task_client.recv().await; - log::debug!("RequestReceiver: Exiting"); + tracing::debug!("RequestReceiver: Exiting"); } } @@ -544,17 +546,17 @@ impl FragmentedMessageReceiver { if let Some(new_messages) = new_messages { self.received_buffer.handle_new_received(new_messages).await?; } else { - log::trace!("FragmentedMessageReceiver: Stopping since channel closed"); + tracing::trace!("FragmentedMessageReceiver: Stopping since channel closed"); break; } }, _ = self.task_client.recv_with_delay() => { - log::trace!("FragmentedMessageReceiver: Received shutdown"); + tracing::trace!("FragmentedMessageReceiver: Received shutdown"); } } } self.task_client.recv_timeout().await; - log::debug!("FragmentedMessageReceiver: Exiting"); + tracing::debug!("FragmentedMessageReceiver: Exiting"); Ok(()) } } @@ -600,14 +602,20 @@ impl ReceivedMessagesBufferControll let mut fragmented_message_receiver = self.fragmented_message_receiver; let mut request_receiver = self.request_receiver; - spawn_future(async move { - match fragmented_message_receiver.run().await { - Ok(_) => {} - Err(e) => error!("{e}"), - } - }); - spawn_future(async move { - request_receiver.run().await; - }); + spawn_future!( + async move { + match fragmented_message_receiver.run().await { + Ok(_) => {} + Err(e) => error!("{e}"), + } + }, + "ReceivedMessagesBufferController::FragmentedMessageReceiver" + ); + spawn_future!( + async move { + request_receiver.run().await; + }, + "ReceivedMessagesBufferController::RequestReceiver" + ); } } diff --git a/common/client-core/src/client/replies/reply_controller/key_rotation_helpers.rs b/common/client-core/src/client/replies/reply_controller/key_rotation_helpers.rs new file mode 100644 index 0000000000..21553a4235 --- /dev/null +++ b/common/client-core/src/client/replies/reply_controller/key_rotation_helpers.rs @@ -0,0 +1,169 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_topology::NymTopologyMetadata; +use nym_validator_client::models::{ + EpochId, KeyRotationId, KeyRotationInfoResponse, KeyRotationState, +}; +use std::time::Duration; +use time::OffsetDateTime; + +#[derive(Clone, Copy)] +pub(crate) enum SurbRefreshState { + WaitingForNextRotation { last_known: KeyRotationId }, + ScheduledForNextInvocation, +} + +#[derive(Clone, Copy)] +pub(crate) struct ReferenceEpoch { + pub(crate) absolute_epoch_id: EpochId, + pub(crate) start_time: OffsetDateTime, +} + +#[derive(Clone, Copy)] +pub(crate) struct KeyRotationConfig { + pub(crate) epoch_duration: Duration, + pub(crate) rotation_state: KeyRotationState, + pub(crate) reference_epoch: ReferenceEpoch, +} + +impl From for KeyRotationConfig { + fn from(value: KeyRotationInfoResponse) -> Self { + KeyRotationConfig { + epoch_duration: value.details.epoch_duration, + rotation_state: value.details.key_rotation_state, + reference_epoch: ReferenceEpoch { + absolute_epoch_id: value.details.current_absolute_epoch_id, + start_time: value.details.current_epoch_start, + }, + } + } +} + +impl KeyRotationConfig { + pub(crate) fn rotation_lifetime(&self) -> Duration { + (self.rotation_state.validity_epochs + 1) * self.epoch_duration + } + + pub(crate) fn key_rotation_id(&self, current_absolute_epoch_id: EpochId) -> KeyRotationId { + self.rotation_state + .key_rotation_id(current_absolute_epoch_id) + } + + // this is called with the assumption that now is always > reference epoch start + pub(crate) fn expected_current_epoch_id(&self, now: OffsetDateTime) -> EpochId { + let diff_secs = (now - self.reference_epoch.start_time).as_seconds_f64(); + let epochs = (diff_secs / self.epoch_duration.as_secs_f64()).floor() as u32; + + self.reference_epoch.absolute_epoch_id + epochs + } + + fn initial_rotation_epoch_start(&self) -> OffsetDateTime { + let epochs_diff = self + .reference_epoch + .absolute_epoch_id + .saturating_sub(self.rotation_state.initial_epoch_id); + + self.reference_epoch.start_time - epochs_diff * self.epoch_duration + } + + pub(crate) fn key_rotation_start(&self, key_rotation_id: KeyRotationId) -> OffsetDateTime { + let rotation_duration = self.rotation_state.validity_epochs * self.epoch_duration; + let initial_start = self.initial_rotation_epoch_start(); + + // note: key rotation starts from 0 + initial_start + rotation_duration * key_rotation_id + } + + pub(crate) fn expected_current_key_rotation_id(&self, now: OffsetDateTime) -> KeyRotationId { + let expected_current_epoch = self.expected_current_epoch_id(now); + self.key_rotation_id(expected_current_epoch) + } + + pub(crate) fn expected_current_key_rotation_start( + &self, + now: OffsetDateTime, + ) -> OffsetDateTime { + let expected_current_key_rotation_id = self.expected_current_key_rotation_id(now); + self.key_rotation_start(expected_current_key_rotation_id) + } + + pub(crate) fn epoch_stuck(&self, topology_metadata: NymTopologyMetadata) -> bool { + // add leeway of 2mins each direction since transition is not instantaneous + let lower_bound = topology_metadata.refreshed_at - Duration::from_secs(2); + let upper_bound = topology_metadata.refreshed_at + Duration::from_secs(2); + + let expected_epoch_lower = self.expected_current_epoch_id(lower_bound); + let expected_epoch_upper = self.expected_current_epoch_id(upper_bound); + + topology_metadata.absolute_epoch_id != expected_epoch_lower + && topology_metadata.absolute_epoch_id != expected_epoch_upper + } +} + +#[cfg(test)] +mod tests { + use super::*; + use time::macros::datetime; + + fn mock_config() -> KeyRotationConfig { + KeyRotationConfig { + epoch_duration: Duration::from_secs(60 * 60), + rotation_state: KeyRotationState { + validity_epochs: 10, + initial_epoch_id: 80, + }, + reference_epoch: ReferenceEpoch { + absolute_epoch_id: 100, + start_time: datetime!(2025-06-30 12:00:00+00:00), + }, + } + } + + #[test] + fn expected_current_key_rotation_start() { + // rot0: 80-89 + // rot1: 90-99 + // rot2: 100-109 + // rot3: 110-119 + // ... etc + let cfg = mock_config(); + + assert_eq!( + cfg.initial_rotation_epoch_start(), + datetime!(2025-06-29 16:00:00+00:00) + ); + + let fake_now = datetime!(2025-06-30 12:00:00+00:00); + assert_eq!(cfg.expected_current_epoch_id(fake_now), 100); + assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 2); + assert_eq!( + cfg.expected_current_key_rotation_start(fake_now), + datetime!(2025-06-30 12:00:00+00:00) + ); + + let fake_now = datetime!(2025-06-30 12:30:00+00:00); + assert_eq!(cfg.expected_current_epoch_id(fake_now), 100); + assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 2); + assert_eq!( + cfg.expected_current_key_rotation_start(fake_now), + datetime!(2025-06-30 12:00:00+00:00) + ); + + let fake_now = datetime!(2025-06-30 13:01:00+00:00); + assert_eq!(cfg.expected_current_epoch_id(fake_now), 101); + assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 2); + assert_eq!( + cfg.expected_current_key_rotation_start(fake_now), + datetime!(2025-06-30 12:00:00+00:00) + ); + + let fake_now = datetime!(2025-06-30 22:02:00+00:00); + assert_eq!(cfg.expected_current_epoch_id(fake_now), 110); + assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 3); + assert_eq!( + cfg.expected_current_key_rotation_start(fake_now), + datetime!(2025-06-30 22:00:00+00:00) + ); + } +} diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index 2cf7ac9eaa..60004c2011 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -1,45 +1,46 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; -use crate::client::real_messages_control::message_handler::{ - FragmentWithMaxRetransmissions, MessageHandler, PreparationError, -}; +use crate::client::helpers::new_interval_stream; +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig; use crate::client::replies::reply_storage::CombinedReplyStorage; -use futures::channel::oneshot; +use crate::config; use futures::StreamExt; -use log::{debug, error, info, trace, warn}; -use nym_sphinx::addressing::clients::Recipient; -use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use nym_sphinx::anonymous_replies::ReplySurb; -use nym_sphinx::chunking::fragment::FragmentIdentifier; -use nym_task::connections::{ConnectionId, TransmissionLane}; use nym_task::TaskClient; +use rand::rngs::OsRng; use rand::{CryptoRng, Rng}; -use std::cmp::{max, min}; -use std::collections::btree_map::Entry; -use std::collections::{BTreeMap, HashMap}; -use std::sync::{Arc, Weak}; use std::time::Duration; use time::OffsetDateTime; +use tracing::debug; -use crate::client::helpers::new_interval_stream; -use crate::client::transmission_buffer::TransmissionBuffer; -use crate::config; +use crate::client::replies::reply_controller::receiver_controller::ReceiverReplyController; +use crate::client::replies::reply_controller::sender_controller::SenderReplyController; pub(crate) use requests::{ReplyControllerMessage, ReplyControllerReceiver, ReplyControllerSender}; +pub mod key_rotation_helpers; +mod receiver_controller; pub mod requests; +mod sender_controller; -// this is still left as a separate config so I wouldn't need to replace it everywhere -// plus its not unreasonable to think that we might need something outside config::ReplySurbs struct +#[derive(Clone, Copy)] pub struct Config { reply_surbs: config::ReplySurbs, + + /// Current configuration value of the key rotation as setup on this network. + /// This includes things such as number of epochs per rotation, duration of epochs, etc. + // NOTE: this is operating on the assumption of constant-length epochs + key_rotation: KeyRotationConfig, } impl Config { - pub(crate) fn new(reply_surbs_cfg: config::ReplySurbs) -> Self { + pub(crate) fn new( + reply_surbs_cfg: config::ReplySurbs, + key_rotation: KeyRotationConfig, + ) -> Self { Self { reply_surbs: reply_surbs_cfg, + key_rotation, } } } @@ -50,651 +51,50 @@ impl Config { // - so I guess it will handle all 'RepliableMessage' and requests from 'ReplyMessage' // - replies to "give additional surbs" requests // - will reply to future heartbeats - pub type MaxRetransmissions = Option; -// TODO: this should be split into ingress and egress controllers -// because currently its trying to perform two distinct jobs pub struct ReplyController { config: Config, - // TODO: incorporate that field at some point - // and use binomial distribution to determine the expected required number - // of surbs required to send the message through - // expected_reliability: f32, + sender_controller: SenderReplyController, + receiver_controller: ReceiverReplyController, + request_receiver: ReplyControllerReceiver, - pending_replies: - HashMap>, - - /// Retransmission packets that have already timed out and are waiting for additional reply SURBs - /// so that they could be sent back to the network. Once we receive more SURBs, we should send them ASAP. - // TODO: when purging stale entries, we must take extra care to also purge all pending ACK data!! - pending_retransmissions: - HashMap>>, - - message_handler: MessageHandler, - full_reply_storage: CombinedReplyStorage, // Listen for shutdown signals task_client: TaskClient, } -impl ReplyController -where - R: CryptoRng + Rng, -{ +impl ReplyController { pub(crate) fn new( config: Config, - message_handler: MessageHandler, + message_handler: MessageHandler, full_reply_storage: CombinedReplyStorage, request_receiver: ReplyControllerReceiver, task_client: TaskClient, ) -> Self { ReplyController { config, + sender_controller: SenderReplyController::new( + config, + &full_reply_storage, + message_handler.clone(), + ), + receiver_controller: ReceiverReplyController::new( + config, + full_reply_storage.surbs_storage(), + message_handler, + ), request_receiver, - pending_replies: HashMap::new(), - pending_retransmissions: HashMap::new(), - message_handler, - full_reply_storage, task_client, } } +} - fn insert_pending_replies>( - &mut self, - recipient: &AnonymousSenderTag, - fragments: I, - lane: TransmissionLane, - ) { - trace!("buffering pending replies for {recipient}"); - self.pending_replies - .entry(*recipient) - .or_insert_with(TransmissionBuffer::new) - .store(&lane, fragments) - } - - fn re_insert_pending_replies( - &mut self, - recipient: &AnonymousSenderTag, - fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>, - ) { - trace!("re-inserting pending replies for {recipient}"); - // the buffer should ALWAYS exist at this point, if it doesn't, it's a bug... - self.pending_replies - .entry(*recipient) - .or_insert_with(TransmissionBuffer::new) - .store_multiple(fragments) - } - - fn re_insert_pending_retransmission( - &mut self, - recipient: &AnonymousSenderTag, - data: Vec>, - ) { - trace!("re-inserting pending retransmissions for {recipient}"); - // the underlying entry MUST exist as we've just got data from there - let map_entry = self - .pending_retransmissions - .get_mut(recipient) - .expect("our pending retransmission entry is somehow gone!"); - - for pending in data { - // if it's 0, we don't need to do anything - we just got that ack! - if Arc::strong_count(&pending) > 1 { - let id = pending.inner_fragment_identifier(); - let downgraded = Arc::downgrade(&pending); - map_entry.insert(id, downgraded); - } - } - } - - fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool { - trace!("checking if we should request more surbs from {target}"); - - let pending_queue_size = self - .pending_replies - .get(target) - .map(|pending_queue| pending_queue.total_size()) - .unwrap_or_default(); - - let retransmission_queue = self - .pending_retransmissions - .get(target) - .map(|pending_queue| pending_queue.len()) - .unwrap_or_default(); - - let total_queue = pending_queue_size + retransmission_queue; - - let available_surbs = self - .full_reply_storage - .surbs_storage_ref() - .available_surbs(target); - let pending_surbs = self - .full_reply_storage - .surbs_storage_ref() - .pending_reception(target) as usize; - let min_surbs_threshold = self - .full_reply_storage - .surbs_storage_ref() - .min_surb_threshold(); - let max_surbs_threshold = self - .full_reply_storage - .surbs_storage_ref() - .max_surb_threshold(); - let min_surbs_threshold_buffer = - self.config.reply_surbs.minimum_reply_surb_threshold_buffer; - - // After clearing the queue, we want to have at least `min_surbs_threshold` surbs available - // and reserved for requesting additional surbs, and in addition to that we also want to - // have `min_surbs_threshold_buffer` surbs available proactively. - let target_surbs_after_clearing_queue = min_surbs_threshold + min_surbs_threshold_buffer; - - // Check if we have enough surbs to handle the total queue and maintain minimum thresholds - let total_required_surbs = total_queue + target_surbs_after_clearing_queue; - let total_available_surbs = pending_surbs + available_surbs; - - debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}"); - - // We should request more surbs if: - // 1. We haven't hit the maximum surb threshold, and - // 2. We don't have enough surbs to handle the queue plus minimum thresholds - let is_below_max_threshold = total_available_surbs < max_surbs_threshold; - let is_below_required_surbs = total_available_surbs < total_required_surbs; - - is_below_max_threshold && is_below_required_surbs - } - - async fn handle_send_reply( - &mut self, - recipient_tag: AnonymousSenderTag, - data: Vec, - lane: TransmissionLane, - max_retransmissions: Option, - ) { - if !self - .full_reply_storage - .surbs_storage_ref() - .contains_surbs_for(&recipient_tag) - { - warn!("received reply request for {:?} but we don't have any surbs stored for that recipient!", recipient_tag); - return; - } - - trace!("handling reply to {:?}", recipient_tag); - let mut fragments = self.message_handler.split_reply_message(data); - let total_size = fragments.len(); - trace!("This reply requires {:?} SURBs", total_size); - - let available_surbs = self - .full_reply_storage - .surbs_storage_ref() - .available_surbs(&recipient_tag); - let min_surbs_threshold = self - .full_reply_storage - .surbs_storage_ref() - .min_surb_threshold(); - - let max_to_send = if available_surbs > min_surbs_threshold { - min(fragments.len(), available_surbs - min_surbs_threshold) - } else { - 0 - }; - - if max_to_send > 0 { - let (surbs, _surbs_left) = self - .full_reply_storage - .surbs_storage_ref() - .get_reply_surbs(&recipient_tag, max_to_send); - - if let Some(reply_surbs) = surbs { - let to_send = fragments - .drain(..max_to_send) - .map(|f| FragmentWithMaxRetransmissions { - fragment: f, - max_retransmissions, - }) - .collect::>(); - - if let Err(err) = self - .message_handler - .try_send_reply_chunks_on_lane( - recipient_tag, - to_send.clone(), - reply_surbs, - lane, - ) - .await - { - let err = err.return_unused_surbs( - self.full_reply_storage.surbs_storage_ref(), - &recipient_tag, - ); - warn!("failed to send reply to {recipient_tag}: {err}"); - info!( - "buffering {no_fragments} fragments for {recipient_tag}", - no_fragments = to_send.len() - ); - self.insert_pending_replies(&recipient_tag, to_send, lane); - } - } - } - - // if there's leftover data we didn't send because we didn't have enough (or any) surbs - buffer it - if !fragments.is_empty() { - // Ideally we should have enough surbs above the minimum threshold to handle sending - // new replies without having to first request more surbs. That's why I'd like to log - // these cases as they might indicate a problem with the surb management. - debug!( - "buffering {no_fragments} fragments for {recipient_tag}", - no_fragments = fragments.len() - ); - let fragments: Vec<_> = fragments - .into_iter() - .map(|fragment| FragmentWithMaxRetransmissions { - fragment, - max_retransmissions, - }) - .collect(); - self.insert_pending_replies(&recipient_tag, fragments, lane); - } - - if self.should_request_more_surbs(&recipient_tag) { - self.request_reply_surbs_for_queue_clearing(recipient_tag) - .await; - } - } - - async fn request_additional_reply_surbs( - &mut self, - target: AnonymousSenderTag, - amount: u32, - ) -> Result<(), PreparationError> { - debug!("requesting {amount} additional reply surbs for {target}"); - let reply_surb = self - .full_reply_storage - .surbs_storage_ref() - .get_reply_surb_ignoring_threshold(&target) - .and_then(|(reply_surb, _)| reply_surb) - .ok_or(PreparationError::NotEnoughSurbs { - available: 0, - required: 1, - })?; - - if let Err(err) = self - .message_handler - .try_request_additional_reply_surbs(target, reply_surb, amount) - .await - { - let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); - warn!( - "failed to request additional surbs from {:?} - {err}", - target - ); - return Err(err); - } else { - self.full_reply_storage - .surbs_storage_ref() - .increment_pending_reception(&target, amount); - } - - Ok(()) - } - - async fn try_clear_pending_retransmission(&mut self, target: AnonymousSenderTag) { - trace!("trying to clear pending retransmission queue"); - let available_surbs = self - .full_reply_storage - .surbs_storage_ref() - .available_surbs(&target); - let min_surbs_threshold = self - .full_reply_storage - .surbs_storage_ref() - .min_surb_threshold(); - - let max_to_clear = if available_surbs > min_surbs_threshold { - available_surbs - min_surbs_threshold - } else { - trace!("we don't have enough surbs for retransmission queue clearing..."); - return; - }; - trace!("we can clear up to {max_to_clear} entries"); - - let Some(pending) = self.pending_retransmissions.get_mut(&target) else { - trace!("there are no pending retransmissions for {target}!"); - return; - }; - - let mut to_take = Vec::new(); - - while to_take.len() < max_to_clear { - if let Some((_, data)) = pending.pop_first() { - // no need to do anything if we failed to upgrade the reference, - // it means we got the ack while the data was waiting in the queue - if let Some(upgraded) = data.upgrade() { - to_take.push(upgraded) - } - } else { - // our map is empty! - break; - } - } - - if to_take.is_empty() { - // no need to do anything - return; - } - - let (surbs_for_reply, _) = self - .full_reply_storage - .surbs_storage_ref() - .get_reply_surbs(&target, to_take.len()); - - let Some(surbs_for_reply) = surbs_for_reply else { - error!("somehow different task has stolen our reply surbs! - this should have been impossible"); - self.re_insert_pending_retransmission(&target, to_take); - return; - }; - - let to_send_vec = to_take.iter().map(|ack| ack.fragment_data()).collect(); - - let prepared_fragments = match self - .message_handler - .prepare_reply_chunks_for_sending(to_send_vec, surbs_for_reply) - .await - { - Ok(prepared) => prepared, - Err(err) => { - let err = - err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); - self.re_insert_pending_retransmission(&target, to_take); - - warn!( - "failed to clear pending retransmission queue for {:?} - {err}", - target - ); - return; - } - }; - - // we can't fail at this point, so drop all references to acks so that timer updates wouldn't blow up - drop(to_take); - - self.message_handler - .send_retransmission_reply_chunks(prepared_fragments, TransmissionLane::Retransmission) - .await; - } - - fn pop_at_most_pending_replies( - &mut self, - from: &AnonymousSenderTag, - amount: usize, - ) -> Option> { - // if possible, pop all pending replies, if not, pop only entries for which we'd have a reply surb - let total = self.pending_replies.get(from)?.total_size(); - trace!("pending queue has {total} elements"); - if total == 0 { - return None; - } - self.pending_replies - .get_mut(from)? - .pop_at_most_n_next_messages_at_random(amount) - } - - async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) { - trace!("trying to clear pending queue"); - let available_surbs = self - .full_reply_storage - .surbs_storage_ref() - .available_surbs(&target); - let min_surbs_threshold = self - .full_reply_storage - .surbs_storage_ref() - .min_surb_threshold(); - - let max_to_clear = if available_surbs > min_surbs_threshold { - available_surbs - min_surbs_threshold - } else { - trace!("we don't have enough surbs for queue clearing..."); - return; - }; - trace!("we can clear up to {max_to_clear} entries"); - - // we're guaranteed to not get more entries than we have reply surbs for - if let Some(to_send) = self.pop_at_most_pending_replies(&target, max_to_clear) { - let to_send_clone = to_send.clone(); - - if to_send_clone.is_empty() { - panic!( - "please let the devs know if you ever see this message (reply_controller.rs)" - ); - } - - let (surbs_for_reply, _) = self - .full_reply_storage - .surbs_storage_ref() - .get_reply_surbs(&target, to_send_clone.len()); - - let Some(surbs_for_reply) = surbs_for_reply else { - error!("somehow different task has stolen our reply surbs! - this should have been impossible"); - self.re_insert_pending_replies(&target, to_send); - return; - }; - - if let Err(err) = self - .message_handler - .try_send_reply_chunks(target, to_send_clone, surbs_for_reply) - .await - { - let err = - err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); - self.re_insert_pending_replies(&target, to_send); - warn!("failed to clear pending queue for {:?} - {err}", target); - } - } else { - trace!("the pending queue is empty"); - } - } - - async fn handle_received_surbs( - &mut self, - from: AnonymousSenderTag, - reply_surbs: Vec, - from_surb_request: bool, - ) { - trace!("handling received surbs"); - - // clear the requesting flag since we should have been asking for surbs - self.full_reply_storage - .surbs_storage_ref() - .reset_surbs_last_received_at(&from); - if from_surb_request { - self.full_reply_storage - .surbs_storage_ref() - .decrement_pending_reception(&from, reply_surbs.len() as u32); - } - - // store received surbs - self.full_reply_storage - .surbs_storage_ref() - .insert_surbs(&from, reply_surbs); - - // use as many as we can for clearing pending retransmission queue - self.try_clear_pending_retransmission(from).await; - - // use as many as we can for clearing pending 'normal' queue - self.try_clear_pending_queue(from).await; - - // if we have to, request more - if self.should_request_more_surbs(&from) { - self.request_reply_surbs_for_queue_clearing(from).await; - } - } - - async fn handle_surb_request(&mut self, recipient: Recipient, mut amount: u32) { - // 1. check whether we sent any surbs in the past to this recipient, otherwise - // they have no business in asking for more - if !self - .full_reply_storage - .tags_storage_ref() - .exists(&recipient) - { - warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"); - return; - } - - // 2. check whether the requested amount is within sane range - if amount - > self - .config - .reply_surbs - .maximum_allowed_reply_surb_request_size - { - warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.reply_surbs.maximum_allowed_reply_surb_request_size); - amount = self - .config - .reply_surbs - .maximum_allowed_reply_surb_request_size; - } - - // 3. construct and send the surbs away - // (send them in smaller batches to make the experience a bit smoother - let mut remaining = amount; - while remaining > 0 { - let to_send = min(remaining, 100); - if let Err(err) = self - .message_handler - .try_send_additional_reply_surbs( - recipient, - to_send, - nym_sphinx::params::PacketType::Mix, - ) - .await - { - warn!("failed to send additional surbs to {recipient} - {err}"); - } else { - trace!("sent {to_send} reply SURBs to {recipient}"); - } - - remaining -= to_send; - } - } - - fn buffer_pending_ack( - &mut self, - recipient: AnonymousSenderTag, - ack_ref: Arc, - weak_ack_ref: Weak, - ) { - let frag_id = ack_ref.inner_fragment_identifier(); - if let Some(existing) = self.pending_retransmissions.get_mut(&recipient) { - if let Entry::Vacant(e) = existing.entry(frag_id) { - e.insert(weak_ack_ref); - } else { - warn!("we're already trying to retransmit {frag_id}. We must be really behind in surbs!"); - } - } else { - let mut inner = BTreeMap::new(); - inner.insert(frag_id, weak_ack_ref); - self.pending_retransmissions.insert(recipient, inner); - } - } - - async fn handle_reply_retransmission( - &mut self, - recipient_tag: AnonymousSenderTag, - timed_out_ack: Weak, - extra_surbs_request: bool, - ) { - // seems we got the ack in the end - let ack_ref = match timed_out_ack.upgrade() { - Some(ack) => ack, - None => { - debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue"); - return; - } - }; - - // if this is retransmission for obtaining additional reply surbs, - // we can dip below the storage threshold - let (maybe_reply_surb, _) = if extra_surbs_request { - self.full_reply_storage - .surbs_storage_ref() - .get_reply_surb_ignoring_threshold(&recipient_tag) - } else { - self.full_reply_storage - .surbs_storage_ref() - .get_reply_surb(&recipient_tag) - } - .expect("attempted to retransmit a packet to an unknown recipient - we shouldn't have sent the original packet in the first place!"); - - if let Some(reply_surb) = maybe_reply_surb { - match self - .message_handler - .try_prepare_single_reply_chunk_for_sending(reply_surb, ack_ref.fragment_data()) - .await - { - Ok(prepared) => { - // drop the ack ref so that controller would not panic on `UpdateTimer` if that task - // got to handle the action before this function terminated (which is very much - // possible if `forward_messages` takes a while) - drop(ack_ref); - - self.message_handler - .update_ack_delay(prepared.fragment_identifier, prepared.total_delay); - self.message_handler - .forward_messages(vec![prepared.into()], TransmissionLane::Retransmission) - .await; - } - Err(err) => { - let err = err.return_unused_surbs( - self.full_reply_storage.surbs_storage_ref(), - &recipient_tag, - ); - warn!("failed to prepare message for retransmission - {err}"); - // we buffer that packet and to try another day - self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); - - if self.should_request_more_surbs(&recipient_tag) { - self.request_reply_surbs_for_queue_clearing(recipient_tag) - .await; - } - } - }; - } else { - self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); - - if self.should_request_more_surbs(&recipient_tag) { - self.request_reply_surbs_for_queue_clearing(recipient_tag) - .await; - } - } - } - - // to be honest this doesn't make a lot of sense in the context of `connection_id`, - // it should really be asked per tag - fn handle_lane_queue_length( - &self, - connection_id: ConnectionId, - response_channel: oneshot::Sender, - ) { - // TODO: if we ever have duplicate ids for different senders, it means our rng is super weak - // thus I don't think we have to worry about it? - let lane = TransmissionLane::ConnectionId(connection_id); - for buf in self.pending_replies.values() { - if let Some(length) = buf.lane_length(&lane) { - if response_channel.send(length).is_err() { - error!("the requester for lane queue length has dropped the response channel!") - } - return; - } - } - // make sure that if we didn't find that lane, we reply with 0 - if response_channel.send(0).is_err() { - error!("the requester for lane queue length has dropped the response channel!") - } - } - +impl ReplyController +where + R: CryptoRng + Rng, +{ async fn handle_request(&mut self, request: ReplyControllerMessage) { match request { ReplyControllerMessage::RetransmitReply { @@ -702,7 +102,8 @@ where timed_out_ack, extra_surb_request, } => { - self.handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request) + self.receiver_controller + .handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request) .await } ReplyControllerMessage::SendReply { @@ -711,7 +112,8 @@ where lane, max_retransmissions, } => { - self.handle_send_reply(recipient, message, lane, max_retransmissions) + self.receiver_controller + .handle_send_reply(recipient, message, lane, max_retransmissions) .await } ReplyControllerMessage::AdditionalSurbs { @@ -719,225 +121,67 @@ where reply_surbs, from_surb_request, } => { - self.handle_received_surbs(sender_tag, reply_surbs, from_surb_request) + self.receiver_controller + .handle_received_surbs(sender_tag, reply_surbs, from_surb_request) .await } ReplyControllerMessage::LaneQueueLength { connection_id, response_channel, - } => self.handle_lane_queue_length(connection_id, response_channel), + } => self + .receiver_controller + .handle_lane_queue_length(connection_id, response_channel), ReplyControllerMessage::AdditionalSurbsRequest { recipient, amount } => { - self.handle_surb_request(*recipient, amount).await + self.sender_controller + .handle_surb_request(*recipient, amount) + .await } } } - // TODO: modify this method to more accurately determine the amount of surbs it needs to request - // it should take into consideration the average latency, sending rate and queue size. - // it should request as many surbs as it takes to saturate its sending rate before next batch arrives - async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) { - trace!("requesting surbs for queue clearing"); - - let pending_queue_size = self - .pending_replies - .get(&target) - .map(|pending_queue| pending_queue.total_size()) - .unwrap_or_default(); - - let retransmission_queue = self - .pending_retransmissions - .get(&target) - .map(|pending_queue| pending_queue.len()) - .unwrap_or_default(); - - let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32; - - let total_queue = (pending_queue_size + retransmission_queue) as u32; - - // To proactively request additional surbs, we aim to have a buffer of extra surbs in our - // storage. - let total_queue_with_buffer = total_queue + min_surbs_buffer; - - let request_size = min( - self.config.reply_surbs.maximum_reply_surb_request_size, - max( - total_queue_with_buffer, - self.config.reply_surbs.minimum_reply_surb_request_size, - ), - ); - - if let Err(err) = self - .request_additional_reply_surbs(target, request_size) - .await - { - info!("{err}") - } - } - - async fn inspect_stale_entries(&mut self) { - let mut to_request = Vec::new(); - let mut to_remove = Vec::new(); - - let now = OffsetDateTime::now_utc(); - for (pending_reply_target, vals) in &self.pending_replies { - if vals.is_empty() { - continue; - } - - let Some(last_received) = self - .full_reply_storage - .surbs_storage_ref() - .surbs_last_received_at(pending_reply_target) - else { - error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", vals.total_size()); - to_remove.push(*pending_reply_target); - continue; - }; - - // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually - // purge that malformed data - let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else { - error!("somehow our stored timestamp ({last_received}) for surbs from {pending_reply_target} is corrupted!. Going to remove all the associated entries"); - to_remove.push(*pending_reply_target); - continue; - }; - - let diff = now - last_received_time; - let max_rerequest_wait = self - .config - .reply_surbs - .maximum_reply_surb_rerequest_waiting_period; - let max_drop_wait = self - .config - .reply_surbs - .maximum_reply_surb_drop_waiting_period; - - if diff > max_rerequest_wait { - if diff > max_drop_wait { - to_remove.push(*pending_reply_target) - } else { - debug!("We haven't received any surbs in {:?} from {pending_reply_target}. Going to explicitly ask for more", diff); - to_request.push(*pending_reply_target); - } - } - } - - for pending_reply_target in to_request { - self.request_reply_surbs_for_queue_clearing(pending_reply_target) - .await; - self.full_reply_storage - .surbs_storage_ref() - .reset_pending_reception(&pending_reply_target) - } - for to_remove in to_remove { - self.pending_replies.remove(&to_remove); - } - } - - async fn invalidate_old_data(&self) { + async fn remove_stale_storage(&mut self) { let now = OffsetDateTime::now_utc(); - let mut to_remove_surbs = Vec::new(); - let mut to_remove_keys = Vec::new(); - for map_ref in self.full_reply_storage.surbs_storage_ref().as_raw_iter() { - let (sender, received) = map_ref.pair(); - // TODO: handle the following edge case: - // there's a malicious client sending us exactly one reply surb just before we should have invalidated - // the data thus making us keep everything in memory - // possible solution: keep timestamp PER reply surb (but that seems like an overkill) - // but I doubt this is ever going to be a problem... - // ... - // However, if you're reading this message, it probably became a legit problem, - // so I guess add timestamp per surb then? chop-chop. - - let last_received = received.surbs_last_received_at(); - // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually - // purge that malformed data - let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else { - error!("somehow our stored timestamp ({last_received}) for surbs from {sender} is corrupted!. Going to remove all the associated entries"); - to_remove_surbs.push(*sender); - continue; - }; - let diff = now - last_received_time; - - if diff > self.config.reply_surbs.maximum_reply_surb_age { - info!("it's been {diff:?} since we last received any reply surb from {sender}. Going to remove all stored entries..."); - - to_remove_surbs.push(*sender); - } - } - - for map_ref in self.full_reply_storage.key_storage_ref().as_raw_iter() { - let (digest, reply_key) = map_ref.pair(); - - // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually - // purge that malformed data - let Ok(sent_at) = OffsetDateTime::from_unix_timestamp(reply_key.sent_at_timestamp) - else { - error!("somehow our stored timestamp ({}) for one of our reply key is corrupted!. Going to remove all the entry", reply_key.sent_at_timestamp); - to_remove_keys.push(*digest); - continue; - }; - - let diff = now - sent_at; - - if diff > self.config.reply_surbs.maximum_reply_key_age { - debug!("it's been {diff:?} since we created this reply key. it's probably never going to get used, so we're going to purge it..."); - to_remove_keys.push(*digest); - } - } - - for to_remove in to_remove_surbs { - self.full_reply_storage - .surbs_storage_ref() - .remove(&to_remove); - } - - for to_remove in to_remove_keys { - self.full_reply_storage.key_storage().remove(to_remove) - } + self.receiver_controller + .inspect_and_clear_stale_data(now) + .await; + self.sender_controller.inspect_and_clear_stale_data(now) } - // #[cfg(not(target_arch = "wasm32"))] - // async fn log_status(&self) { - // todo!() - // } - pub(crate) async fn run(&mut self) { debug!("Started ReplyController with graceful shutdown support"); - let mut shutdown = self.task_client.fork("select"); + let mut shutdown = self.task_client.fork("reply-controller"); let polling_rate = Duration::from_secs(5); let mut stale_inspection = new_interval_stream(polling_rate); - // this is in the order of hours/days so we don't have to poll it that often - let polling_rate = - Duration::from_secs(self.config.reply_surbs.maximum_reply_surb_age.as_secs() / 10); + let polling_rate = self.config.key_rotation.epoch_duration / 8; let mut invalidation_inspection = new_interval_stream(polling_rate); while !shutdown.is_shutdown() { tokio::select! { biased; _ = shutdown.recv() => { - log::trace!("ReplyController: Received shutdown"); + tracing::trace!("ReplyController: Received shutdown"); }, req = self.request_receiver.next() => match req { Some(req) => self.handle_request(req).await, None => { - log::trace!("ReplyController: Stopping since channel closed"); + tracing::trace!("ReplyController: Stopping since channel closed"); break; } }, _ = stale_inspection.next() => { - self.inspect_stale_entries().await + self.receiver_controller.inspect_stale_pending_data().await }, _ = invalidation_inspection.next() => { - self.invalidate_old_data().await + self.receiver_controller.check_surb_refresh().await; + self.remove_stale_storage().await; } } } assert!(shutdown.is_shutdown_poll()); - log::debug!("ReplyController: Exiting"); + tracing::debug!("ReplyController: Exiting"); } } diff --git a/common/client-core/src/client/replies/reply_controller/receiver_controller.rs b/common/client-core/src/client/replies/reply_controller/receiver_controller.rs new file mode 100644 index 0000000000..11905c3a85 --- /dev/null +++ b/common/client-core/src/client/replies/reply_controller/receiver_controller.rs @@ -0,0 +1,901 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; +use crate::client::real_messages_control::message_handler::{ + FragmentWithMaxRetransmissions, MessageHandler, PreparationError, +}; +use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState; +use crate::client::replies::reply_controller::Config; +use crate::client::topology_control::TopologyAccessor; +use crate::client::transmission_buffer::TransmissionBuffer; +use futures::channel::oneshot; +use nym_client_core_surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap}; +use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng; +use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; +use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; +use nym_sphinx::chunking::fragment::FragmentIdentifier; +use nym_task::connections::{ConnectionId, TransmissionLane}; +use nym_topology::NymTopologyMetadata; +use rand::Rng; +use std::cmp::{max, min}; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, HashMap}; +use std::mem; +use std::sync::{Arc, Weak}; +use time::OffsetDateTime; +use tracing::{debug, error, info, trace, warn}; + +struct SenderData { + current_clear_rerequest_counter: usize, + pending_replies: TransmissionBuffer, + pending_retransmissions: BTreeMap>, + last_request_failure: OffsetDateTime, +} + +impl Default for SenderData { + fn default() -> Self { + SenderData { + current_clear_rerequest_counter: 0, + pending_replies: Default::default(), + pending_retransmissions: Default::default(), + last_request_failure: OffsetDateTime::UNIX_EPOCH, + } + } +} + +impl SenderData { + fn total_pending(&self) -> usize { + let pending_replies = self.pending_replies.total_size(); + let pending_retransmissions = self.pending_retransmissions.len(); + let total_pending = pending_retransmissions + pending_replies; + + debug!("total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}"); + + total_pending + } + + pub(crate) fn increment_current_clear_rerequest_counter(&mut self) { + self.current_clear_rerequest_counter += 1; + } + + pub(crate) fn reset_current_clear_rerequest_counter(&mut self) { + self.current_clear_rerequest_counter = 0; + } + + pub(crate) fn reset_last_request_failure(&mut self, now: OffsetDateTime) -> OffsetDateTime { + mem::replace(&mut self.last_request_failure, now) + } +} + +/// Reply controller responsible for controlling receiver-related part +/// of replies, such as requesting additional reply SURBs +pub struct ReceiverReplyController { + config: Config, + + surb_refresh_state: SurbRefreshState, + topology_access: TopologyAccessor, + + surb_senders: HashMap, + unavailable: HashMap, + surbs_storage: ReceivedReplySurbsMap, + + // TODO: incorporate that field at some point + // and use binomial distribution to determine the expected required number + // of surbs required to send the message through + // expected_reliability: f32, + message_handler: MessageHandler, +} + +impl ReceiverReplyController +where + R: CryptoRng + Rng, +{ + pub(crate) fn new( + config: Config, + storage: ReceivedReplySurbsMap, + message_handler: MessageHandler, + ) -> Self { + let topology_access = message_handler.topology_access_handle().clone(); + + ReceiverReplyController { + config, + surb_refresh_state: SurbRefreshState::WaitingForNextRotation { + last_known: config + .key_rotation + .expected_current_key_rotation_id(OffsetDateTime::now_utc()), + }, + topology_access, + surb_senders: Default::default(), + unavailable: Default::default(), + surbs_storage: storage, + message_handler, + } + } + + fn get_or_create_surb_sender(&mut self, tag: &AnonymousSenderTag) -> &mut SenderData { + self.surb_senders.entry(*tag).or_default() + } + + async fn current_topology_metadata(&self) -> Option { + self.topology_access.current_metadata().await + } + + fn insert_pending_replies>( + &mut self, + recipient: &AnonymousSenderTag, + fragments: I, + lane: TransmissionLane, + ) { + trace!("buffering pending replies for {recipient}"); + self.surb_senders + .entry(*recipient) + .or_default() + .pending_replies + .store(&lane, fragments) + } + + fn re_insert_pending_replies( + &mut self, + recipient: &AnonymousSenderTag, + fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>, + ) { + trace!("re-inserting pending replies for {recipient}"); + // the buffer should ALWAYS exist at this point, if it doesn't, it's a bug... + self.surb_senders + .entry(*recipient) + .or_default() + .pending_replies + .store_multiple(fragments) + } + + fn re_insert_pending_retransmission( + &mut self, + recipient: &AnonymousSenderTag, + data: Vec>, + ) { + trace!("re-inserting pending retransmissions for {recipient}"); + // SAFETY: the underlying entry MUST exist as we've just got data from there + // and we hold a mut reference + #[allow(clippy::expect_used)] + let map_entry = &mut self + .surb_senders + .get_mut(recipient) + .expect("our pending retransmission entry is somehow gone!") + .pending_retransmissions; + + for pending in data { + // if it's 0, we don't need to do anything - we just got that ack! + if Arc::strong_count(&pending) > 1 { + let id = pending.inner_fragment_identifier(); + let downgraded = Arc::downgrade(&pending); + map_entry.insert(id, downgraded); + } + } + } + + fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool { + trace!("checking if we should request more surbs from {target}"); + + let total_queue = self + .surb_senders + .get(target) + .map(|pending| pending.total_pending()) + .unwrap_or_default(); + + // only consider 'fresh' surbs + let available_surbs = self.surbs_storage.available_fresh_surbs(target); + let pending_surbs = self.surbs_storage.pending_reception(target) as usize; + let min_surbs_threshold = self.surbs_storage.min_surb_threshold(); + let max_surbs_threshold = self.surbs_storage.max_surb_threshold(); + let min_surbs_threshold_buffer = + self.config.reply_surbs.minimum_reply_surb_threshold_buffer; + + // After clearing the queue, we want to have at least `min_surbs_threshold` surbs available + // and reserved for requesting additional surbs, and in addition to that we also want to + // have `min_surbs_threshold_buffer` surbs available proactively. + let target_surbs_after_clearing_queue = min_surbs_threshold + min_surbs_threshold_buffer; + + // Check if we have enough surbs to handle the total queue and maintain minimum thresholds + let total_required_surbs = total_queue + target_surbs_after_clearing_queue; + let total_available_surbs = pending_surbs + available_surbs; + + debug!("available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}"); + + // We should request more surbs if: + // 1. We haven't hit the maximum surb threshold, and + // 2. We don't have enough surbs to handle the queue plus minimum thresholds + let is_below_max_threshold = total_available_surbs < max_surbs_threshold; + let is_below_required_surbs = total_available_surbs < total_required_surbs; + + is_below_max_threshold && is_below_required_surbs + } + + pub(crate) async fn handle_send_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + max_retransmissions: Option, + ) { + if !self.surbs_storage.contains_surbs_for(&recipient_tag) { + if self + .unavailable + .insert(recipient_tag, OffsetDateTime::now_utc()) + .is_none() + { + // don't report it every single time + warn!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"); + } else { + trace!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"); + } + return; + } + + trace!("handling reply to {recipient_tag}"); + let mut fragments = self.message_handler.split_reply_message(data); + let total_size = fragments.len(); + trace!("This reply requires {total_size} SURBs"); + + // for the purposes of sending reply, do allow using possibly stale entries + let available_surbs = self.surbs_storage.available_surbs(&recipient_tag); + let min_surbs_threshold = self.surbs_storage.min_surb_threshold(); + + let max_to_send = if available_surbs > min_surbs_threshold { + min(fragments.len(), available_surbs - min_surbs_threshold) + } else { + 0 + }; + + if max_to_send > 0 { + let (surbs, surbs_left) = self + .surbs_storage + .get_reply_surbs(&recipient_tag, max_to_send); + + debug!( + "retrieved {} reply surbs. {surbs_left} surbs remaining in storage", + surbs.as_ref().map(|s| s.len()).unwrap_or_default() + ); + if let Some(reply_surbs) = surbs { + let to_send = fragments + .drain(..reply_surbs.len()) + .map(|f| FragmentWithMaxRetransmissions { + fragment: f, + max_retransmissions, + }) + .collect::>(); + + if let Err(err) = self + .message_handler + .try_send_reply_chunks_on_lane( + recipient_tag, + to_send.clone(), + reply_surbs, + lane, + ) + .await + { + let err = err.return_unused_surbs(&self.surbs_storage, &recipient_tag); + warn!("failed to send reply to {recipient_tag}: {err}"); + info!( + "buffering {no_fragments} fragments for {recipient_tag}", + no_fragments = to_send.len() + ); + self.insert_pending_replies(&recipient_tag, to_send, lane); + } + } + } + + // if there's leftover data we didn't send because we didn't have enough (or any) surbs - buffer it + if !fragments.is_empty() { + // Ideally we should have enough surbs above the minimum threshold to handle sending + // new replies without having to first request more surbs. That's why I'd like to log + // these cases as they might indicate a problem with the surb management. + debug!( + "buffering {no_fragments} fragments for {recipient_tag}", + no_fragments = fragments.len() + ); + let fragments: Vec<_> = fragments + .into_iter() + .map(|fragment| FragmentWithMaxRetransmissions { + fragment, + max_retransmissions, + }) + .collect(); + self.insert_pending_replies(&recipient_tag, fragments, lane); + } + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + + async fn request_additional_reply_surbs( + &mut self, + target: AnonymousSenderTag, + amount: u32, + ) -> Result<(), PreparationError> { + debug!("requesting {amount} additional reply surbs for {target}"); + let (reply_surb, _) = self + .surbs_storage + .get_reply_surb_ignoring_threshold(&target); + + let reply_surb = reply_surb.ok_or(PreparationError::NotEnoughSurbs { + available: 0, + required: 1, + })?; + + if let Err(err) = self + .message_handler + .try_request_additional_reply_surbs(target, reply_surb, amount) + .await + { + let err = err.return_unused_surbs(&self.surbs_storage, &target); + warn!("failed to request additional surbs from {target}: {err}",); + return Err(err); + } else { + self.surbs_storage + .increment_pending_reception(&target, amount); + } + + Ok(()) + } + + async fn try_clear_pending_retransmission(&mut self, target: AnonymousSenderTag) { + trace!("trying to clear pending retransmission queue"); + let available_surbs = self.surbs_storage.available_surbs(&target); + let min_surbs_threshold = self.surbs_storage.min_surb_threshold(); + + let max_to_clear = if available_surbs > min_surbs_threshold { + available_surbs - min_surbs_threshold + } else { + trace!("we don't have enough surbs for retransmission queue clearing..."); + return; + }; + trace!("we can clear up to {max_to_clear} entries"); + + let Some(pending) = self.surb_senders.get_mut(&target) else { + trace!("no pending entry for {target}!"); + return; + }; + + let mut to_take = Vec::new(); + + while to_take.len() < max_to_clear { + if let Some((_, data)) = pending.pending_retransmissions.pop_first() { + // no need to do anything if we failed to upgrade the reference, + // it means we got the ack while the data was waiting in the queue + if let Some(upgraded) = data.upgrade() { + to_take.push(upgraded) + } + } else { + // our map is empty! + break; + } + } + + if to_take.is_empty() { + // no need to do anything + return; + } + + let (surbs_for_reply, _) = self.surbs_storage.get_reply_surbs(&target, to_take.len()); + + let Some(surbs_for_reply) = surbs_for_reply else { + error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + self.re_insert_pending_retransmission(&target, to_take); + return; + }; + + let to_send_vec = to_take.iter().map(|ack| ack.fragment_data()).collect(); + + let prepared_fragments = match self + .message_handler + .prepare_reply_chunks_for_sending(to_send_vec, surbs_for_reply) + .await + { + Ok(prepared) => prepared, + Err(err) => { + let err = err.return_unused_surbs(&self.surbs_storage, &target); + self.re_insert_pending_retransmission(&target, to_take); + + warn!("failed to clear pending retransmission queue for {target}: {err}",); + return; + } + }; + + // we can't fail at this point, so drop all references to acks so that timer updates wouldn't blow up + drop(to_take); + + self.message_handler + .send_retransmission_reply_chunks(prepared_fragments, TransmissionLane::Retransmission) + .await; + } + + fn pop_at_most_pending_replies( + &mut self, + from: &AnonymousSenderTag, + amount: usize, + ) -> Option> { + // if possible, pop all pending replies, if not, pop only entries for which we'd have a reply surb + let pending = self.surb_senders.get_mut(from)?; + let total = pending.pending_replies.total_size(); + trace!("pending queue has {total} elements"); + if total == 0 { + return None; + } + pending + .pending_replies + .pop_at_most_n_next_messages_at_random(amount) + } + + #[allow(clippy::panic)] + async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) { + trace!("trying to clear pending queue"); + let available_surbs = self.surbs_storage.available_surbs(&target); + let min_surbs_threshold = self.surbs_storage.min_surb_threshold(); + + let max_to_clear = if available_surbs > min_surbs_threshold { + available_surbs - min_surbs_threshold + } else { + trace!("we don't have enough surbs for queue clearing..."); + return; + }; + trace!("we can clear up to {max_to_clear} entries"); + + // we're guaranteed to not get more entries than we have reply surbs for + if let Some(to_send) = self.pop_at_most_pending_replies(&target, max_to_clear) { + let to_send_clone = to_send.clone(); + + if to_send_clone.is_empty() { + panic!( + "please let the devs know if you ever see this message (reply_controller.rs)" + ); + } + + let (surbs_for_reply, _) = self + .surbs_storage + .get_reply_surbs(&target, to_send_clone.len()); + + let Some(surbs_for_reply) = surbs_for_reply else { + error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + self.re_insert_pending_replies(&target, to_send); + return; + }; + + if let Err(err) = self + .message_handler + .try_send_reply_chunks(target, to_send_clone, surbs_for_reply) + .await + { + let err = err.return_unused_surbs(&self.surbs_storage, &target); + self.re_insert_pending_replies(&target, to_send); + warn!("failed to clear pending queue for {target}: {err}"); + } + } else { + trace!("the pending queue is empty"); + } + } + + fn reset_rerequest_counter(&mut self, from: &AnonymousSenderTag) { + if let Some(pending) = self.surb_senders.get_mut(from) { + pending.reset_current_clear_rerequest_counter() + } + } + + pub(crate) async fn handle_received_surbs( + &mut self, + from: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + ) { + trace!("handling received surbs"); + + // clear the requesting flag since we should have been asking for surbs + if from_surb_request { + self.surbs_storage + .decrement_pending_reception(&from, reply_surbs.len() as u32); + } + + // store received surbs + self.surbs_storage.insert_fresh_surbs(&from, reply_surbs); + + // reset, if applicable, request counter + self.reset_rerequest_counter(&from); + + // use as many as we can for clearing pending retransmission queue + self.try_clear_pending_retransmission(from).await; + + // use as many as we can for clearing pending 'normal' queue + self.try_clear_pending_queue(from).await; + + // if we have to, request more + if self.should_request_more_surbs(&from) { + self.request_reply_surbs_for_queue_clearing(from).await; + } + } + fn buffer_pending_ack( + &mut self, + recipient: AnonymousSenderTag, + ack_ref: Arc, + weak_ack_ref: Weak, + ) { + let frag_id = ack_ref.inner_fragment_identifier(); + + let pending = self.surb_senders.entry(recipient).or_default(); + if let Entry::Vacant(e) = pending.pending_retransmissions.entry(frag_id) { + e.insert(weak_ack_ref); + } else { + warn!( + "we're already trying to retransmit {frag_id}. We must be really behind in surbs!" + ); + } + } + + pub(crate) async fn handle_reply_retransmission( + &mut self, + recipient_tag: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surbs_request: bool, + ) { + // seems we got the ack in the end + let ack_ref = match timed_out_ack.upgrade() { + Some(ack) => ack, + None => { + debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue"); + return; + } + }; + + // if this is retransmission for obtaining additional reply surbs, + // we can dip below the storage threshold + let (maybe_reply_surb, _) = if extra_surbs_request { + self.surbs_storage + .get_reply_surb_ignoring_threshold(&recipient_tag) + } else { + self.surbs_storage.get_reply_surb(&recipient_tag) + }; + + if let Some(reply_surb) = maybe_reply_surb { + match self + .message_handler + .try_prepare_single_reply_chunk_for_sending(reply_surb, ack_ref.fragment_data()) + .await + { + Ok(prepared) => { + // drop the ack ref so that controller would not panic on `UpdateTimer` if that task + // got to handle the action before this function terminated (which is very much + // possible if `forward_messages` takes a while) + drop(ack_ref); + + self.message_handler + .update_ack_delay(prepared.fragment_identifier, prepared.total_delay); + self.message_handler + .forward_messages(vec![prepared.into()], TransmissionLane::Retransmission) + .await; + } + Err(err) => { + let err = err.return_unused_surbs(&self.surbs_storage, &recipient_tag); + warn!("failed to prepare message for retransmission - {err}"); + // we buffer that packet and to try another day + self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + }; + } else { + self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + } + + // to be honest this doesn't make a lot of sense in the context of `connection_id`, + // it should really be asked per tag + pub(crate) fn handle_lane_queue_length( + &self, + connection_id: ConnectionId, + response_channel: oneshot::Sender, + ) { + // TODO: if we ever have duplicate ids for different senders, it means our rng is super weak + // thus I don't think we have to worry about it? + let lane = TransmissionLane::ConnectionId(connection_id); + for buf in self.surb_senders.values().map(|p| &p.pending_replies) { + if let Some(length) = buf.lane_length(&lane) { + if response_channel.send(length).is_err() { + error!("the requester for lane queue length has dropped the response channel!") + } + return; + } + } + // make sure that if we didn't find that lane, we reply with 0 + if response_channel.send(0).is_err() { + error!("the requester for lane queue length has dropped the response channel!") + } + } + + // TODO: modify this method to more accurately determine the amount of surbs it needs to request + // it should take into consideration the average latency, sending rate and queue size. + // it should request as many surbs as it takes to saturate its sending rate before next batch arrives + async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) { + trace!("requesting surbs for queue clearing"); + + let total_queue = self + .surb_senders + .get(&target) + .map(|pending| pending.total_pending() as u32) + .unwrap_or_default(); + + let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32; + + // To proactively request additional surbs, we aim to have a buffer of extra surbs in our + // storage. + let total_queue_with_buffer = total_queue + min_surbs_buffer; + + let request_size = min( + self.config.reply_surbs.maximum_reply_surb_request_size, + max( + total_queue_with_buffer, + self.config.reply_surbs.minimum_reply_surb_request_size, + ), + ); + + if let Err(err) = self + .request_additional_reply_surbs(target, request_size) + .await + { + let now = OffsetDateTime::now_utc(); + let sender_info = self.get_or_create_surb_sender(&target); + let last_failure = sender_info.reset_last_request_failure(now); + + // only log at higher level if it's the first time this error has occurred in a while + if now - last_failure > time::Duration::seconds(30) { + warn!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}") + } else { + debug!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}") + } + } + } + + pub(crate) async fn inspect_stale_pending_data(&mut self) { + let mut to_request = Vec::new(); + let mut to_remove = Vec::new(); + + let now = OffsetDateTime::now_utc(); + for (pending_reply_target, vals) in self.surb_senders.iter_mut() { + // for now recreate old behaviour + let retransmission_buf = &vals.pending_replies; + + if retransmission_buf.is_empty() { + continue; + } + + let Some(last_received_time) = self + .surbs_storage + .surbs_last_received_at(pending_reply_target) + else { + error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", retransmission_buf.total_size()); + to_remove.push(*pending_reply_target); + continue; + }; + + let diff = now - last_received_time; + let max_rerequest_wait = self + .config + .reply_surbs + .maximum_reply_surb_rerequest_waiting_period; + let max_drop_wait = self + .config + .reply_surbs + .maximum_reply_surb_drop_waiting_period; + let max_rerequests = self.config.reply_surbs.maximum_reply_surbs_rerequests; + + // if we have already requested extra surbs because of the stale entry, + // don't do it again (otherwise we'll get stuck in a constant cycle of requesting more surbs + // if client is offline) + if vals.current_clear_rerequest_counter > max_rerequests { + to_remove.push(*pending_reply_target); + debug!("we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender"); + continue; + } + + if diff > max_rerequest_wait { + if diff > max_drop_wait { + to_remove.push(*pending_reply_target) + } else { + debug!("We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more", humantime::format_duration(diff.unsigned_abs())); + vals.increment_current_clear_rerequest_counter(); + to_request.push(*pending_reply_target); + } + } + } + + for pending_reply_target in to_request { + self.request_reply_surbs_for_queue_clearing(pending_reply_target) + .await; + self.surbs_storage + .reset_pending_reception(&pending_reply_target) + } + for to_remove in to_remove { + // TODO: in the 'old' version we just removed pending messages, + // not retransmissions, but I think those should follow the same logic. + // if something breaks because of that. I guess here is your explanation, future reader + self.surb_senders.remove(&to_remove); + } + } + + pub(crate) async fn check_surb_refresh(&mut self) { + let Some(current_rotation_id) = self.topology_access.current_key_rotation_id().await else { + warn!("failed to retrieve current key rotation id from the network topology"); + return; + }; + + if let SurbRefreshState::WaitingForNextRotation { last_known } = self.surb_refresh_state { + if last_known == current_rotation_id { + trace!("no changes in key rotation id"); + } else { + // key rotation actually changed and given the polling rate (1/8th epoch) we should have plenty + // of time to perform the upgrade. + // but wait for one more call before doing this so that the clients could also resync + // their topologies and discover new rotation + self.surb_refresh_state = SurbRefreshState::ScheduledForNextInvocation; + } + return; + } + + // here we are in `SurbRefreshState::ScheduledForNextInvocation` state + + let mut marked_as_stale = HashMap::new(); + + // 1. mark all existing surbs we have as possibly stale + for mut map_entry in self.surbs_storage.as_raw_iter_mut() { + let (sender, received) = map_entry.pair_mut(); + let num_downgraded = received.downgrade_freshness(); + trace!("{sender}: {num_downgraded} downgraded"); + if num_downgraded != 0 { + marked_as_stale.insert(*sender, num_downgraded); + } + } + + // 2. attempt to re-request the equivalent number of fresh surbs + // TODO PROBLEM: if our request gets lost, we might be in trouble... + // we need some sort of retry mechanism + for (sender, num_to_request) in marked_as_stale { + if self + .request_additional_reply_surbs(sender, num_to_request as u32) + .await + .is_err() + { + warn!("surb refresh request failed") + } + } + + self.surb_refresh_state = SurbRefreshState::WaitingForNextRotation { + last_known: current_rotation_id, + }; + } + + pub(crate) async fn inspect_and_clear_stale_data(&mut self, now: OffsetDateTime) { + // technically we don't know if epoch is stuck, but we're flying in blind here, + // so we have to assume the worst and not purge anything depending on proper epoch progression + let is_epoch_stuck = self + .current_topology_metadata() + .await + .map(|m| self.config.key_rotation.epoch_stuck(m)) + .unwrap_or(false); + + // expected time of when the CURRENT key rotation has begun + let expected_current_key_rotation_start = self + .config + .key_rotation + .expected_current_key_rotation_start(now); + + // expected ID of the CURRENT key rotation + let expected_current_key_rotation = self + .config + .key_rotation + .expected_current_key_rotation_id(now); + + // time of the start of one epoch BEFORE the CURRENT rotation has begun + // this indicates the starting time of when packets with the current keys might have been constructed + let prior_epoch_start = + expected_current_key_rotation_start - self.config.key_rotation.epoch_duration; + + // time of the start of one epoch AFTER the current rotation has begun + // this indicates the end of transition period and any packets constructed with keys different + // from the current one are definitely invalid + let following_epoch_start = + expected_current_key_rotation_start + self.config.key_rotation.epoch_duration; + + // define a closure for validating individual surbs + // (we have to run it twice for different piles) + let basic_surb_retention_logic = |received_surb: &ReceivedReplySurb| { + if is_epoch_stuck { + let diff = now - received_surb.received_at(); + return diff < self.config.key_rotation.rotation_lifetime(); + } + + if received_surb.received_at() < prior_epoch_start { + // it's definitely from previous rotation + return false; + } + let surb_rotation = received_surb.key_rotation(); + + if surb_rotation.is_unknown() { + // can't do anything, so just retain it + return true; + } + + // TODO: will this backfire during transition period where we need surbs to refresh surbs + // and we failed to send a request? + if surb_rotation.is_even() && expected_current_key_rotation % 2 == 1 { + return false; + } + + if surb_rotation.is_odd() && expected_current_key_rotation % 2 == 0 { + return false; + } + + true + }; + + // 1. purge full old clients data (this applies to RECEIVER) + self.surbs_storage.retain(|_, received| { + if is_epoch_stuck { + // if epoch is stuck, we can't do much (because we don't know for certain if rotation has advanced) + // apart from the basic check of surbs being received more than maximum lifetime of a rotation + // because at that point we know they must be invalid + let diff = now - received.surbs_last_received_at(); + return diff < self.config.key_rotation.rotation_lifetime(); + } + + // if surbs were received more than 1h before the start of the current rotation, + // they're DEFINITELY invalid. + // if it was up until 1h AFTER the start of the current rotation they MIGHT be valid - + // we don't know for sure, unless the client explicitly attached rotation information + // (which only applies to more recent versions of clients so we can't 100% rely on that) + if received.surbs_last_received_at() < prior_epoch_start { + return false; + } + + // 1.1. check individual surbs (same basic logic applies) + received.retain_fresh_surbs(&basic_surb_retention_logic); + + // 1.2. check the possibly stale entries + // 1.2.1. check if we're beyond the key rotation transition period, + // if so those surbs are definitely unusable + if now > following_epoch_start { + received.drop_possibly_stale_surbs(); + } + + // 1.2.2. otherwise continue with the same logic as the fresh ones + received.retain_possibly_stale_surbs(&basic_surb_retention_logic); + + // no surbs left, we're not expecting any AND we haven't received anything in a while + // (i.e. sender probably abandoned us) + let max_drop_wait = self + .config + .reply_surbs + .maximum_reply_surb_drop_waiting_period; + let last_received = received.surbs_last_received_at(); + + let possibly_abandoned = last_received + max_drop_wait < now; + if received.is_empty() && received.pending_reception() == 0 && possibly_abandoned { + return false; + } + + true + }); + + // 1.3 inspect old unavailable receivers to clear any stale data + self.unavailable + .retain(|_, last_reported| now - *last_reported < time::Duration::seconds(30)); + } +} diff --git a/common/client-core/src/client/replies/reply_controller/requests.rs b/common/client-core/src/client/replies/reply_controller/requests.rs index 30afc2585a..d4a7014065 100644 --- a/common/client-core/src/client/replies/reply_controller/requests.rs +++ b/common/client-core/src/client/replies/reply_controller/requests.rs @@ -3,12 +3,12 @@ use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; use futures::channel::{mpsc, oneshot}; -use log::error; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use nym_sphinx::anonymous_replies::ReplySurb; +use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; use nym_task::connections::{ConnectionId, TransmissionLane}; use std::sync::Weak; +use tracing::error; pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) { let (tx, rx) = mpsc::unbounded(); @@ -81,7 +81,7 @@ impl ReplyControllerSender { pub(crate) fn send_additional_surbs( &self, sender_tag: AnonymousSenderTag, - reply_surbs: Vec, + reply_surbs: Vec, from_surb_request: bool, ) -> Result<(), ReplyControllerSenderError> { self.0 @@ -167,7 +167,7 @@ pub enum ReplyControllerMessage { AdditionalSurbs { sender_tag: AnonymousSenderTag, - reply_surbs: Vec, + reply_surbs: Vec, from_surb_request: bool, }, diff --git a/common/client-core/src/client/replies/reply_controller/sender_controller.rs b/common/client-core/src/client/replies/reply_controller/sender_controller.rs new file mode 100644 index 0000000000..4d5aac999b --- /dev/null +++ b/common/client-core/src/client/replies/reply_controller/sender_controller.rs @@ -0,0 +1,101 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::Config; +use nym_client_core_surb_storage::{CombinedReplyStorage, SentReplyKeys, UsedSenderTags}; +use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng; +use nym_sphinx::addressing::Recipient; +use rand::Rng; +use std::cmp::min; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::{debug, trace, warn}; + +/// Reply controller responsible for controlling sender-related part +/// of replies, such as checking if any reply keys are stale +pub struct SenderReplyController { + config: Config, + + tags_storage: UsedSenderTags, + sent_reply_keys: SentReplyKeys, + message_handler: MessageHandler, +} + +impl SenderReplyController +where + R: CryptoRng + Rng, +{ + pub(crate) fn new( + config: Config, + storage: &CombinedReplyStorage, + message_handler: MessageHandler, + ) -> Self { + SenderReplyController { + config, + tags_storage: storage.tags_storage(), + sent_reply_keys: storage.key_storage(), + message_handler, + } + } + + pub(crate) async fn handle_surb_request(&mut self, recipient: Recipient, mut amount: u32) { + // 1. check whether we sent any surbs in the past to this recipient, otherwise + // they have no business in asking for more + if !self.tags_storage.exists(&recipient) { + warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"); + return; + } + + // 2. check whether the requested amount is within sane range + if amount + > self + .config + .reply_surbs + .maximum_allowed_reply_surb_request_size + { + warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.reply_surbs.maximum_allowed_reply_surb_request_size); + amount = self + .config + .reply_surbs + .maximum_allowed_reply_surb_request_size; + } + + // 3. construct and send the surbs away + // (send them in smaller batches to make the experience a bit smoother + let mut remaining = amount; + while remaining > 0 { + let to_send = min(remaining, 100); + if let Err(err) = self + .message_handler + .try_send_additional_reply_surbs( + recipient, + to_send, + nym_sphinx::params::PacketType::Mix, + ) + .await + { + warn!("failed to send additional surbs to {recipient} - {err}"); + } else { + trace!("sent {to_send} reply SURBs to {recipient}"); + } + + remaining -= to_send; + } + } + + pub(crate) fn inspect_and_clear_stale_data(&self, now: OffsetDateTime) { + // check reply keys (this applies to SENDER) + self.sent_reply_keys.retain(|_, reply_key| { + let diff = now - reply_key.sent_at; + if diff > self.config.reply_surbs.maximum_reply_key_age { + let std_diff = Duration::try_from(diff).unwrap_or_default(); + let diff_formatted = humantime::format_duration(std_diff); + debug!("it's been {diff_formatted} since we created this reply key. it's probably never going to get used, so we're going to purge it..."); + false + } else { + true + } + }); + } +} diff --git a/common/client-core/src/client/statistics_control.rs b/common/client-core/src/client/statistics_control.rs index 01f4d25f1b..6d315f4eb2 100644 --- a/common/client-core/src/client/statistics_control.rs +++ b/common/client-core/src/client/statistics_control.rs @@ -93,14 +93,14 @@ impl StatisticsControl { None, ); if let Err(err) = self.report_tx.send(report_message).await { - log::error!("Failed to report client stats: {:?}", err); + tracing::error!("Failed to report client stats: {err:?}"); } else { self.stats.reset(); } } async fn run(&mut self) { - log::debug!("Started StatisticsControl with graceful shutdown support"); + tracing::debug!("Started StatisticsControl with graceful shutdown support"); #[cfg(not(target_arch = "wasm32"))] let mut stats_report_interval = tokio_stream::wrappers::IntervalStream::new( @@ -133,13 +133,13 @@ impl StatisticsControl { tokio::select! { biased; _ = self.task_client.recv() => { - log::trace!("StatisticsControl: Received shutdown"); + tracing::trace!("StatisticsControl: Received shutdown"); break; }, stats_event = self.stats_rx.recv() => match stats_event { Some(stats_event) => self.stats.handle_event(stats_event), None => { - log::trace!("StatisticsControl: shutting down due to closed stats channel"); + tracing::trace!("StatisticsControl: shutting down due to closed stats channel"); break; } }, @@ -161,13 +161,16 @@ impl StatisticsControl { } } } - log::debug!("StatisticsControl: Exiting"); + tracing::debug!("StatisticsControl: Exiting"); } pub(crate) fn start(mut self) { - spawn_future(async move { - self.run().await; - }) + spawn_future!( + async move { + self.run().await; + }, + "StatisticsControl" + ) } pub(crate) fn create_and_start( diff --git a/common/client-core/src/client/topology_control/accessor.rs b/common/client-core/src/client/topology_control/accessor.rs index b1f74c4f24..9841127b52 100644 --- a/common/client-core/src/client/topology_control/accessor.rs +++ b/common/client-core/src/client/topology_control/accessor.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use nym_sphinx::addressing::clients::Recipient; -use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError}; +use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError, NymTopologyMetadata}; +use nym_validator_client::models::KeyRotationId; use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -125,7 +126,7 @@ impl TopologyAccessor { .map(|p| p.topology.clone()) } - pub async fn current_route_provider(&self) -> Option> { + pub async fn current_route_provider(&self) -> Option> { let provider = self.inner.topology.read().await; if provider.topology.is_empty() { None @@ -134,6 +135,21 @@ impl TopologyAccessor { } } + pub async fn current_mixnet_epoch_id(&self) -> Option { + let route_provider = self.current_route_provider().await?; + Some(route_provider.absolute_epoch_id()) + } + + pub async fn current_key_rotation_id(&self) -> Option { + let route_provider = self.current_route_provider().await?; + Some(route_provider.current_key_rotation()) + } + + pub async fn current_metadata(&self) -> Option { + let route_provider = self.current_route_provider().await?; + Some(route_provider.metadata()) + } + pub async fn manually_change_topology(&self, new_topology: NymTopology) { self.inner.controlled_manually.store(true, Ordering::SeqCst); self.inner.update(Some(new_topology)).await; diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index 3e36babf8c..b3c4d61f3f 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -4,11 +4,11 @@ use crate::spawn_future; pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit}; use futures::StreamExt; -use log::*; use nym_sphinx::addressing::nodes::NodeIdentity; use nym_task::TaskClient; use nym_topology::NymTopologyError; use std::time::Duration; +use tracing::*; #[cfg(not(target_arch = "wasm32"))] use tokio::time::sleep; @@ -20,7 +20,7 @@ mod accessor; pub mod nym_api_provider; pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider}; -pub use nym_topology::provider_trait::TopologyProvider; +pub use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; // TODO: move it to config later const MAX_FAILURE_COUNT: usize = 10; @@ -145,30 +145,39 @@ impl TopologyRefresher { } pub fn start(mut self) { - spawn_future(async move { - debug!("Started TopologyRefresher with graceful shutdown support"); + spawn_future!( + async move { + debug!("Started TopologyRefresher with graceful shutdown support"); - #[cfg(not(target_arch = "wasm32"))] - let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval( - self.refresh_rate, - )); + #[cfg(not(target_arch = "wasm32"))] + let mut interval = tokio_stream::wrappers::IntervalStream::new( + tokio::time::interval(self.refresh_rate), + ); - #[cfg(target_arch = "wasm32")] - let mut interval = - gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); + #[cfg(target_arch = "wasm32")] + let mut interval = + gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); - while !self.task_client.is_shutdown() { - tokio::select! { - _ = interval.next() => { - self.try_refresh().await; - }, - _ = self.task_client.recv() => { - log::trace!("TopologyRefresher: Received shutdown"); - }, + // We already have an initial topology, so no need to refresh it immediately. + // My understanding is that js setInterval does not fire immediately, so it's not + // needed there. + #[cfg(not(target_arch = "wasm32"))] + interval.next().await; + + while !self.task_client.is_shutdown() { + tokio::select! { + _ = interval.next() => { + self.try_refresh().await; + }, + _ = self.task_client.recv() => { + tracing::trace!("TopologyRefresher: Received shutdown"); + }, + } } - } - self.task_client.recv_timeout().await; - log::debug!("TopologyRefresher: Exiting"); - }) + self.task_client.recv_timeout().await; + tracing::debug!("TopologyRefresher: Exiting"); + }, + "TopologyRefresher" + ) } } diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 30d2461abd..339cd12653 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -use log::{debug, error, warn}; -use nym_topology::provider_trait::TopologyProvider; +use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; use nym_topology::NymTopology; -use nym_validator_client::UserAgent; use rand::prelude::SliceRandom; use rand::thread_rng; use std::cmp::min; +use tracing::{debug, error, warn}; use url::Url; #[derive(Debug)] @@ -49,18 +48,10 @@ impl NymApiTopologyProvider { pub fn new( config: impl Into, mut nym_api_urls: Vec, - user_agent: Option, + mut validator_client: nym_validator_client::client::NymApiClient, ) -> Self { nym_api_urls.shuffle(&mut thread_rng()); - - let validator_client = if let Some(user_agent) = user_agent { - nym_validator_client::client::NymApiClient::new_with_user_agent( - nym_api_urls[0].clone(), - user_agent, - ) - } else { - nym_validator_client::client::NymApiClient::new(nym_api_urls[0].clone()) - }; + validator_client.change_nym_api(nym_api_urls[0].clone()); NymApiTopologyProvider { config: config.into(), @@ -70,6 +61,10 @@ impl NymApiTopologyProvider { } } + pub fn disable_bincode(&mut self) { + self.validator_client.use_bincode = false; + } + fn use_next_nym_api(&mut self) { if self.nym_api_urls.len() == 1 { warn!("There's only a single nym API available - it won't be possible to use a different one"); @@ -82,47 +77,58 @@ impl NymApiTopologyProvider { } async fn get_current_compatible_topology(&mut self) -> Option { - let rewarded_set = self - .validator_client - .get_current_rewarded_set() - .await - .inspect_err(|err| error!("failed to get current rewarded set: {err}")) - .ok()?; + let rewarded_set_fut = self.validator_client.get_current_rewarded_set(); - let mut topology = NymTopology::new_empty(rewarded_set); + let topology = if self.config.use_extended_topology { + let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata(); - if self.config.use_extended_topology { - let all_nodes = self - .validator_client - .get_all_basic_nodes() - .await + // Join rewarded_set_fut and all_nodes_fut concurrently + let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut) .inspect_err(|err| error!("failed to get network nodes: {err}")) .ok()?; + let metadata = all_nodes_res.metadata; + let all_nodes = all_nodes_res.nodes; + debug!( "there are {} nodes on the network (before filtering)", all_nodes.len() ); - topology.add_additional_nodes(all_nodes.iter().filter(|n| { - n.performance.round_to_integer() >= self.config.min_node_performance() - })); + let nodes_filtered = all_nodes + .into_iter() + .filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance()) + .collect::>(); + + NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) + .with_skimmed_nodes(&nodes_filtered) } else { // if we're not using extended topology, we're only getting active set mixnodes and gateways - let mixnodes = self + let mixnodes_fut = self .validator_client - .get_all_basic_active_mixing_assigned_nodes() - .await - .inspect_err(|err| error!("failed to get network mixnodes: {err}")) - .ok()?; + .get_all_basic_active_mixing_assigned_nodes_with_metadata(); // TODO: we really should be getting ACTIVE gateways only - let gateways = self + let gateways_fut = self .validator_client - .get_all_basic_entry_assigned_nodes() - .await - .inspect_err(|err| error!("failed to get network gateways: {err}")) - .ok()?; + .get_all_basic_entry_assigned_nodes_with_metadata(); + + let (rewarded_set, mixnodes_res, gateways_res) = + futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut) + .inspect_err(|err| { + error!("failed to get network nodes: {err}"); + }) + .ok()?; + + let metadata = mixnodes_res.metadata; + let mixnodes = mixnodes_res.nodes; + + if !gateways_res.metadata.consistency_check(&metadata) { + warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata); + return None; + } + + let gateways = gateways_res.nodes; debug!( "there are {} mixnodes and {} gateways in total (before performance filtering)", @@ -130,12 +136,20 @@ impl NymApiTopologyProvider { gateways.len() ); - topology.add_additional_nodes(mixnodes.iter().filter(|m| { - m.performance.round_to_integer() >= self.config.min_mixnode_performance - })); - topology.add_additional_nodes(gateways.iter().filter(|m| { - m.performance.round_to_integer() >= self.config.min_gateway_performance - })); + let mut nodes = Vec::new(); + for mix in mixnodes { + if mix.performance.round_to_integer() >= self.config.min_mixnode_performance { + nodes.push(mix) + } + } + for gateway in gateways { + if gateway.performance.round_to_integer() >= self.config.min_gateway_performance { + nodes.push(gateway) + } + } + + NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) + .with_skimmed_nodes(&nodes) }; if !topology.is_minimally_routable() { diff --git a/common/client-core/src/client/transmission_buffer.rs b/common/client-core/src/client/transmission_buffer.rs index e6eb7d6891..ee15210d14 100644 --- a/common/client-core/src/client/transmission_buffer.rs +++ b/common/client-core/src/client/transmission_buffer.rs @@ -36,11 +36,18 @@ impl SizedData for Fragment { } } -#[derive(Default)] pub(crate) struct TransmissionBuffer { buffer: HashMap>, } +impl Default for TransmissionBuffer { + fn default() -> Self { + TransmissionBuffer { + buffer: HashMap::new(), + } + } +} + impl TransmissionBuffer { pub(crate) fn new() -> Self { TransmissionBuffer { @@ -211,7 +218,7 @@ impl TransmissionBuffer { }; let msg = self.pop_front_from_lane(&lane)?; - log::trace!("picking to send from lane: {:?}", lane); + tracing::trace!("picking to send from lane: {lane:?}"); Some((lane, msg)) } diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index 69c30c69ae..0705a93c3a 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -4,6 +4,6 @@ pub use nym_client_core_config_types::disk_persistence; pub use nym_client_core_config_types::old::{ old_config_v1_1_13, old_config_v1_1_20, old_config_v1_1_20_2, old_config_v1_1_30, - old_config_v1_1_33, + old_config_v1_1_33, old_config_v1_1_54, }; pub use nym_client_core_config_types::*; diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 0cf2a88b6b..1c26e96dfb 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -6,7 +6,10 @@ use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; use nym_topology::node::RoutingNodeError; use nym_topology::{NodeId, NymTopologyError}; +use nym_validator_client::nym_api::error::NymAPIError; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::ValidatorClientError; +use rand::distributions::WeightedError; use std::error::Error; use std::path::PathBuf; @@ -18,7 +21,7 @@ pub enum ClientCoreError { #[error("gateway client error ({gateway_id}): {source}")] GatewayClientError { gateway_id: String, - source: GatewayClientError, + source: Box, }, #[error("custom gateway client error: {source}")] @@ -52,7 +55,15 @@ pub enum ClientCoreError { #[error("list of nym apis is empty")] ListOfNymApisIsEmpty, - #[error("the current network topology seem to be insufficient to route any packets through")] + #[error("failed to resolve a query to nym API: {source}")] + NymApiQueryFailure { + #[from] + source: NymAPIError, + }, + + #[error( + "the current network topology seem to be insufficient to route any packets through:\n\t{0}" + )] InsufficientNetworkTopology(#[from] NymTopologyError), #[error("experienced a failure with our reply surb persistent storage: {source}")] @@ -88,10 +99,7 @@ pub enum ClientCoreError { }, #[error("failed to establish connection to gateway: {source}")] - GatewayConnectionFailure { - #[from] - source: tungstenite::Error, - }, + GatewayConnectionFailure { source: Box }, #[cfg(target_arch = "wasm32")] #[error("failed to establish gateway connection (wasm)")] @@ -224,7 +232,27 @@ pub enum ClientCoreError { UnexpectedKeyUpgrade { gateway_id: String }, #[error("failed to derive keys from master key")] - HkdfDerivationError {}, + HkdfDerivationError, + + #[error("missing url for constructing RPC client")] + RpcClientMissingUrl, + + #[error("provided nym network details were malformed: {source}")] + InvalidNetworkDetails { source: NyxdError }, + + #[error("failed to construct RPC client: {source}")] + RpcClientCreationFailure { source: NyxdError }, + + #[error("failed to select valid gateway due to incomputable latency")] + GatewaySelectionFailure { source: WeightedError }, +} + +impl From for ClientCoreError { + fn from(err: tungstenite::Error) -> ClientCoreError { + ClientCoreError::GatewayConnectionFailure { + source: Box::new(err), + } + } } /// Set of messages that the client can send to listeners via the task manager diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index b007243ce6..1ca4b68191 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -4,7 +4,6 @@ use crate::error::ClientCoreError; use crate::init::types::RegistrationResult; use futures::{SinkExt, StreamExt}; -use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::GatewayClient; use nym_topology::node::RoutingNode; @@ -14,6 +13,7 @@ use rand::{seq::SliceRandom, Rng}; #[cfg(unix)] use std::os::fd::RawFd; use std::{sync::Arc, time::Duration}; +use tracing::{debug, info, trace, warn}; use tungstenite::Message; use url::Url; @@ -105,12 +105,15 @@ pub async fn gateways_for_init( nym_validator_client::client::NymApiClient::new(nym_api.clone()) }; - log::debug!("Fetching list of gateways from: {nym_api}"); + tracing::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_all_basic_entry_assigned_nodes().await?; + let gateways = client + .get_all_basic_entry_assigned_nodes_with_metadata() + .await? + .nodes; info!("nym api reports {} gateways", gateways.len()); - log::trace!("Gateways: {:#?}", gateways); + tracing::trace!("Gateways: {gateways:#?}"); // filter out gateways below minimum performance and ones that could operate as a mixnode // (we don't want instability) @@ -120,10 +123,10 @@ pub async fn gateways_for_init( .filter(|g| g.performance.round_to_integer() >= minimum_performance) .filter_map(|gateway| gateway.try_into().ok()) .collect::>(); - log::debug!("After checking validity: {}", valid_gateways.len()); - log::trace!("Valid gateways: {:#?}", valid_gateways); + tracing::debug!("After checking validity: {}", valid_gateways.len()); + tracing::trace!("Valid gateways: {valid_gateways:#?}"); - log::info!( + tracing::info!( "and {} after validity and performance filtering", valid_gateways.len() ); @@ -145,7 +148,7 @@ async fn connect(endpoint: &str) -> Result { JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure) } -async fn measure_latency(gateway: &G) -> Result, ClientCoreError> +async fn measure_latency(gateway: &G) -> Result, ClientCoreError> where G: ConnectableGateway, { @@ -242,7 +245,7 @@ pub async fn choose_gateway_by_latency( let gateways_with_latency = gateways_with_latency.lock().await; let chosen = gateways_with_latency .choose_weighted(rng, |item| 1. / item.latency.as_secs_f32()) - .expect("invalid selection weight!"); + .map_err(|source| ClientCoreError::GatewaySelectionFailure { source })?; info!( "chose gateway {} with average latency of {:?}", @@ -286,7 +289,7 @@ pub(super) fn get_specified_gateway( gateways: &[RoutingNode], must_use_tls: bool, ) -> Result { - log::debug!("Requesting specified gateway: {}", gateway_identity); + tracing::debug!("Requesting specified gateway: {gateway_identity}"); let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; @@ -326,20 +329,20 @@ pub(super) async fn register_with_gateway( ); gateway_client.establish_connection().await.map_err(|err| { - log::warn!("Failed to establish connection with gateway!"); + tracing::warn!("Failed to establish connection with gateway!"); ClientCoreError::GatewayClientError { gateway_id: gateway_id.to_base58_string(), - source: err, + source: Box::new(err), } })?; let auth_response = gateway_client .perform_initial_authentication() .await .map_err(|err| { - log::warn!("Failed to register with the gateway {gateway_id}: {err}"); + tracing::warn!("Failed to register with the gateway {gateway_id}: {err}"); ClientCoreError::GatewayClientError { gateway_id: gateway_id.to_base58_string(), - source: err, + source: Box::new(err), } })?; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 105fd0a457..5bece573fd 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -63,7 +63,7 @@ where K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, { - log::trace!("Setting up new gateway"); + tracing::trace!("Setting up new gateway"); // if we're setting up new gateway, we must have had generated long-term client keys before let client_keys = load_client_keys(key_store).await?; @@ -202,10 +202,10 @@ where K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, { - log::debug!("Setting up gateway"); + tracing::debug!("Setting up gateway"); match setup { GatewaySetup::MustLoad { gateway_id } => { - log::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}"); + tracing::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}"); use_loaded_gateway_details(key_store, details_store, gateway_id).await } GatewaySetup::New { @@ -214,7 +214,7 @@ where #[cfg(unix)] connection_fd_callback, } => { - log::debug!("GatewaySetup::New with spec: {specification:?}"); + tracing::debug!("GatewaySetup::New with spec: {specification:?}"); setup_new_gateway( key_store, details_store, @@ -230,9 +230,9 @@ where gateway_details, client_keys: managed_keys, } => { - log::debug!("GatewaySetup::ReuseConnection"); + tracing::debug!("GatewaySetup::ReuseConnection"); Ok(reuse_gateway_connection( - authenticated_ephemeral_client, + *authenticated_ephemeral_client, *gateway_details, managed_keys, )) diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index d9a17206c3..8f09980a43 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -218,7 +218,7 @@ pub enum GatewaySetup { ReuseConnection { /// The authenticated ephemeral client that was created during `init` - authenticated_ephemeral_client: InitGatewayClient, + authenticated_ephemeral_client: Box, // Details of this pre-initialised client (i.e. gateway and keys) gateway_details: Box, @@ -261,7 +261,7 @@ impl GatewaySetup { pub fn try_reuse_connection(init_res: InitialisationResult) -> Result { if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client { Ok(GatewaySetup::ReuseConnection { - authenticated_ephemeral_client, + authenticated_ephemeral_client: Box::new(authenticated_ephemeral_client), gateway_details: Box::new(init_res.gateway_registration), client_keys: init_res.client_keys, }) diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index ba1d80f4c4..8be8b10133 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -18,18 +18,54 @@ pub use nym_topology::{ }; #[cfg(target_arch = "wasm32")] -pub(crate) fn spawn_future(future: F) +pub fn spawn_future(future: F) where F: Future + 'static, { wasm_bindgen_futures::spawn_local(future); } +// TODO: expose similar API to the rest of the codebase, +// perhaps with some simple trait for a task to define its name + #[cfg(not(target_arch = "wasm32"))] -pub(crate) fn spawn_future(future: F) +#[track_caller] +pub fn spawn_future(future: F) where F: Future + Send + 'static, F::Output: Send + 'static, { tokio::spawn(future); } + +#[cfg(not(target_arch = "wasm32"))] +#[track_caller] +pub fn spawn_named_future(future: F, name: &str) +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + cfg_if::cfg_if! {if #[cfg(tokio_unstable)] { + #[allow(clippy::expect_used)] + tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future"); + } else { + let _ = name; + tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#); + spawn_future(future); + }} +} + +#[macro_export] +macro_rules! spawn_future { + ($future:expr) => {{ + $crate::spawn_future($future) + }}; + ($future:expr, $name:expr) => {{ + cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] { + $crate::spawn_named_future($future, $name) + } else { + let _ = $name; + $crate::spawn_future($future) + }} + }}; +} diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 7a902bef94..be03487d95 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true [dependencies] async-trait.workspace = true dashmap.workspace = true -log.workspace = true +tracing.workspace = true thiserror.workspace = true time.workspace = true @@ -17,15 +17,27 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +workspace = true +features = ["fs"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true -features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] optional = true +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard] +path = "../../../sqlx-pool-guard" + [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } [features] fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"] diff --git a/common/client-core/surb-storage/build.rs b/common/client-core/surb-storage/build.rs index 62c02c3968..7f77560668 100644 --- a/common/client-core/surb-storage/build.rs +++ b/common/client-core/surb-storage/build.rs @@ -2,23 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { #[cfg(feature = "fs-surb-storage")] { + use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/fs-surbs-example.sqlite"); let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./fs_surbs_migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -28,4 +29,6 @@ async fn main() { // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); } + + Ok(()) } diff --git a/common/client-core/surb-storage/fs_surbs_migrations/20250425120000_add_surb_key_rotation.sql b/common/client-core/surb-storage/fs_surbs_migrations/20250425120000_add_surb_key_rotation.sql new file mode 100644 index 0000000000..8dd70e2e9d --- /dev/null +++ b/common/client-core/surb-storage/fs_surbs_migrations/20250425120000_add_surb_key_rotation.sql @@ -0,0 +1,8 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- default value of 0 implies 'unknown' variant +ALTER TABLE reply_surb + ADD COLUMN encoded_key_rotation TINYINT NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/common/client-core/surb-storage/fs_surbs_migrations/20250627120000_types_cleanup.sql b/common/client-core/surb-storage/fs_surbs_migrations/20250627120000_types_cleanup.sql new file mode 100644 index 0000000000..f0bf246cc1 --- /dev/null +++ b/common/client-core/surb-storage/fs_surbs_migrations/20250627120000_types_cleanup.sql @@ -0,0 +1,81 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- change `previous_flush_timestamp` unix timestamp to `previous_flush` timestamp +CREATE TABLE status_new +( + flush_in_progress INTEGER NOT NULL, + previous_flush TIMESTAMP WITHOUT TIME ZONE NOT NULL, + client_in_use INTEGER NOT NULL +); + +INSERT INTO status_new (flush_in_progress, previous_flush, client_in_use) +SELECT flush_in_progress, + datetime(previous_flush_timestamp, 'unixepoch') AS previous_flush, + client_in_use +FROM status; + +DROP TABLE status; +ALTER TABLE status_new + RENAME TO status; + + +-- change `sent_at_timestamp` unix timestamp to `sent_at` timestamp +CREATE TABLE reply_key_new +( + key_digest BLOB NOT NULL UNIQUE, + reply_key BLOB NOT NULL UNIQUE, + sent_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +INSERT INTO reply_key_new (key_digest, reply_key, sent_at) +SELECT key_digest, + reply_key, + datetime(sent_at_timestamp, 'unixepoch') AS sent_at +FROM reply_key; + +DROP TABLE reply_key; +ALTER TABLE reply_key_new + RENAME TO reply_key; + + +-- change `last_sent_timestamp` unix timestamp to `sent_at` last_sent +CREATE TABLE reply_surb_sender_new +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + last_sent TIMESTAMP WITHOUT TIME ZONE NOT NULL, + tag BLOB NOT NULL UNIQUE +); + +INSERT INTO reply_surb_sender_new (id, last_sent, tag) +SELECT id, + datetime(last_sent_timestamp, 'unixepoch') AS last_sent, + tag +FROM reply_surb_sender; + + +-- recreate `reply_surb` table due to foreign key constraint +CREATE TABLE reply_surb_new +( + reply_surb_sender_id INTEGER NOT NULL, + reply_surb BLOB NOT NULL, + encoded_key_rotation TINYINT NOT NULL, + + FOREIGN KEY (reply_surb_sender_id) REFERENCES reply_surb_sender_new (id) +); + +INSERT INTO reply_surb_new +SELECT * +FROM reply_surb; + +DROP TABLE reply_surb; +ALTER TABLE reply_surb_new + RENAME TO reply_surb; + +DROP TABLE reply_surb_sender; +ALTER TABLE reply_surb_sender_new + RENAME TO reply_surb_sender; + + diff --git a/common/client-core/surb-storage/fs_surbs_migrations/20250702120000_remove_sender_tag.sql b/common/client-core/surb-storage/fs_surbs_migrations/20250702120000_remove_sender_tag.sql new file mode 100644 index 0000000000..2b44a49a2e --- /dev/null +++ b/common/client-core/surb-storage/fs_surbs_migrations/20250702120000_remove_sender_tag.sql @@ -0,0 +1,12 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- don't persist sender_tag in the DB. instead generate fresh one on each restart +-- this will: +-- A) further help against correlation attacks +-- B) realistically after client restarts, we might be in new key rotation anyway meaning receiver would have to start +-- "from scratch" with surbs + +DROP TABLE sender_tag; \ No newline at end of file diff --git a/common/client-core/surb-storage/src/backend/browser_backend.rs b/common/client-core/surb-storage/src/backend/browser_backend.rs index de39fd49b6..3be6eb4aa2 100644 --- a/common/client-core/surb-storage/src/backend/browser_backend.rs +++ b/common/client-core/surb-storage/src/backend/browser_backend.rs @@ -4,6 +4,7 @@ use crate::backend::Empty; use crate::{CombinedReplyStorage, ReplyStorageBackend}; use async_trait::async_trait; +use time::OffsetDateTime; // well, right now we don't have the browser storage : ( // so we keep everything in memory @@ -38,7 +39,10 @@ impl ReplyStorageBackend for Backend { self.empty.init_fresh(fresh).await } - async fn load_surb_storage(&self) -> Result { - self.empty.load_surb_storage().await + async fn load_surb_storage( + &self, + surb_freshness_cutoff: OffsetDateTime, + ) -> Result { + self.empty.load_surb_storage(surb_freshness_cutoff).await } } diff --git a/common/client-core/surb-storage/src/backend/fs_backend/error.rs b/common/client-core/surb-storage/src/backend/fs_backend/error.rs index 02742758ae..e1eb9036da 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/error.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/error.rs @@ -1,8 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::io; -use std::path::PathBuf; +use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] @@ -30,7 +29,6 @@ pub enum StorageError { #[error("failed to perform sqlx migration: {source}")] MigrationError { - #[source] #[from] source: sqlx::migrate::MigrateError, }, @@ -43,7 +41,6 @@ pub enum StorageError { #[error("failed to run the SQL query: {source}")] QueryError { - #[source] #[from] source: sqlx::error::Error, }, diff --git a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs index 02316ddb7f..6edde091ce 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs @@ -3,21 +3,21 @@ use crate::backend::fs_backend::{ error::StorageError, - models::{ - ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, - StoredSurbSender, - }, + models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender}, }; -use log::{error, info}; use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; use std::path::Path; +use time::OffsetDateTime; +use tracing::{error, info}; + +use sqlx_pool_guard::SqlitePoolGuard; #[derive(Debug, Clone)] pub struct StorageManager { - pub connection_pool: sqlx::SqlitePool, + connection_pool: SqlitePoolGuard, } // all SQL goes here @@ -49,11 +49,14 @@ impl StorageManager { } }; + let connection_pool = SqlitePoolGuard::new(connection_pool); + if let Err(err) = sqlx::migrate!("./fs_surbs_migrations") - .run(&connection_pool) + .run(&*connection_pool) .await { error!("Failed to initialize SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } @@ -61,53 +64,60 @@ impl StorageManager { Ok(StorageManager { connection_pool }) } + /// Close connection pool waiting for all connections to be closed. + pub async fn close_pool(&self) { + self.connection_pool.close().await; + } + #[allow(dead_code)] pub async fn status_table_exists(&self) -> Result { sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await .map(|r| r.is_some()) } pub async fn create_status_table(&self) -> Result<(), sqlx::Error> { - sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)") - .execute(&self.connection_pool) - .await?; + sqlx::query!( + "INSERT INTO status(flush_in_progress, previous_flush, client_in_use) VALUES (0, 0, 1)" + ) + .execute(&*self.connection_pool) + .await?; Ok(()) } pub async fn get_flush_status(&self) -> Result { sqlx::query!("SELECT flush_in_progress FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.flush_in_progress > 0) } - pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> { - sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp) - .execute(&self.connection_pool) + pub async fn set_previous_flush(&self, timestamp: OffsetDateTime) -> Result<(), sqlx::Error> { + sqlx::query!("UPDATE status SET previous_flush = ?", timestamp) + .execute(&*self.connection_pool) .await?; Ok(()) } - pub async fn get_previous_flush_timestamp(&self) -> Result { - sqlx::query!("SELECT previous_flush_timestamp FROM status;") - .fetch_one(&self.connection_pool) + pub async fn get_previous_flush_time(&self) -> Result { + sqlx::query!(r#"SELECT previous_flush AS "previous_flush: OffsetDateTime" FROM status"#) + .fetch_one(&*self.connection_pool) .await - .map(|r| r.previous_flush_timestamp) + .map(|r| r.previous_flush) } pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { let in_progress_int = i64::from(in_progress); sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_client_in_use_status(&self) -> Result { sqlx::query!("SELECT client_in_use FROM status;") - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await .map(|r| r.client_in_use > 0) } @@ -115,47 +125,21 @@ impl StorageManager { pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { let in_use_int = i64::from(in_use); sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } - pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> { - sqlx::query!("DELETE FROM sender_tag;") - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - pub async fn get_tags(&self) -> Result, sqlx::Error> { - sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",) - .fetch_all(&self.connection_pool) - .await - } - - pub async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - INSERT INTO sender_tag(recipient, tag) VALUES (?, ?); - "#, - stored_tag.recipient, - stored_tag.tag - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_key;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_reply_keys(&self) -> Result, sqlx::Error> { - sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",) - .fetch_all(&self.connection_pool) + sqlx::query_as("SELECT * FROM reply_key;") + .fetch_all(&*self.connection_pool) .await } @@ -165,20 +149,20 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO reply_key(key_digest, reply_key, sent_at_timestamp) VALUES (?, ?, ?); + INSERT INTO reply_key(key_digest, reply_key, sent_at) VALUES (?, ?, ?); "#, stored_reply_key.key_digest, stored_reply_key.reply_key, - stored_reply_key.sent_at_timestamp + stored_reply_key.sent_at ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } pub async fn get_surb_senders(&self) -> Result, sqlx::Error> { - sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",) - .fetch_all(&self.connection_pool) + sqlx::query_as("SELECT * FROM reply_surb_sender;") + .fetch_all(&*self.connection_pool) .await } @@ -188,12 +172,12 @@ impl StorageManager { ) -> Result { let id = sqlx::query!( r#" - INSERT INTO reply_surb_sender(tag, last_sent_timestamp) VALUES (?, ?); + INSERT INTO reply_surb_sender(tag, last_sent) VALUES (?, ?); "#, stored_surb_sender.tag, - stored_surb_sender.last_sent_timestamp + stored_surb_sender.last_sent ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await? .last_insert_rowid(); Ok(id) @@ -205,20 +189,23 @@ impl StorageManager { ) -> Result, sqlx::Error> { sqlx::query_as!( StoredReplySurb, - "SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?", + r#" + SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb + WHERE reply_surb_sender_id = ? + "#, sender_id ) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_surb;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; sqlx::query!("DELETE FROM reply_surb_sender;") - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) @@ -230,12 +217,13 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?); + INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?); "#, stored_reply_surb.reply_surb_sender_id, - stored_reply_surb.reply_surb + stored_reply_surb.reply_surb, + stored_reply_surb.encoded_key_rotation ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -249,7 +237,7 @@ impl StorageManager { SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata; "#, ) - .fetch_one(&self.connection_pool) + .fetch_one(&*self.connection_pool) .await } @@ -263,7 +251,7 @@ impl StorageManager { "#, metadata.min_reply_surb_threshold, metadata.max_reply_surb_threshold, - ).execute(&self.connection_pool).await?; + ).execute(&*self.connection_pool).await?; Ok(()) } } diff --git a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs index 4467e17538..625913e174 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs @@ -1,20 +1,19 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::backend::fs_backend::manager::StorageManager; -use crate::backend::fs_backend::models::{ - ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, -}; -use crate::surb_storage::ReceivedReplySurbs; use crate::{ - CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags, + backend::fs_backend::{ + manager::StorageManager, + models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender}, + }, + surb_storage::ReceivedReplySurbs, + CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, }; use async_trait::async_trait; -use log::{debug, error, info, warn}; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use std::fs; use std::path::{Path, PathBuf}; use time::OffsetDateTime; +use tracing::{error, info, warn}; pub use self::error::StorageError; @@ -41,21 +40,20 @@ impl Backend { } let manager = StorageManager::init(database_path, true).await?; - manager.create_status_table().await?; - - let backend = Backend { - temporary_old_path: None, - database_path: owned_path, - manager, - }; - - Ok(backend) + match manager.create_status_table().await { + Ok(()) => Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }), + Err(err) => { + manager.close_pool().await; + Err(err.into()) + } + } } - pub async fn try_load>( - database_path: P, - fresh_sender_tags: bool, - ) -> Result { + pub async fn try_load>(database_path: P) -> Result { let owned_path: PathBuf = database_path.as_ref().into(); if owned_path.file_name().is_none() { return Err(StorageError::DatabasePathWithoutFilename { @@ -64,15 +62,33 @@ impl Backend { } let manager = StorageManager::init(database_path, false).await?; + match Self::try_load_inner(&manager).await { + Ok(()) => Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }), + Err(e) => { + manager.close_pool().await; + Err(e) + } + } + } + /// Gracefully close sqlite connection pool and drop backend. + pub async fn shutdown(self) { + self.manager.close_pool().await + } + + async fn try_load_inner(manager: &StorageManager) -> Result<(), StorageError> { // the database flush wasn't fully finished and thus the data is in inconsistent state // (we don't really know what's properly saved or what's not) if manager.get_flush_status().await? { return Err(StorageError::IncompleteDataFlush); } - let last_flush_timestamp = manager.get_previous_flush_timestamp().await?; - if last_flush_timestamp == 0 { + let last_flush = manager.get_previous_flush_time().await?; + if last_flush == OffsetDateTime::UNIX_EPOCH { // either this client has been running since 1970 or the flush failed return Err(StorageError::IncompleteDataFlush); } @@ -92,15 +108,6 @@ impl Backend { return Err(err.into()); } - let last_flush = match OffsetDateTime::from_unix_timestamp(last_flush_timestamp) { - Ok(last_flush) => last_flush, - Err(err) => { - return Err(StorageError::CorruptedData { - details: format!("failed to parse stored timestamp - {err}"), - }); - } - }; - // in theory clients can use our reply surbs whenever they want, even a year in the future // (assuming no key rotation has happened) // but the way it's currently coded, everyone will purge old data @@ -118,28 +125,11 @@ impl Backend { manager.delete_all_reply_keys().await?; } - if days > 2 { - info!("it's been over {days} days and {hours} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now."); - manager.delete_all_tags().await?; - } else if fresh_sender_tags { - debug!("starting with fresh sender tags"); - manager.delete_all_tags().await?; - } - - Ok(Backend { - temporary_old_path: None, - database_path: owned_path, - // manager: StorageManagerState::Storage(manager), - manager, - }) - } - - async fn close_pool(&mut self) { - self.manager.connection_pool.close().await; + Ok(()) } async fn rotate(&mut self) -> Result<(), StorageError> { - self.close_pool().await; + self.manager.close_pool().await; let new_extension = if let Some(existing_extension) = self.database_path.extension().and_then(|ext| ext.to_str()) @@ -152,7 +142,8 @@ impl Backend { let mut temp_old = self.database_path.clone(); temp_old.set_extension(new_extension); - fs::rename(&self.database_path, &temp_old) + tokio::fs::rename(&self.database_path, &temp_old) + .await .map_err(|err| StorageError::DatabaseRenameError { source: err })?; self.manager = StorageManager::init(&self.database_path, true).await?; self.manager.create_status_table().await?; @@ -161,9 +152,10 @@ impl Backend { Ok(()) } - fn remove_old(&mut self) -> Result<(), StorageError> { + async fn remove_old(&mut self) -> Result<(), StorageError> { if let Some(old_path) = self.temporary_old_path.take() { - fs::remove_file(old_path) + tokio::fs::remove_file(old_path) + .await .map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err }) } else { warn!("the old database file doesn't seem to exist!"); @@ -177,7 +169,7 @@ impl Backend { async fn end_storage_flush(&self) -> Result<(), StorageError> { self.manager - .set_previous_flush_timestamp(OffsetDateTime::now_utc().unix_timestamp()) + .set_previous_flush(OffsetDateTime::now_utc()) .await?; Ok(self.manager.set_flush_status(false).await?) } @@ -190,29 +182,6 @@ impl Backend { Ok(self.manager.set_client_in_use_status(false).await?) } - async fn get_stored_tags(&self) -> Result { - let stored = self.manager.get_tags().await?; - - // stop at the first instance of corruption. if even a single entry is malformed, - // something weird has happened and we can't trust the rest of the data - let raw = stored - .into_iter() - .map(TryInto::try_into) - .collect::>()?; - - Ok(UsedSenderTags::from_raw(raw)) - } - - async fn dump_sender_tags(&self, tags: &UsedSenderTags) -> Result<(), StorageError> { - for map_ref in tags.as_raw_iter() { - let (recipient, tag) = map_ref.pair(); - self.manager - .insert_tag(StoredSenderTag::new(*recipient, *tag)) - .await?; - } - Ok(()) - } - async fn get_stored_reply_keys(&self) -> Result { let stored = self.manager.get_reply_keys().await?; @@ -236,14 +205,17 @@ impl Backend { Ok(()) } - async fn get_stored_reply_surbs(&self) -> Result { + async fn get_stored_reply_surbs( + &self, + surb_freshness_cutoff: OffsetDateTime, + ) -> Result { let surb_senders = self.manager.get_surb_senders().await?; let metadata = self.get_reply_surb_storage_metadata().await?; let mut received_surbs = Vec::with_capacity(surb_senders.len()); for sender in surb_senders { let sender_id = sender.id; - let (sender_tag, surbs_last_received_at_timestamp): (AnonymousSenderTag, i64) = + let (sender_tag, surbs_last_received_at): (AnonymousSenderTag, OffsetDateTime) = sender.try_into()?; let stored_surbs = self .manager @@ -255,15 +227,17 @@ impl Backend { received_surbs.push(( sender_tag, - ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at_timestamp), + ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at), )) } - Ok(ReceivedReplySurbsMap::from_raw( + let received_surbs = ReceivedReplySurbsMap::from_raw( metadata.min_reply_surb_threshold as usize, metadata.max_reply_surb_threshold as usize, received_surbs, - )) + ); + received_surbs.drop_stale_loaded_surbs(surb_freshness_cutoff); + Ok(received_surbs) } async fn dump_reply_surbs( @@ -285,6 +259,14 @@ impl Backend { .insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb)) .await? } + + // TODO: should we also retain the stale ones? + if received_surbs.possibly_stale_left() != 0 { + warn!( + "dropping {} possibly stale surbs for {tag}", + received_surbs.possibly_stale_left() + ); + } } Ok(()) } @@ -328,14 +310,13 @@ impl ReplyStorageBackend for Backend { self.rotate().await?; self.start_storage_flush().await?; - self.dump_sender_tags(storage.tags_storage_ref()).await?; self.dump_sender_reply_keys(storage.key_storage_ref()) .await?; let surbs_ref = storage.surbs_storage_ref(); self.dump_reply_surb_storage_metadata(surbs_ref).await?; self.dump_reply_surbs(surbs_ref).await?; - self.remove_old()?; + self.remove_old().await?; self.end_storage_flush().await } @@ -345,12 +326,14 @@ impl ReplyStorageBackend for Backend { .await } - async fn load_surb_storage(&self) -> Result { + async fn load_surb_storage( + &self, + surb_freshness_cutoff: OffsetDateTime, + ) -> Result { let reply_keys = self.get_stored_reply_keys().await?; - let tags = self.get_stored_tags().await?; - let reply_surbs = self.get_stored_reply_surbs().await?; + let reply_surbs = self.get_stored_reply_surbs(surb_freshness_cutoff).await?; - Ok(CombinedReplyStorage::load(reply_keys, reply_surbs, tags)) + Ok(CombinedReplyStorage::load(reply_keys, reply_surbs)) } async fn stop_storage_session(self) -> Result<(), Self::StorageError> { diff --git a/common/client-core/surb-storage/src/backend/fs_backend/models.rs b/common/client-core/surb-storage/src/backend/fs_backend/models.rs index 8acf83f676..3c3ad9da3b 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/models.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/models.rs @@ -3,13 +3,18 @@ use crate::backend::fs_backend::error::StorageError; use crate::key_storage::UsedReplyKey; +use crate::ReceivedReplySurb; use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::Digest; use nym_sphinx::addressing::clients::{Recipient, RecipientBytes}; use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; -use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize}; -use nym_sphinx::params::ReplySurbKeyDigestAlgorithm; +use nym_sphinx::anonymous_replies::{ + ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize, +}; +use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation}; +use sqlx::FromRow; +use time::OffsetDateTime; #[derive(Debug, Clone)] pub struct StoredSenderTag { @@ -56,11 +61,11 @@ impl TryFrom for (RecipientBytes, AnonymousSenderTag) { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromRow)] pub struct StoredReplyKey { pub key_digest: Vec, pub reply_key: Vec, - pub sent_at_timestamp: i64, + pub sent_at: OffsetDateTime, } impl StoredReplyKey { @@ -68,7 +73,7 @@ impl StoredReplyKey { StoredReplyKey { key_digest: key_digest.to_vec(), reply_key: (*reply_key).to_bytes(), - sent_at_timestamp: reply_key.sent_at_timestamp, + sent_at: reply_key.sent_at, } } } @@ -98,32 +103,30 @@ impl TryFrom for (EncryptionKeyDigest, UsedReplyKey) { }); }; - Ok(( - digest, - UsedReplyKey::new(reply_key, value.sent_at_timestamp), - )) + Ok((digest, UsedReplyKey::new(reply_key, value.sent_at))) } } +#[derive(FromRow)] pub struct StoredSurbSender { pub id: i64, pub tag: Vec, - pub last_sent_timestamp: i64, + pub last_sent: OffsetDateTime, } impl StoredSurbSender { - pub fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self { + pub fn new(tag: AnonymousSenderTag, last_sent: OffsetDateTime) -> Self { StoredSurbSender { // for the purposes of STORING data, // we ignore that field anyway id: 0, tag: tag.to_bytes().to_vec(), - last_sent_timestamp, + last_sent, } } } -impl TryFrom for (AnonymousSenderTag, i64) { +impl TryFrom for (AnonymousSenderTag, OffsetDateTime) { type Error = StorageError; fn try_from(value: StoredSurbSender) -> Result { @@ -138,7 +141,7 @@ impl TryFrom for (AnonymousSenderTag, i64) { Ok(( AnonymousSenderTag::from_bytes(sender_tag_bytes), - value.last_sent_timestamp, + value.last_sent, )) } } @@ -146,24 +149,40 @@ impl TryFrom for (AnonymousSenderTag, i64) { pub struct StoredReplySurb { pub reply_surb_sender_id: i64, pub reply_surb: Vec, + + // encodes only whether it's 'even', 'odd' or 'unknown' (default) + // and not the whole id because that's redundant + pub encoded_key_rotation: u8, } impl StoredReplySurb { - pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self { + pub fn new(reply_surb_sender_id: i64, reply_surb: &ReceivedReplySurb) -> Self { StoredReplySurb { reply_surb_sender_id, - reply_surb: reply_surb.to_bytes(), + reply_surb: reply_surb.surb.inner_reply_surb().to_bytes(), + encoded_key_rotation: reply_surb.key_rotation() as u8, } } } -impl TryFrom for ReplySurb { +impl TryFrom for ReplySurbWithKeyRotation { type Error = StorageError; fn try_from(value: StoredReplySurb) -> Result { - ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData { - details: format!("failed to recover the reply surb: {err}"), - }) + let key_rotation = + SphinxKeyRotation::try_from(value.encoded_key_rotation).map_err(|err| { + StorageError::CorruptedData { + details: format!("stored key rotation was malformed: {err}"), + } + })?; + + let reply_surb = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| { + StorageError::CorruptedData { + details: format!("failed to recover the reply surb: {err}"), + } + })?; + + Ok(reply_surb.with_key_rotation(key_rotation)) } } diff --git a/common/client-core/surb-storage/src/backend/mod.rs b/common/client-core/surb-storage/src/backend/mod.rs index 8bef5a9aaf..b848552168 100644 --- a/common/client-core/surb-storage/src/backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/mod.rs @@ -5,6 +5,7 @@ use crate::CombinedReplyStorage; use async_trait::async_trait; use std::error::Error; use thiserror::Error; +use time::OffsetDateTime; // TODO: this should now live inside our wasm/client-core pub mod browser_backend; @@ -53,7 +54,10 @@ impl ReplyStorageBackend for Empty { Ok(()) } - async fn load_surb_storage(&self) -> Result { + async fn load_surb_storage( + &self, + _: OffsetDateTime, + ) -> Result { Ok(CombinedReplyStorage::new( self.min_surb_threshold, self.max_surb_threshold, @@ -80,7 +84,10 @@ pub trait ReplyStorageBackend: Sized { /// (such as surb thresholds) async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>; - async fn load_surb_storage(&self) -> Result; + async fn load_surb_storage( + &self, + surb_freshness_cutoff: OffsetDateTime, + ) -> Result; async fn stop_storage_session(self) -> Result<(), Self::StorageError> { Ok(()) diff --git a/common/client-core/surb-storage/src/combined.rs b/common/client-core/surb-storage/src/combined.rs index 9d44e53923..51c13bc532 100644 --- a/common/client-core/surb-storage/src/combined.rs +++ b/common/client-core/surb-storage/src/combined.rs @@ -25,12 +25,11 @@ impl CombinedReplyStorage { pub fn load( sent_reply_keys: SentReplyKeys, received_reply_surbs: ReceivedReplySurbsMap, - used_tags: UsedSenderTags, ) -> Self { CombinedReplyStorage { sent_reply_keys, received_reply_surbs, - used_tags, + used_tags: UsedSenderTags::new(), } } diff --git a/common/client-core/surb-storage/src/key_storage.rs b/common/client-core/surb-storage/src/key_storage.rs index 61a474ad5c..b62cd87d5f 100644 --- a/common/client-core/surb-storage/src/key_storage.rs +++ b/common/client-core/surb-storage/src/key_storage.rs @@ -47,8 +47,12 @@ impl SentReplyKeys { self.inner.data.iter() } + pub fn retain(&self, f: impl FnMut(&EncryptionKeyDigest, &mut UsedReplyKey) -> bool) { + self.inner.data.retain(f); + } + pub fn insert_multiple(&self, keys: Vec) { - let now = OffsetDateTime::now_utc().unix_timestamp(); + let now = OffsetDateTime::now_utc(); for key in keys { self.insert(UsedReplyKey::new(key, now)) } @@ -71,15 +75,12 @@ impl SentReplyKeys { pub struct UsedReplyKey { key: SurbEncryptionKey, // the purpose of this field is to perform invalidation at relatively very long intervals - pub sent_at_timestamp: i64, + pub sent_at: OffsetDateTime, } impl UsedReplyKey { - pub(crate) fn new(key: SurbEncryptionKey, sent_at_timestamp: i64) -> Self { - UsedReplyKey { - key, - sent_at_timestamp, - } + pub(crate) fn new(key: SurbEncryptionKey, sent_at: OffsetDateTime) -> Self { + UsedReplyKey { key, sent_at } } } diff --git a/common/client-core/surb-storage/src/lib.rs b/common/client-core/surb-storage/src/lib.rs index 9e0ba14b34..b7ac924706 100644 --- a/common/client-core/surb-storage/src/lib.rs +++ b/common/client-core/surb-storage/src/lib.rs @@ -4,8 +4,9 @@ pub use backend::*; pub use combined::CombinedReplyStorage; pub use key_storage::SentReplyKeys; -pub use surb_storage::ReceivedReplySurbsMap; +pub use surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap, RetrievedReplySurb}; pub use tag_storage::UsedSenderTags; +use time::OffsetDateTime; mod backend; mod combined; @@ -29,17 +30,19 @@ where PersistentReplyStorage { backend } } - pub async fn load_state_from_backend(&self) -> Result { - self.backend.load_surb_storage().await + pub async fn load_state_from_backend( + &self, + surb_freshness_cutoff: OffsetDateTime, + ) -> Result { + self.backend.load_surb_storage(surb_freshness_cutoff).await } - // this will have to get enabled after merging develop pub async fn flush_on_shutdown( mut self, mem_state: CombinedReplyStorage, mut shutdown: nym_task::TaskClient, ) { - use log::{debug, error, info}; + use tracing::{debug, error, info}; debug!("Started PersistentReplyStorage"); if let Err(err) = self.backend.start_storage_session().await { @@ -50,7 +53,6 @@ where shutdown.recv().await; info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); - info!("you MUST NOT forcefully shutdown now or you risk data corruption!"); if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { error!("failed to flush our reply-related data to the persistent storage: {err}") } else { diff --git a/common/client-core/surb-storage/src/surb_storage.rs b/common/client-core/surb-storage/src/surb_storage.rs index 92050c0104..2852c8b8cb 100644 --- a/common/client-core/surb-storage/src/surb_storage.rs +++ b/common/client-core/surb-storage/src/surb_storage.rs @@ -1,15 +1,45 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use dashmap::iter::Iter; +use dashmap::iter::{Iter, IterMut}; use dashmap::DashMap; -use log::trace; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; -use nym_sphinx::anonymous_replies::ReplySurb; +use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; +use nym_sphinx::params::SphinxKeyRotation; +use std::cmp::min; use std::collections::VecDeque; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use time::OffsetDateTime; +use tracing::{error, info, trace}; + +#[derive(Debug)] +pub struct RetrievedReplySurb { + pub(crate) reply_surb: ReceivedReplySurb, + pub(crate) stale_pile: bool, +} + +impl RetrievedReplySurb { + pub(crate) fn new_fresh(reply_surb: ReceivedReplySurb) -> Self { + RetrievedReplySurb { + reply_surb, + stale_pile: false, + } + } + + pub(crate) fn new_stale(reply_surb: ReceivedReplySurb) -> Self { + RetrievedReplySurb { + reply_surb, + stale_pile: true, + } + } +} + +impl From for ReplySurbWithKeyRotation { + fn from(retrieved: RetrievedReplySurb) -> Self { + retrieved.reply_surb.into() + } +} #[derive(Debug, Clone)] pub struct ReceivedReplySurbsMap { @@ -57,17 +87,40 @@ impl ReceivedReplySurbsMap { self.inner.data.iter() } - pub fn remove(&self, target: &AnonymousSenderTag) { - self.inner.data.remove(target); + pub fn as_raw_iter_mut(&self) -> IterMut<'_, AnonymousSenderTag, ReceivedReplySurbs> { + self.inner.data.iter_mut() } - pub fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) { - if let Some(mut entry) = self.inner.data.get_mut(target) { - entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + fn total_surbs(&self) -> usize { + self.inner + .data + .iter() + .map(|entry| entry.value().data.len()) + .sum() + } + + pub fn drop_stale_loaded_surbs(&self, cutoff: OffsetDateTime) { + let before = self.total_surbs(); + self.inner.data.retain(|_, v| { + if v.surbs_last_received_at() < cutoff { + return false; + } + + v.data.retain(|s| s.received_at > cutoff); + !v.data.is_empty() + }); + let after = self.total_surbs(); + let diff = before - after; + if diff != 0 { + info!("removed {diff} stale reply SURBs") } } - pub fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option { + pub fn retain(&self, f: impl FnMut(&AnonymousSenderTag, &mut ReceivedReplySurbs) -> bool) { + self.inner.data.retain(f); + } + + pub fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option { self.inner .data .get(target) @@ -126,15 +179,25 @@ impl ReceivedReplySurbsMap { .unwrap_or_default() } + pub fn available_fresh_surbs(&self, target: &AnonymousSenderTag) -> usize { + self.inner + .data + .get(target) + .map(|entry| entry.fresh_left()) + .unwrap_or_default() + } + pub fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool { self.inner.data.contains_key(target) } + /// Attempt to retrieve the specified number of reply SURBs for the target sender + /// and return the number of SURBs remaining in the storage after the call. pub fn get_reply_surbs( &self, target: &AnonymousSenderTag, amount: usize, - ) -> (Option>, usize) { + ) -> (Option>, usize) { if let Some(mut entry) = self.inner.data.get_mut(target) { let surbs_left = entry.items_left(); if surbs_left < self.min_surb_threshold() + amount { @@ -150,34 +213,72 @@ impl ReceivedReplySurbsMap { pub fn get_reply_surb_ignoring_threshold( &self, target: &AnonymousSenderTag, - ) -> Option<(Option, usize)> { - self.inner - .data - .get_mut(target) - .map(|mut s| s.get_reply_surb()) + ) -> (Option, usize) { + let Some(mut entry) = self.inner.data.get_mut(target) else { + return (None, 0); + }; + + entry.get_reply_surb() } pub fn get_reply_surb( &self, target: &AnonymousSenderTag, - ) -> Option<(Option, usize)> { - self.inner.data.get_mut(target).map(|mut entry| { - let surbs_left = entry.items_left(); - if surbs_left < self.min_surb_threshold() { - (None, surbs_left) - } else { - entry.get_reply_surb() - } - }) + ) -> (Option, usize) { + let Some(mut entry) = self.inner.data.get_mut(target) else { + return (None, 0); + }; + + let surbs_left = entry.items_left(); + if surbs_left < self.min_surb_threshold() { + (None, surbs_left) + } else { + entry.get_reply_surb() + } } - pub fn insert_surbs>( + pub fn re_insert_reply_surbs( + &self, + target: &AnonymousSenderTag, + surbs: Vec, + ) { + error!("re-inserting {} unused surbs", surbs.len()); + let mut entry = self.inner.data.entry(*target).or_insert_with(|| { + // this branch should realistically NEVER happen, but software be software, so let's not crash + error!("attempting to return surbs to no longer existing entry {target}"); + ReceivedReplySurbs::new(VecDeque::new()) + }); + + let entry = entry.value_mut(); + for returned_surb in surbs.into_iter().rev() { + if returned_surb.stale_pile { + entry.possibly_stale.push_front(returned_surb.reply_surb) + } else { + entry.data.push_front(returned_surb.reply_surb) + } + } + } + + pub fn insert_fresh_surbs>( &self, target: &AnonymousSenderTag, surbs: I, ) { if let Some(mut existing_data) = self.inner.data.get_mut(target) { - existing_data.insert_reply_surbs(surbs) + existing_data.insert_fresh_reply_surbs(surbs); + + if existing_data.possibly_stale.is_empty() { + return; + } + + // if we're above the minimum threshold, remove stale surbs + let threshold = self.min_surb_threshold(); + let diff = existing_data.data.len().saturating_sub(threshold); + + trace!("will attempt to remove up to {diff} stale surbs"); + if diff > 0 { + existing_data.remove_stale_surbs(diff); + } } else { let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect()); self.inner.data.insert(*target, new_entry); @@ -185,44 +286,102 @@ impl ReceivedReplySurbsMap { } } +#[derive(Debug)] +pub struct ReceivedReplySurb { + pub(crate) surb: ReplySurbWithKeyRotation, + pub(crate) received_at: OffsetDateTime, +} + +impl From for ReplySurbWithKeyRotation { + fn from(surb: ReceivedReplySurb) -> Self { + surb.surb + } +} + +impl ReceivedReplySurb { + pub fn received_at(&self) -> OffsetDateTime { + self.received_at + } + + pub fn key_rotation(&self) -> SphinxKeyRotation { + self.surb.key_rotation() + } +} + #[derive(Debug)] pub struct ReceivedReplySurbs { - // in the future we'd probably want to put extra data here to indicate when the SURBs got received - // so we could invalidate entries from the previous key rotations - data: VecDeque, + data: VecDeque, + possibly_stale: VecDeque, pending_reception: u32, - surbs_last_received_at_timestamp: i64, + surbs_last_received_at: OffsetDateTime, } impl ReceivedReplySurbs { - fn new(initial_surbs: VecDeque) -> Self { - ReceivedReplySurbs { - data: initial_surbs, + fn new(initial_surbs: VecDeque) -> Self { + let mut this = ReceivedReplySurbs { + data: Default::default(), + possibly_stale: Default::default(), pending_reception: 0, - surbs_last_received_at_timestamp: OffsetDateTime::now_utc().unix_timestamp(), - } + surbs_last_received_at: OffsetDateTime::now_utc(), + }; + this.insert_fresh_reply_surbs(initial_surbs); + this } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub fn new_retrieved( - surbs: Vec, - surbs_last_received_at_timestamp: i64, + surbs: Vec, + surbs_last_received_at: OffsetDateTime, ) -> ReceivedReplySurbs { - ReceivedReplySurbs { - data: surbs.into(), + let mut this = ReceivedReplySurbs { + data: Default::default(), + possibly_stale: Default::default(), pending_reception: 0, - surbs_last_received_at_timestamp, - } + surbs_last_received_at, + }; + this.insert_fresh_reply_surbs(surbs); + this.surbs_last_received_at = surbs_last_received_at; + this + } + + pub fn downgrade_freshness(&mut self) -> usize { + debug_assert!(self.possibly_stale.is_empty()); + std::mem::swap(&mut self.data, &mut self.possibly_stale); + self.possibly_stale.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() && self.possibly_stale.is_empty() } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub fn surbs_ref(&self) -> &VecDeque { + pub fn surbs_ref(&self) -> &VecDeque { &self.data } - pub fn surbs_last_received_at(&self) -> i64 { - self.surbs_last_received_at_timestamp + pub fn retain_fresh_surbs(&mut self, f: impl FnMut(&ReceivedReplySurb) -> bool) { + self.data.retain(f); + } + + pub fn retain_possibly_stale_surbs(&mut self, f: impl FnMut(&ReceivedReplySurb) -> bool) { + self.possibly_stale.retain(f); + } + + pub fn fresh_left(&self) -> usize { + self.data.len() + } + + pub fn possibly_stale_left(&self) -> usize { + self.possibly_stale.len() + } + + pub fn drop_possibly_stale_surbs(&mut self) { + self.possibly_stale = VecDeque::new(); + } + + pub fn surbs_last_received_at(&self) -> OffsetDateTime { + self.surbs_last_received_at } pub fn pending_reception(&self) -> u32 { @@ -243,33 +402,78 @@ impl ReceivedReplySurbs { self.pending_reception = 0; } - pub fn get_reply_surbs(&mut self, amount: usize) -> (Option>, usize) { + /// Attempt to retrieve the specified number of reply SURBs (if at least that many are present) + /// and return the number of SURBs remaining in the storage after the call. + pub fn get_reply_surbs(&mut self, amount: usize) -> (Option>, usize) { if self.items_left() < amount { (None, self.items_left()) } else { - let surbs = self.data.drain(..amount).collect(); - (Some(surbs), self.items_left()) + let available_fresh = self.fresh_left(); + + // prefer the 'fresh' data if available. otherwise fallback to the possibly stale entries + let mut reply_surbs = Vec::with_capacity(amount); + + let fresh_to_retrieve = min(available_fresh, amount); + + for surb in self.data.drain(..fresh_to_retrieve) { + reply_surbs.push(RetrievedReplySurb::new_fresh(surb)) + } + + if available_fresh < amount { + let stale_to_retrieve = amount - fresh_to_retrieve; + for surb in self.possibly_stale.drain(..stale_to_retrieve) { + reply_surbs.push(RetrievedReplySurb::new_stale(surb)) + } + } + + (Some(reply_surbs), self.items_left()) } } - pub fn get_reply_surb(&mut self) -> (Option, usize) { + pub fn get_reply_surb(&mut self) -> (Option, usize) { (self.pop_surb(), self.items_left()) } - fn pop_surb(&mut self) -> Option { - self.data.pop_front() + fn pop_surb(&mut self) -> Option { + // prefer the 'fresh' data if available. otherwise fallback to the possibly stale entries + if let Some(fresh) = self.data.pop_front() { + return Some(RetrievedReplySurb::new_fresh(fresh)); + } + if let Some(stale) = self.possibly_stale.pop_front() { + return Some(RetrievedReplySurb::new_stale(stale)); + } + None } fn items_left(&self) -> usize { - self.data.len() + self.data.len() + self.possibly_stale.len() + } + + pub fn remove_stale_surbs(&mut self, amount: usize) { + // remove up to amount number of possibly stale surbs + let amount = min(amount, self.possibly_stale.len()); + + self.possibly_stale.drain(..amount); } // realistically we're always going to be getting multiple surbs at once - pub fn insert_reply_surbs>(&mut self, surbs: I) { - let mut v = surbs.into_iter().collect::>(); + pub(crate) fn insert_fresh_reply_surbs>( + &mut self, + surbs: I, + ) { + let received_at = OffsetDateTime::now_utc(); + let mut v = surbs + .into_iter() + .map(|surb| ReceivedReplySurb { surb, received_at }) + .collect::>(); + + if v.is_empty() { + return; + } + trace!("storing {} surbs in the storage", v.len()); self.data.append(&mut v); - self.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + self.surbs_last_received_at = received_at; trace!("we now have {} surbs!", self.data.len()); } } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index fc1676ab67..de90b3ad35 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, - SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, + GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey, + SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; @@ -201,7 +201,7 @@ impl GatewayClient { #[cfg(not(target_arch = "wasm32"))] pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { debug!( - "Attemting to establish connection to gateway at: {}", + "Attempting to establish connection to gateway at: {}", self.gateway_address ); let (ws_stream, _) = connect_async( @@ -272,7 +272,7 @@ impl GatewayClient { ) -> Result<(), GatewayClientError> { if let Some(shared_key) = self.shared_key() { let encrypted = message.encrypt(&*shared_key)?; - Box::pin(self.send_websocket_message(encrypted)).await?; + Box::pin(self.send_websocket_message_without_response(encrypted)).await?; Ok(()) } else { Err(GatewayClientError::ConnectionInInvalidState) @@ -330,9 +330,80 @@ impl GatewayClient { } } + /// Attempt to send a websocket message to the gateway without waiting for any response + async fn send_websocket_message_without_response( + &mut self, + msg: impl Into, + ) -> Result<(), GatewayClientError> { + match self.connection { + SocketState::Available(ref mut conn) => Ok(conn.send(msg.into()).await?), + SocketState::PartiallyDelegated(ref mut partially_delegated) => { + if let Err(err) = partially_delegated.send_without_response(msg.into()).await { + error!("failed to send message without response - {err}..."); + // we must ensure we do not leave the task still active + if let Err(err) = self.recover_socket_connection().await { + error!("... and the delegated stream has also errored out - {err}") + } + Err(err) + } else { + Ok(()) + } + } + SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished), + _ => Err(GatewayClientError::ConnectionInInvalidState), + } + } + + // A very nasty hack due to lack of id tags on messages - send a non-sphinx packet websocket + // message and wait until first non 'Send' response within timeout + pub async fn send_websocket_message_with_non_send_response( + &mut self, + msg: impl Into, + ) -> Result { + let should_restart_mixnet_listener = if self.connection.is_partially_delegated() { + self.recover_socket_connection().await?; + true + } else { + false + }; + + let conn = match self.connection { + SocketState::Available(ref mut conn) => conn, + SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished), + _ => return Err(GatewayClientError::ConnectionInInvalidState), + }; + conn.send(msg.into()).await?; + + let timeout = sleep(self.cfg.connection.response_timeout_duration); + tokio::pin!(timeout); + + let response = loop { + tokio::select! { + _ = &mut timeout => { + break Err(GatewayClientError::Timeout); + } + // note: the below will also listen for shutdown signals + msg = self.read_control_response() => { + match msg { + Ok(res) => if !res.is_send() { + break Ok(res); + }, + Err(err) => break Err(err), + } + } + } + }; + + if should_restart_mixnet_listener { + self.start_listening_for_mixnet_messages()?; + } + response + } + + /// Attempt to send a websocket message to the gateway and wait until we receive a response. // If we want to send a message (with response), we need to have a full control over the socket, // as we need to be able to write the request and read the subsequent response - pub async fn send_websocket_message( + pub async fn send_websocket_message_with_response( &mut self, msg: impl Into, ) -> Result { @@ -387,29 +458,6 @@ impl GatewayClient { } } - async fn send_websocket_message_without_response( - &mut self, - msg: Message, - ) -> Result<(), GatewayClientError> { - match self.connection { - SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?), - SocketState::PartiallyDelegated(ref mut partially_delegated) => { - if let Err(err) = partially_delegated.send_without_response(msg).await { - error!("failed to send message without response - {err}..."); - // we must ensure we do not leave the task still active - if let Err(err) = self.recover_socket_connection().await { - error!("... and the delegated stream has also errored out - {err}") - } - Err(err) - } else { - Ok(()) - } - } - SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished), - _ => Err(GatewayClientError::ConnectionInInvalidState), - } - } - fn check_gateway_protocol( &self, gateway_protocol: Option, @@ -535,7 +583,10 @@ impl GatewayClient { .encrypt(legacy_key)?; info!("sending upgrade request and awaiting the acknowledgement back"); - let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? { + let (ciphertext, nonce) = match self + .send_websocket_message_with_response(upgrade_request) + .await? + { ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce), ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) @@ -567,7 +618,7 @@ impl GatewayClient { &mut self, msg: ClientControlRequest, ) -> Result<(), GatewayClientError> { - match self.send_websocket_message(msg).await? { + match self.send_websocket_message_with_response(msg).await? { ServerResponse::Authenticate { protocol_version, status, @@ -662,6 +713,7 @@ impl GatewayClient { let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); + let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet(); if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); @@ -669,6 +721,9 @@ impl GatewayClient { if !supports_auth_v2 { warn!("this gateway is on an old version that doesn't support authentication v2") } + if !supports_key_rotation_info { + warn!("this gateway is on an old version that doesn't support key rotation packets") + } if self.authenticated { debug!("Already authenticated"); @@ -713,13 +768,16 @@ impl GatewayClient { } } + /// Attempt to retrieve the currently supported gateway protocol version of the remote. pub async fn get_gateway_protocol(&mut self) -> Result { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } match self - .send_websocket_message(ClientControlRequest::SupportedProtocol {}) + .send_websocket_message_with_non_send_response( + ClientControlRequest::SupportedProtocol {}, + ) .await? { ServerResponse::SupportedProtocol { version } => Ok(version), @@ -736,7 +794,10 @@ impl GatewayClient { credential, self.shared_key.as_ref().unwrap(), )?; - let bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self + .send_websocket_message_with_non_send_response(msg) + .await? + { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::TypedError { error } => { @@ -754,7 +815,10 @@ impl GatewayClient { async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; - let bandwidth_remaining = match self.send_websocket_message(msg).await? { + let bandwidth_remaining = match self + .send_websocket_message_with_non_send_response(msg) + .await? + { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), @@ -849,6 +913,22 @@ impl GatewayClient { } } + fn mix_packet_to_ws_message(&self, packet: MixPacket) -> Result { + // note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should + // be more explicit in the naming? + let req = if self.negotiated_protocol.supports_key_rotation_packet() { + BinaryRequest::ForwardSphinxV2 { packet } + } else { + BinaryRequest::ForwardSphinx { packet } + }; + + req.into_ws_message( + self.shared_key + .as_ref() + .expect("no shared key present even though we're authenticated!"), + ) + } + pub async fn batch_send_mix_packets( &mut self, packets: Vec, @@ -877,13 +957,7 @@ impl GatewayClient { let messages: Result, _> = packets .into_iter() - .map(|mix_packet| { - BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message( - self.shared_key - .as_ref() - .expect("no shared key present even though we're authenticated!"), - ) - }) + .map(|mix_packet| self.mix_packet_to_ws_message(mix_packet)) .collect(); if let Err(err) = self @@ -949,13 +1023,8 @@ impl GatewayClient { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } - // note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should - // be more explicit in the naming? - let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message( - self.shared_key - .as_ref() - .expect("no shared key present even though we're authenticated!"), - )?; + + let msg = self.mix_packet_to_ws_message(mix_packet)?; self.send_with_reconnection_on_failure(msg).await } diff --git a/common/client-libs/gateway-client/src/client/websockets.rs b/common/client-libs/gateway-client/src/client/websockets.rs index 293435b1cf..9b04c9e028 100644 --- a/common/client-libs/gateway-client/src/client/websockets.rs +++ b/common/client-libs/gateway-client/src/client/websockets.rs @@ -56,7 +56,7 @@ pub(crate) async fn connect_async( } .map_err(|err| GatewayClientError::NetworkConnectionFailed { address: endpoint.to_owned(), - source: err.into(), + source: Box::new(tungstenite::Error::from(err)), })?; #[cfg(unix)] @@ -72,7 +72,7 @@ pub(crate) async fn connect_async( Err(err) => { stream = Err(GatewayClientError::NetworkConnectionFailed { address: endpoint.to_owned(), - source: err.into(), + source: Box::new(tungstenite::Error::from(err)), }); continue; } @@ -83,6 +83,6 @@ pub(crate) async fn connect_async( .await .map_err(|error| GatewayClientError::NetworkConnectionFailed { address: endpoint.to_owned(), - source: error, + source: Box::new(error), }) } diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index a9ce175504..8766f9f5a6 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -25,7 +25,7 @@ pub enum GatewayClientError { RequestError(#[from] GatewayRequestsError), #[error("There was a network error: {0}")] - NetworkError(#[from] WsError), + NetworkError(Box), #[error("failed to upgrade our shared key - the gateway sent malformed response")] FatalKeyUpgradeFailure, @@ -41,7 +41,10 @@ pub enum GatewayClientError { NetworkErrorWasm(#[from] JsError), #[error("connection failed: {address}: {source}")] - NetworkConnectionFailed { address: String, source: WsError }, + NetworkConnectionFailed { + address: String, + source: Box, + }, #[error("no socket address for endpoint: {address}")] NoEndpointForConnection { address: String }, @@ -127,10 +130,16 @@ pub enum GatewayClientError { ShutdownInProgress, } +impl From for GatewayClientError { + fn from(error: WsError) -> Self { + GatewayClientError::NetworkError(Box::new(error)) + } +} + impl GatewayClientError { pub fn is_closed_connection(&self) -> bool { match self { - GatewayClientError::NetworkError(ws_err) => match ws_err { + GatewayClientError::NetworkError(ws_err) => match ws_err.as_ref() { WsError::AlreadyClosed | WsError::ConnectionClosed => true, WsError::Io(io_err) => matches!( io_err.kind(), diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index e8d992027a..394ba352d0 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -28,7 +28,7 @@ pub(crate) fn cleanup_socket_message( msg: Option>, ) -> Result { match msg { - Some(msg) => msg.map_err(GatewayClientError::NetworkError), + Some(msg) => msg.map_err(GatewayClientError::from), None => Err(GatewayClientError::ConnectionAbruptlyClosed), } } @@ -39,7 +39,7 @@ pub(crate) fn cleanup_socket_messages( match msgs { Some(msgs) => msgs .into_iter() - .map(|msg| msg.map_err(GatewayClientError::NetworkError)) + .map(|msg| msg.map_err(GatewayClientError::from)) .collect(), None => Err(GatewayClientError::ConnectionAbruptlyClosed), } diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 319a7bb624..5489ec3628 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -10,7 +10,7 @@ use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use nym_gateway_requests::shared_key::SharedGatewayKey; -use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError}; +use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError}; use nym_task::TaskClient; use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; @@ -188,6 +188,34 @@ impl PartiallyDelegatedRouter { } } } + ServerResponse::EncryptedResponse { ciphertext, nonce } => { + match SensitiveServerResponse::decrypt( + &ciphertext, + &nonce, + self.shared_key.as_ref(), + ) { + Ok(response) => match response { + SensitiveServerResponse::ForgetMeAck {} => { + info!("received forget me acknowledgement"); + } + SensitiveServerResponse::RememberMeAck {} => { + info!("received remember me acknowledgement"); + } + SensitiveServerResponse::KeyUpgradeAck {} => { + warn!( + "received illegal key upgrade acknowledgement in an authenticated client" + ); + } + _ => { + warn!("received unknown SensitiveServerResponse"); + } + }, + Err(e) => { + error!("failed to handle encrypted response: {e}"); + } + } + Ok(()) + } other => { let name = other.name(); warn!("received illegal message of type '{name}' in an authenticated client"); @@ -309,7 +337,7 @@ impl PartiallyDelegatedHandle { // check if the split stream didn't error out let receive_res = stream_receiver .try_recv() - .expect("stream sender was somehow dropped without sending anything!"); + .map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?; if let Some(res) = receive_res { let _res = res?; diff --git a/common/client-libs/mixnet-client/Cargo.toml b/common/client-libs/mixnet-client/Cargo.toml index b240aab513..2a8a39383b 100644 --- a/common/client-libs/mixnet-client/Cargo.toml +++ b/common/client-libs/mixnet-client/Cargo.toml @@ -16,9 +16,14 @@ tokio-util = { workspace = true, features = ["codec"], optional = true } tokio-stream = { workspace = true } # internal +nym-noise = { path = "../../nymnoise" } nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task", optional = true } [features] default = ["client"] -client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"] \ No newline at end of file +client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"] + +[dev-dependencies] +nym-crypto = { path = "../../crypto" } +rand = { workspace = true } diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index 8788a0aee7..5f001810bd 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -3,11 +3,11 @@ use dashmap::DashMap; use futures::StreamExt; -use nym_sphinx::addressing::nodes::NymNodeRoutingAddress; +use nym_noise::config::NoiseConfig; +use nym_noise::upgrade_noise_initiator; +use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; -use nym_sphinx::params::PacketType; -use nym_sphinx::NymPacket; use std::io; use std::net::SocketAddr; use std::ops::Deref; @@ -28,6 +28,7 @@ pub struct Config { pub maximum_reconnection_backoff: Duration, pub initial_connection_timeout: Duration, pub maximum_connection_buffer_size: usize, + pub use_legacy_packet_encoding: bool, } impl Config { @@ -36,12 +37,14 @@ impl Config { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_packet_encoding: bool, ) -> Self { Config { initial_reconnection_backoff, maximum_reconnection_backoff, initial_connection_timeout, maximum_connection_buffer_size, + use_legacy_packet_encoding, } } } @@ -49,23 +52,19 @@ impl Config { pub trait SendWithoutResponse { // Without response in this context means we will not listen for anything we might get back (not // that we should get anything), including any possible io errors - fn send_without_response( - &self, - address: NymNodeRoutingAddress, - packet: NymPacket, - packet_type: PacketType, - ) -> io::Result<()>; + fn send_without_response(&self, packet: MixPacket) -> io::Result<()>; } pub struct Client { active_connections: ActiveConnections, + noise_config: NoiseConfig, connections_count: Arc, config: Config, } #[derive(Default, Clone)] pub struct ActiveConnections { - inner: Arc>, + inner: Arc>, } impl ActiveConnections { @@ -82,7 +81,7 @@ impl ActiveConnections { } impl Deref for ActiveConnections { - type Target = DashMap; + type Target = DashMap; fn deref(&self) -> &Self::Target { &self.inner } @@ -104,6 +103,7 @@ impl ConnectionSender { struct ManagedConnection { address: SocketAddr, + noise_config: NoiseConfig, message_receiver: ReceiverStream, connection_timeout: Duration, current_reconnection: Arc, @@ -112,12 +112,14 @@ struct ManagedConnection { impl ManagedConnection { fn new( address: SocketAddr, + noise_config: NoiseConfig, message_receiver: mpsc::Receiver, connection_timeout: Duration, current_reconnection: Arc, ) -> Self { ManagedConnection { address, + noise_config, message_receiver: ReceiverStream::new(message_receiver), connection_timeout, current_reconnection, @@ -132,9 +134,21 @@ impl ManagedConnection { Ok(stream_res) => match stream_res { Ok(stream) => { debug!("Managed to establish connection to {}", self.address); - // if we managed to connect, reset the reconnection count (whatever it might have been) + + let noise_stream = + match upgrade_noise_initiator(stream, &self.noise_config).await { + Ok(noise_stream) => noise_stream, + Err(err) => { + error!("Failed to perform Noise handshake with {address} - {err}"); + // we failed to finish the noise handshake - increase reconnection attempt + self.current_reconnection.fetch_add(1, Ordering::SeqCst); + return; + } + }; + // if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been) self.current_reconnection.store(0, Ordering::Release); - Framed::new(stream, NymCodec) + debug!("Noise initiator handshake completed for {:?}", address); + Framed::new(noise_stream, NymCodec) } Err(err) => { debug!("failed to establish connection to {address} (err: {err})",); @@ -167,9 +181,14 @@ impl ManagedConnection { } impl Client { - pub fn new(config: Config, connections_count: Arc) -> Client { + pub fn new( + config: Config, + noise_config: NoiseConfig, + connections_count: Arc, + ) -> Client { Client { active_connections: Default::default(), + noise_config, connections_count, config, } @@ -196,7 +215,7 @@ impl Client { } } - fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) { + fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) { let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size); // this CAN'T fail because we just created the channel which has a non-zero capacity @@ -224,6 +243,7 @@ impl Client { let initial_connection_timeout = self.config.initial_connection_timeout; let connections_count = self.connections_count.clone(); + let noise_config = self.noise_config.clone(); tokio::spawn(async move { // before executing the manager, wait for what was specified, if anything if let Some(backoff) = backoff { @@ -233,7 +253,8 @@ impl Client { connections_count.fetch_add(1, Ordering::SeqCst); ManagedConnection::new( - address.into(), + address, + noise_config, receiver, initial_connection_timeout, current_reconnection_attempt, @@ -246,18 +267,19 @@ impl Client { } impl SendWithoutResponse for Client { - fn send_without_response( - &self, - address: NymNodeRoutingAddress, - packet: NymPacket, - packet_type: PacketType, - ) -> io::Result<()> { - trace!("Sending packet to {address:?}"); - let framed_packet = FramedNymPacket::new(packet, packet_type); + fn send_without_response(&self, packet: MixPacket) -> io::Result<()> { + let address = packet.next_hop_address(); + trace!("Sending packet to {address}"); + + // TODO: optimisation for the future: rather than constantly using legacy encoding, + // once we're addressing by node_id (and thus have full node info here), + // we could simply infer supported encoding based on their version + let framed_packet = + FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding); let Some(sender) = self.active_connections.get_mut(&address) else { // there was never a connection to begin with - debug!("establishing initial connection to {}", address); + debug!("establishing initial connection to {address}"); // it's not a 'big' error, but we did not manage to send the packet, but queue the packet // for sending for as soon as the connection is created self.make_connection(address, framed_packet); @@ -302,15 +324,25 @@ impl SendWithoutResponse for Client { #[cfg(test)] mod tests { use super::*; + use nym_crypto::asymmetric::x25519; + use nym_noise::config::NoiseNetworkView; + use rand::rngs::OsRng; fn dummy_client() -> Client { + let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade Client::new( Config { initial_reconnection_backoff: Duration::from_millis(10_000), maximum_reconnection_backoff: Duration::from_millis(300_000), initial_connection_timeout: Duration::from_millis(1_500), maximum_connection_buffer_size: 128, + use_legacy_packet_encoding: false, }, + NoiseConfig::new( + Arc::new(x25519::KeyPair::new(&mut rng)), + NoiseNetworkView::new_empty(), + Duration::from_millis(1_500), + ), Default::default(), ) } diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 7ff310b039..ea4e8beeb4 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -19,6 +19,7 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } +nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" } nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/common/client-libs/validator-client/examples/offline_signing.rs b/common/client-libs/validator-client/examples/offline_signing.rs index 0e5bce5681..62c160f1a8 100644 --- a/common/client-libs/validator-client/examples/offline_signing.rs +++ b/common/client-libs/validator-client/examples/offline_signing.rs @@ -16,8 +16,8 @@ async fn main() { let prefix = "n"; let denom: Denom = "unym".parse().unwrap(); let signer_mnemonic: bip39::Mnemonic = "".parse().unwrap(); - let validator = "https://qwerty-validator.qa.nymte.ch"; - let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap(); + let validator = "https://rpc.sandbox.nymtech.net"; + let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap(); let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic); let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone(); diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 4cb7aaa3b6..2e6d19d9ea 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -25,7 +25,9 @@ use nym_api_requests::models::{ NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; -use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_api_requests::nym_nodes::{ + NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata, +}; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_mixnet_contract_common::EpochRewardedSet; @@ -46,6 +48,46 @@ use crate::rpc::http_client; #[cfg(feature = "http-client")] use crate::{DirectSigningHttpRpcValidatorClient, HttpRpcClient, QueryHttpRpcValidatorClient}; +// a simple helper macro to define to repeatedly call a paged query until a full response is constructed +macro_rules! collect_paged_skimmed_v2 { + ( $self:ident, $f: ident ) => {{ + // unroll first loop iteration in order to obtain the metadata + let mut page = 0; + let res = $self + .nym_api + .$f(false, Some(page), None, $self.use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let mut res = $self + .nym_api + .$f(false, Some(page), None, $self.use_bincode) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(ValidatorClientError::InconsistentPagedMetadata); + } + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + }}; +} + #[must_use] #[derive(Debug, Clone)] pub struct Config { @@ -200,11 +242,11 @@ impl Client { #[allow(deprecated)] impl Client { pub fn api_url(&self) -> &Url { - self.nym_api.current_url() + self.nym_api.current_url().as_ref() } pub fn change_nym_api(&mut self, new_endpoint: Url) { - self.nym_api.change_base_url(new_endpoint) + self.nym_api.change_base_urls(vec![new_endpoint.into()]) } #[deprecated] @@ -345,25 +387,47 @@ impl Client { #[derive(Clone)] pub struct NymApiClient { + pub use_bincode: bool, pub nym_api: nym_api::Client, // TODO: perhaps if we really need it at some (currently I don't see any reasons for it) // we could re-implement the communication with the REST API on port 1317 } +impl From for NymApiClient { + fn from(nym_api: nym_api::Client) -> Self { + NymApiClient { + use_bincode: false, + nym_api, + } + } +} + // we have to allow the use of deprecated method here as they're calling the deprecated trait methods #[allow(deprecated)] impl NymApiClient { pub fn new(api_url: Url) -> Self { let nym_api = nym_api::Client::new(api_url, None); - NymApiClient { nym_api } + NymApiClient { + use_bincode: true, + nym_api, + } } #[cfg(not(target_arch = "wasm32"))] pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { let nym_api = nym_api::Client::new(api_url, Some(timeout)); - NymApiClient { nym_api } + NymApiClient { + use_bincode: true, + nym_api, + } + } + + #[must_use] + pub fn with_bincode(mut self, use_bincode: bool) -> Self { + self.use_bincode = use_bincode; + self } pub fn new_with_user_agent(api_url: Url, user_agent: impl Into) -> Self { @@ -373,15 +437,18 @@ impl NymApiClient { .build::() .expect("failed to build nym api client"); - NymApiClient { nym_api } + NymApiClient { + use_bincode: false, + nym_api, + } } pub fn api_url(&self) -> &Url { - self.nym_api.current_url() + self.nym_api.current_url().as_ref() } pub fn change_nym_api(&mut self, new_endpoint: Url) { - self.nym_api.change_base_url(new_endpoint); + self.nym_api.change_base_urls(vec![new_endpoint.into()]); } #[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")] @@ -400,92 +467,93 @@ impl NymApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes + #[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")] pub async fn get_all_basic_entry_assigned_nodes( &self, ) -> Result, ValidatorClientError> { - // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - let mut page = 0; - let mut nodes = Vec::new(); + self.get_all_basic_entry_assigned_nodes_with_metadata() + .await + .map(|res| res.nodes) + } - loop { - let mut res = self - .nym_api - .get_basic_entry_assigned_nodes(false, Some(page), None) - .await?; - - nodes.append(&mut res.nodes.data); - if nodes.len() < res.nodes.pagination.total { - page += 1 - } else { - break; - } - } - - Ok(nodes) + pub async fn get_all_basic_entry_assigned_nodes_with_metadata( + &self, + ) -> Result { + collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2) } /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes + #[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")] pub async fn get_all_basic_active_mixing_assigned_nodes( &self, ) -> Result, ValidatorClientError> { - // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - let mut page = 0; - let mut nodes = Vec::new(); + self.get_all_basic_active_mixing_assigned_nodes_with_metadata() + .await + .map(|res| res.nodes) + } - loop { - let mut res = self - .nym_api - .get_basic_active_mixing_assigned_nodes(false, Some(page), None) - .await?; - - nodes.append(&mut res.nodes.data); - if nodes.len() < res.nodes.pagination.total { - page += 1 - } else { - break; - } - } - - Ok(nodes) + pub async fn get_all_basic_active_mixing_assigned_nodes_with_metadata( + &self, + ) -> Result { + collect_paged_skimmed_v2!(self, get_basic_active_mixing_assigned_nodes_v2) } /// retrieve basic information for nodes are capable of operating as a mixnode /// this includes legacy mixnodes and nym-nodes + #[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")] pub async fn get_all_basic_mixing_capable_nodes( &self, ) -> Result, ValidatorClientError> { - // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - let mut page = 0; - let mut nodes = Vec::new(); + self.get_all_basic_mixing_capable_nodes_with_metadata() + .await + .map(|res| res.nodes) + } - loop { - let mut res = self - .nym_api - .get_basic_mixing_capable_nodes(false, Some(page), None) - .await?; - - nodes.append(&mut res.nodes.data); - if nodes.len() < res.nodes.pagination.total { - page += 1 - } else { - break; - } - } - - Ok(nodes) + pub async fn get_all_basic_mixing_capable_nodes_with_metadata( + &self, + ) -> Result { + collect_paged_skimmed_v2!(self, get_basic_mixing_capable_nodes_v2) } /// retrieve basic information for all bonded nodes on the network + #[deprecated(note = "use get_all_basic_nodes_with_metadata instead")] pub async fn get_all_basic_nodes(&self) -> Result, ValidatorClientError> { - // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + self.get_all_basic_nodes_with_metadata() + .await + .map(|res| res.nodes) + } + + pub async fn get_all_basic_nodes_with_metadata( + &self, + ) -> Result { + collect_paged_skimmed_v2!(self, get_basic_nodes_v2) + } + + /// retrieve expanded information for all bonded nodes on the network + pub async fn get_all_expanded_nodes( + &self, + ) -> Result { + // Unroll the first iteration to get the metadata let mut page = 0; - let mut nodes = Vec::new(); + + let res = self + .nym_api + .get_expanded_nodes(false, Some(page), None) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; loop { let mut res = self .nym_api - .get_basic_nodes(false, Some(page), None) + .get_expanded_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -496,7 +564,7 @@ impl NymApiClient { } } - Ok(nodes) + Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)) } pub async fn health(&self) -> Result { @@ -651,10 +719,11 @@ impl NymApiClient { pub async fn partial_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { Ok(self .nym_api - .partial_expiration_date_signatures(expiration_date) + .partial_expiration_date_signatures(expiration_date, epoch_id) .await?) } @@ -671,10 +740,11 @@ impl NymApiClient { pub async fn global_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { Ok(self .nym_api - .global_expiration_date_signatures(expiration_date) + .global_expiration_date_signatures(expiration_date, epoch_id) .await?) } diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index 11bdb3d745..6acbc73c78 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -22,6 +22,9 @@ pub enum ValidatorClientError { #[error("nyxd request failed: {0}")] NyxdError(#[from] crate::nyxd::error::NyxdError), + #[error("the response metadata has changed between pages")] + InconsistentPagedMetadata, + #[error("No validator API url has been provided")] NoAPIUrlAvailable, } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index fa916c9d12..aa13634503 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -6,19 +6,22 @@ use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; use async_trait::async_trait; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, - BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse, - IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse, - IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, - IssuedTicketbooksForResponse, VerifyEcashTicketBody, + BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse, + EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest, + IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest, + IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse, + VerifyEcashTicketBody, }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ - AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse, - LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription, - PerformanceHistoryResponse, RewardedSetResponse, + AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse, + ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, + NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, + SignerInformationResponse, }; use nym_api_requests::nym_nodes::{ - NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse, + NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1, + PaginatedCachedNodesResponseV2, }; use nym_api_requests::pagination::PaginatedResponse; pub use nym_api_requests::{ @@ -34,7 +37,7 @@ pub use nym_api_requests::{ MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }, - nym_nodes::{CachedNodesResponse, SkimmedNode}, + nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode}, NymNetworkDetailsResponse, }; use nym_contracts_common::IdentityKey; @@ -62,7 +65,7 @@ pub trait NymApiClientExt: ApiClient { async fn health(&self) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::API_STATUS_ROUTES, routes::HEALTH, ], @@ -75,7 +78,7 @@ pub trait NymApiClientExt: ApiClient { async fn build_information(&self) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::API_STATUS_ROUTES, routes::BUILD_INFORMATION, ], @@ -87,7 +90,7 @@ pub trait NymApiClientExt: ApiClient { #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { - self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) + self.get_json(&[routes::V1_API_VERSION, routes::MIXNODES], NO_PARAMS) .await } @@ -96,7 +99,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::MIXNODES, routes::DETAILED, @@ -111,7 +114,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_gateways_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::GATEWAYS, routes::DETAILED, @@ -128,7 +131,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::GATEWAYS, routes::DETAILED_UNFILTERED, @@ -145,7 +148,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::MIXNODES, routes::DETAILED_UNFILTERED, @@ -158,7 +161,7 @@ pub trait NymApiClientExt: ApiClient { #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_gateways(&self) -> Result, NymAPIError> { - self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) + self.get_json(&[routes::V1_API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } @@ -166,7 +169,7 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( - &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], + &[routes::V1_API_VERSION, routes::GATEWAYS, routes::DESCRIBED], NO_PARAMS, ) .await @@ -176,7 +179,7 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( - &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], + &[routes::V1_API_VERSION, routes::MIXNODES, routes::DESCRIBED], NO_PARAMS, ) .await @@ -201,7 +204,7 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_PERFORMANCE_HISTORY, &*node_id.to_string(), @@ -229,7 +232,7 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_DESCRIBED, ], @@ -256,7 +259,7 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_BONDED, ], @@ -270,7 +273,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_basic_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "mixnodes", @@ -286,7 +289,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_basic_gateways(&self) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "gateways", @@ -301,7 +304,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_rewarded_set(&self) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_REWARDED_SET, ], @@ -312,13 +315,15 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes + #[deprecated(note = "use get_basic_entry_assigned_nodes_v2")] #[instrument(level = "debug", skip(self))] async fn get_basic_entry_assigned_nodes( &self, no_legacy: bool, page: Option, per_page: Option, - ) -> Result, NymAPIError> { + use_bincode: bool, + ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { @@ -333,9 +338,13 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ - routes::API_VERSION, + routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", @@ -347,15 +356,16 @@ pub trait NymApiClientExt: ApiClient { .await } - /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch - /// this includes legacy mixnodes and nym-nodes + /// retrieve basic information for nodes are capable of operating as an entry gateway + /// this includes legacy gateways and nym-nodes #[instrument(level = "debug", skip(self))] - async fn get_basic_active_mixing_assigned_nodes( + async fn get_basic_entry_assigned_nodes_v2( &self, no_legacy: bool, page: Option, per_page: Option, - ) -> Result, NymAPIError> { + use_bincode: bool, + ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { @@ -370,9 +380,55 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ - routes::API_VERSION, + routes::V2_API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "skimmed", + "entry-gateways", + ], + ¶ms, + ) + .await + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + #[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")] + #[instrument(level = "debug", skip(self))] + async fn get_basic_active_mixing_assigned_nodes( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + use_bincode: bool, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( + &[ + routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", @@ -387,12 +443,13 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes #[instrument(level = "debug", skip(self))] - async fn get_basic_mixing_capable_nodes( + async fn get_basic_active_mixing_assigned_nodes_v2( &self, no_legacy: bool, page: Option, per_page: Option, - ) -> Result, NymAPIError> { + use_bincode: bool, + ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { @@ -407,9 +464,56 @@ pub trait NymApiClientExt: ApiClient { params.push(("per_page", per_page.to_string())) } - self.get_json( + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( &[ - routes::API_VERSION, + routes::V2_API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "skimmed", + "mixnodes", + "active", + ], + ¶ms, + ) + .await + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + #[deprecated(note = "use get_basic_mixing_capable_nodes_v2")] + #[instrument(level = "debug", skip(self))] + async fn get_basic_mixing_capable_nodes( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + use_bincode: bool, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( + &[ + routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, "skimmed", @@ -421,13 +525,132 @@ pub trait NymApiClientExt: ApiClient { .await } + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] + async fn get_basic_mixing_capable_nodes_v2( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + use_bincode: bool, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( + &[ + routes::V2_API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "skimmed", + "mixnodes", + "all", + ], + ¶ms, + ) + .await + } + + #[deprecated(note = "use get_basic_nodes_v2")] #[instrument(level = "debug", skip(self))] async fn get_basic_nodes( &self, no_legacy: bool, page: Option, per_page: Option, - ) -> Result, NymAPIError> { + use_bincode: bool, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( + &[ + routes::V1_API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "skimmed", + ], + ¶ms, + ) + .await + } + + #[instrument(level = "debug", skip(self))] + async fn get_basic_nodes_v2( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + use_bincode: bool, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + if use_bincode { + params.push(("output", "bincode".to_string())) + } + + self.get_response( + &[ + routes::V2_API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "skimmed", + ], + ¶ms, + ) + .await + } + + #[instrument(level = "debug", skip(self))] + async fn get_expanded_nodes( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { let mut params = Vec::new(); if no_legacy { @@ -444,10 +667,10 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ - routes::API_VERSION, + routes::V2_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, - "skimmed", + "semi-skimmed", ], ¶ms, ) @@ -458,7 +681,7 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( - &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], + &[routes::V1_API_VERSION, routes::MIXNODES, routes::ACTIVE], NO_PARAMS, ) .await @@ -469,7 +692,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::MIXNODES, routes::ACTIVE, @@ -484,7 +707,7 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { self.get_json( - &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], + &[routes::V1_API_VERSION, routes::MIXNODES, routes::REWARDED], NO_PARAMS, ) .await @@ -498,7 +721,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::MIXNODE, &mix_id.to_string(), @@ -517,7 +740,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::GATEWAY, identity, @@ -536,7 +759,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::MIXNODE, &mix_id.to_string(), @@ -555,7 +778,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::GATEWAY, identity, @@ -573,7 +796,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result, NymAPIError> { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS, routes::MIXNODES, routes::REWARDED, @@ -594,7 +817,7 @@ pub trait NymApiClientExt: ApiClient { if let Some(since) = since { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::GATEWAY, identity, @@ -606,7 +829,7 @@ pub trait NymApiClientExt: ApiClient { } else { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::GATEWAY, identity, @@ -627,7 +850,7 @@ pub trait NymApiClientExt: ApiClient { if let Some(since) = since { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -639,7 +862,7 @@ pub trait NymApiClientExt: ApiClient { } else { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -659,7 +882,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -678,7 +901,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -698,7 +921,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -718,7 +941,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -738,7 +961,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -756,7 +979,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_PERFORMANCE, &node_id.to_string(), @@ -772,7 +995,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_ANNOTATION, &node_id.to_string(), @@ -786,7 +1009,7 @@ pub trait NymApiClientExt: ApiClient { async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, &mix_id.to_string(), @@ -801,7 +1024,11 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( - &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], + &[ + routes::V1_API_VERSION, + routes::MIXNODES, + routes::BLACKLISTED, + ], NO_PARAMS, ) .await @@ -811,7 +1038,11 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { self.get_json( - &[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED], + &[ + routes::V1_API_VERSION, + routes::GATEWAYS, + routes::BLACKLISTED, + ], NO_PARAMS, ) .await @@ -824,7 +1055,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_BLIND_SIGN, ], @@ -841,7 +1072,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::VERIFY_ECASH_TICKET, ], @@ -858,7 +1089,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::BATCH_REDEEM_ECASH_TICKETS, ], @@ -872,8 +1103,9 @@ pub trait NymApiClientExt: ApiClient { async fn partial_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { - let params = match expiration_date { + let mut params = match expiration_date { None => Vec::new(), Some(exp) => vec![( ecash::EXPIRATION_DATE_PARAM, @@ -881,9 +1113,13 @@ pub trait NymApiClientExt: ApiClient { )], }; + if let Some(epoch_id) = epoch_id { + params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string())); + } + self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::PARTIAL_EXPIRATION_DATE_SIGNATURES, ], @@ -904,7 +1140,7 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::PARTIAL_COIN_INDICES_SIGNATURES, ], @@ -917,8 +1153,9 @@ pub trait NymApiClientExt: ApiClient { async fn global_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { - let params = match expiration_date { + let mut params = match expiration_date { None => Vec::new(), Some(exp) => vec![( ecash::EXPIRATION_DATE_PARAM, @@ -926,9 +1163,13 @@ pub trait NymApiClientExt: ApiClient { )], }; + if let Some(epoch_id) = epoch_id { + params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string())); + } + self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::GLOBAL_EXPIRATION_DATE_SIGNATURES, ], @@ -949,7 +1190,7 @@ pub trait NymApiClientExt: ApiClient { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::GLOBAL_COIN_INDICES_SIGNATURES, ], @@ -969,7 +1210,7 @@ pub trait NymApiClientExt: ApiClient { }; self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, ecash::MASTER_VERIFICATION_KEY, ], @@ -985,7 +1226,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result<(), NymAPIError> { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::NYM_NODES_ROUTES, routes::NYM_NODES_REFRESH_DESCRIBED, ], @@ -1002,7 +1243,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_FOR, &expiration_date.to_string(), @@ -1019,7 +1260,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.get_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT, &expiration_date.to_string(), @@ -1036,7 +1277,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, ], @@ -1053,7 +1294,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, routes::ECASH_ROUTES, routes::ECASH_ISSUED_TICKETBOOKS_DATA, ], @@ -1069,7 +1310,7 @@ pub trait NymApiClientExt: ApiClient { ) -> Result { self.post_json( &[ - routes::API_VERSION, + routes::V1_API_VERSION, "unstable", routes::NYM_NODES_ROUTES, routes::nym_nodes::BY_ADDRESSES, @@ -1083,7 +1324,7 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_network_details(&self) -> Result { self.get_json( - &[routes::API_VERSION, routes::NETWORK, routes::DETAILS], + &[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS], NO_PARAMS, ) .await @@ -1092,7 +1333,40 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_chain_status(&self) -> Result { self.get_json( - &[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS], + &[ + routes::V1_API_VERSION, + routes::NETWORK, + routes::CHAIN_STATUS, + ], + NO_PARAMS, + ) + .await + } + + async fn get_chain_blocks_status(&self) -> Result { + self.get_json("/v1/network/chain-blocks-status", NO_PARAMS) + .await + } + + #[instrument(level = "debug", skip(self))] + async fn get_signer_status(&self) -> Result { + self.get_json("/v1/ecash/signer-status", NO_PARAMS).await + } + + #[instrument(level = "debug", skip(self))] + async fn get_signer_information(&self) -> Result { + self.get_json("/v1/api-status/signer-information", NO_PARAMS) + .await + } + + #[instrument(level = "debug", skip(self))] + async fn get_key_rotation_info(&self) -> Result { + self.get_json( + &[ + routes::V1_API_VERSION, + routes::EPOCH, + routes::KEY_ROTATION_INFO, + ], NO_PARAMS, ) .await diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index 320e9904ec..d0f93b5093 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -1,9 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_network_defaults::NYM_API_VERSION; - -pub const API_VERSION: &str = NYM_API_VERSION; +pub const V1_API_VERSION: &str = "v1"; +pub const V2_API_VERSION: &str = "v2"; pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; pub const DESCRIBED: &str = "described"; @@ -79,3 +78,11 @@ pub const SERVICE_PROVIDERS: &str = "services"; pub const DETAILS: &str = "details"; pub const CHAIN_STATUS: &str = "chain-status"; pub const NETWORK: &str = "network"; + +pub const EPOCH: &str = "epoch"; + +pub use epoch_routes::*; +pub mod epoch_routes { + pub const CURRENT: &str = "current"; + pub const KEY_ROTATION_INFO: &str = "key-rotation-info"; +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index 8674d7cb0a..49b5e56cbe 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -8,11 +8,11 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; use cosmwasm_std::Addr; +use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse}; use serde::Deserialize; use tracing::trace; -use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; pub use nym_coconut_dkg_common::{ dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse}, dealing::{ @@ -21,7 +21,9 @@ pub use nym_coconut_dkg_common::{ }, msg::QueryMsg as DkgQueryMsg, types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State}, - verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse}, + verification_key::{ + ContractVKShare, PagedVKSharesResponse, VerificationKeyShare, VkShareResponse, + }, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -41,6 +43,11 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_epoch_at_height(&self, height: u64) -> Result, NyxdError> { + let request = DkgQueryMsg::GetEpochStateAtHeight { height }; + self.query_dkg_contract(request).await + } + async fn can_advance_state(&self) -> Result { let request = DkgQueryMsg::CanAdvanceState {}; self.query_dkg_contract(request).await @@ -87,6 +94,34 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_epoch_dealers_paged( + &self, + epoch_id: EpochId, + start_after: Option, + limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetEpochDealers { + epoch_id, + start_after, + limit, + }; + self.query_dkg_contract(request).await + } + + async fn get_epoch_dealers_addresses_paged( + &self, + epoch_id: EpochId, + start_after: Option, + limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetEpochDealersAddresses { + epoch_id, + start_after, + limit, + }; + self.query_dkg_contract(request).await + } + async fn get_dealer_indices_paged( &self, start_after: Option, @@ -208,6 +243,20 @@ pub trait PagedDkgQueryClient: DkgQueryClient { collect_paged!(self, get_current_dealers_paged, dealers) } + async fn get_all_epoch_dealers( + &self, + epoch_id: EpochId, + ) -> Result, NyxdError> { + collect_paged!(self, get_epoch_dealers_paged, dealers, epoch_id) + } + + async fn get_all_epoch_dealers_addresses( + &self, + epoch_id: EpochId, + ) -> Result, NyxdError> { + collect_paged!(self, get_epoch_dealers_addresses_paged, dealers, epoch_id) + } + async fn get_all_dealer_indices(&self) -> Result, NyxdError> { collect_paged!(self, get_dealer_indices_paged, indices) } @@ -257,6 +306,9 @@ mod tests { match msg { DkgQueryMsg::GetState {} => client.get_state().ignore(), DkgQueryMsg::GetCurrentEpochState {} => client.get_current_epoch().ignore(), + DkgQueryMsg::GetEpochStateAtHeight { height } => { + client.get_epoch_at_height(height).ignore() + } DkgQueryMsg::CanAdvanceState {} => client.can_advance_state().ignore(), DkgQueryMsg::GetCurrentEpochThreshold {} => { client.get_current_epoch_threshold().ignore() @@ -276,6 +328,20 @@ mod tests { DkgQueryMsg::GetCurrentDealers { limit, start_after } => client .get_current_dealers_paged(start_after, limit) .ignore(), + QueryMsg::GetEpochDealers { + epoch_id, + limit, + start_after, + } => client + .get_epoch_dealers_paged(epoch_id, start_after, limit) + .ignore(), + QueryMsg::GetEpochDealersAddresses { + epoch_id, + limit, + start_after, + } => client + .get_epoch_dealers_addresses_paged(epoch_id, start_after, limit) + .ignore(), DkgQueryMsg::GetDealerIndices { limit, start_after } => { client.get_dealer_indices_paged(start_after, limit).ignore() } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 5060fe3df4..b7ae5b61e0 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -12,8 +12,8 @@ use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, Preassi use nym_mixnet_contract_common::nym_node::{ EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, - PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, - StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RewardedSetMetadata, Role, + RolesMetadataResponse, StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode, }; use nym_mixnet_contract_common::reward_params::WorkFactor; use nym_mixnet_contract_common::{ @@ -28,12 +28,12 @@ use nym_mixnet_contract_common::{ ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus, GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry, - IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails, - MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId, - NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse, - PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, - PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, - PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, + IdentityKey, IdentityKeyRef, IntervalEventId, KeyRotationIdResponse, KeyRotationState, + MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, + MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, + NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, + PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, RewardedSet, UnbondedMixnode, }; @@ -546,6 +546,16 @@ pub trait MixnetQueryClient { }) .await } + + async fn get_key_rotation_state(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationState {}) + .await + } + + async fn get_key_rotation_id(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationId {}) + .await + } } // extension trait to the query client to deal with the paged queries @@ -673,12 +683,20 @@ pub trait MixnetQueryClientExt: MixnetQueryClient { async fn get_rewarded_set(&self) -> Result { let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message)); + // bypass for catch 22 for fresh contracts. we can't refresh cache because there's no rewarded set, + // but we can't set the rewarded set because we didn't refresh the cache let metadata = self.get_rewarded_set_metadata().await?; - if !metadata.metadata.fully_assigned { + + let is_default = metadata.metadata == RewardedSetMetadata::default(); + if !metadata.metadata.fully_assigned && !is_default { return error_response("the rewarded set hasn't been fully assigned for this epoch"); } let expected_epoch_id = metadata.metadata.epoch_id; + if is_default { + return Ok(Default::default()); + } + // if we have to query those things more frequently, we could do it concurrently, // but as it stands now, it happens so infrequently it might as well be sequential let entry = self.get_role_assignment(Role::EntryGateway).await?; @@ -955,6 +973,8 @@ mod tests { QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client .get_nym_node_version_history_paged(start_after, limit) .ignore(), + QueryMsg::GetKeyRotationState {} => client.get_key_rotation_state().ignore(), + QueryMsg::GetKeyRotationId {} => client.get_key_rotation_id().ignore(), } } } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs index 8210c5b476..ad7db0991b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mod.rs @@ -13,6 +13,7 @@ pub mod ecash_query_client; pub mod group_query_client; pub mod mixnet_query_client; pub mod multisig_query_client; +pub mod performance_query_client; pub mod vesting_query_client; // signing clients @@ -21,6 +22,7 @@ pub mod ecash_signing_client; pub mod group_signing_client; pub mod mixnet_signing_client; pub mod multisig_signing_client; +pub mod performance_signing_client; pub mod vesting_signing_client; // re-export query traits @@ -29,6 +31,7 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient}; pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient}; pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient}; pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient}; +pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient}; pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient}; // re-export signing traits @@ -37,6 +40,7 @@ pub use ecash_signing_client::EcashSigningClient; pub use group_signing_client::GroupSigningClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_signing_client::MultisigSigningClient; +pub use performance_signing_client::PerformanceSigningClient; pub use vesting_signing_client::VestingSigningClient; // helper for providing blanket implementation for query clients @@ -44,6 +48,7 @@ pub trait NymContractsProvider { // main fn mixnet_contract_address(&self) -> Option<&AccountId>; fn vesting_contract_address(&self) -> Option<&AccountId>; + fn performance_contract_address(&self) -> Option<&AccountId>; // coconut-related fn ecash_contract_address(&self) -> Option<&AccountId>; @@ -56,6 +61,7 @@ pub trait NymContractsProvider { pub struct TypedNymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, + pub performance_contract_address: Option, pub ecash_contract_address: Option, pub group_contract_address: Option, @@ -76,6 +82,10 @@ impl TryFrom for TypedNymContracts { .vesting_contract_address .map(|addr| addr.parse()) .transpose()?, + performance_contract_address: value + .performance_contract_address + .map(|addr| addr.parse()) + .transpose()?, ecash_contract_address: value .ecash_contract_address .map(|addr| addr.parse()) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs new file mode 100644 index 0000000000..470a412e8d --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_query_client.rs @@ -0,0 +1,271 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::collect_paged; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::error::NyxdError; +use crate::nyxd::CosmWasmClient; +use async_trait::async_trait; +use cosmrs::AccountId; +use serde::Deserialize; + +pub use nym_performance_contract_common::{ + msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, EpochId, + EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, + NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PerformanceQueryClient { + async fn query_performance_contract( + &self, + query: PerformanceQueryMsg, + ) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn admin(&self) -> Result { + self.query_performance_contract(PerformanceQueryMsg::Admin {}) + .await + } + + async fn get_node_performance( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id }) + .await + } + + async fn get_node_performance_paged( + &self, + node_id: NodeId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + }) + .await + } + + async fn get_node_measurements( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id }) + .await + } + + async fn get_epoch_measurements_paged( + &self, + epoch_id: EpochId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + }) + .await + } + + async fn get_epoch_performance_paged( + &self, + epoch_id: EpochId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + }) + .await + } + + async fn get_full_historical_performance_paged( + &self, + start_after: Option<(EpochId, NodeId)>, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged { + start_after, + limit, + }) + .await + } + + async fn get_network_monitor( + &self, + address: &AccountId, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor { + address: address.to_string(), + }) + .await + } + + async fn get_network_monitors_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged { + start_after, + limit, + }) + .await + } + + async fn get_retired_network_monitors_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged { + start_after, + limit, + }) + .await + } + + async fn get_last_submission(&self) -> Result { + self.query_performance_contract(PerformanceQueryMsg::LastSubmittedMeasurement {}) + .await + } +} + +// extension trait to the query client to deal with the paged queries +// (it didn't feel appropriate to combine it with the existing trait +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PagedPerformanceQueryClient: PerformanceQueryClient { + async fn get_all_node_performance( + &self, + node_id: NodeId, + ) -> Result, NyxdError> { + collect_paged!(self, get_node_performance_paged, performance, node_id) + } + + async fn get_all_epoch_measurements( + &self, + node_id: NodeId, + ) -> Result, NyxdError> { + collect_paged!(self, get_epoch_measurements_paged, measurements, node_id) + } + + async fn get_all_epoch_performance( + &self, + epoch_id: EpochId, + ) -> Result, NyxdError> { + collect_paged!(self, get_epoch_performance_paged, performance, epoch_id) + } + + async fn get_all_full_historical_performance( + &self, + ) -> Result, NyxdError> { + collect_paged!(self, get_full_historical_performance_paged, performance) + } + + async fn get_all_network_monitors(&self) -> Result, NyxdError> { + collect_paged!(self, get_network_monitors_paged, info) + } + + async fn get_all_retired_network_monitors( + &self, + ) -> Result, NyxdError> { + collect_paged!(self, get_retired_network_monitors_paged, info) + } +} + +#[async_trait] +impl PagedPerformanceQueryClient for T where T: PerformanceQueryClient {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl PerformanceQueryClient for C +where + C: CosmWasmClient + NymContractsProvider + Send + Sync, +{ + async fn query_performance_contract( + &self, + query: PerformanceQueryMsg, + ) -> Result + where + for<'a> T: Deserialize<'a>, + { + let performance_contract_address = &self + .performance_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?; + self.query_contract_smart(performance_contract_address, &query) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::QueryMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_query_variants_are_covered( + client: C, + msg: PerformanceQueryMsg, + ) { + match msg { + PerformanceQueryMsg::Admin {} => client.admin().ignore(), + PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => { + client.get_node_performance(epoch_id, node_id).ignore() + } + PerformanceQueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + } => client + .get_node_performance_paged(node_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => { + client.get_node_measurements(epoch_id, node_id).ignore() + } + PerformanceQueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + } => client + .get_epoch_measurements_paged(epoch_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + } => client + .get_epoch_performance_paged(epoch_id, start_after, limit) + .ignore(), + PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client + .get_full_historical_performance_paged(start_after, limit) + .ignore(), + PerformanceQueryMsg::NetworkMonitor { address } => client + .get_network_monitor(&address.parse().unwrap()) + .ignore(), + PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client + .get_network_monitors_paged(start_after, limit) + .ignore(), + PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client + .get_retired_network_monitors_paged(start_after, limit) + .ignore(), + QueryMsg::LastSubmittedMeasurement {} => client.get_last_submission().ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs new file mode 100644 index 0000000000..78b960265a --- /dev/null +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs @@ -0,0 +1,217 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nyxd::coin::Coin; +use crate::nyxd::contract_traits::NymContractsProvider; +use crate::nyxd::cosmwasm_client::types::ExecuteResult; +use crate::nyxd::cosmwasm_client::ContractResponseData; +use crate::nyxd::error::NyxdError; +use crate::nyxd::{Fee, SigningCosmWasmClient}; +use crate::signing::signer::OfflineSigner; +use async_trait::async_trait; +use nym_performance_contract_common::{ + EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance, + RemoveEpochMeasurementsResponse, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait PerformanceSigningClient { + async fn execute_performance_contract( + &self, + fee: Option, + msg: PerformanceExecuteMsg, + memo: String, + funds: Vec, + ) -> Result; + + async fn update_admin( + &self, + admin: String, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::UpdateAdmin { admin }, + "PerformanceContract::UpdateAdmin".to_string(), + vec![], + ) + .await + } + + async fn submit_performance( + &self, + epoch: EpochId, + data: NodePerformance, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::Submit { epoch, data }, + "PerformanceContract::Submit".to_string(), + vec![], + ) + .await + } + + async fn batch_submit_performance( + &self, + epoch: EpochId, + data: Vec, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::BatchSubmit { epoch, data }, + "PerformanceContract::BatchSubmit".to_string(), + vec![], + ) + .await + } + + async fn authorise_network_monitor( + &self, + address: String, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::AuthoriseNetworkMonitor { address }, + "PerformanceContract::AuthoriseNetworkMonitor".to_string(), + vec![], + ) + .await + } + + async fn retire_network_monitor( + &self, + address: String, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RetireNetworkMonitor { address }, + "PerformanceContract::RetireNetworkMonitor".to_string(), + vec![], + ) + .await + } + + async fn remove_node_measurements( + &self, + epoch_id: EpochId, + node_id: NodeId, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id }, + "PerformanceContract::RemoveNodeMeasurements".to_string(), + vec![], + ) + .await + } + + async fn partial_remove_epoch_measurements( + &self, + epoch_id: EpochId, + fee: Option, + ) -> Result { + self.execute_performance_contract( + fee, + PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id }, + "PerformanceContract::RemoveEpochMeasurements".to_string(), + vec![], + ) + .await + } + + async fn remove_epoch_measurements( + &self, + epoch_id: EpochId, + fee: Option, + ) -> Result<(), NyxdError> { + loop { + let execute_res = self + .partial_remove_epoch_measurements(epoch_id, fee.clone()) + .await?; + let response = execute_res + .parse_singleton_json_contract_response::()?; + if !response.additional_entries_to_remove_remaining { + break; + } + } + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl PerformanceSigningClient for C +where + C: SigningCosmWasmClient + NymContractsProvider + Sync, + NyxdError: From<::Error>, +{ + async fn execute_performance_contract( + &self, + fee: Option, + msg: PerformanceExecuteMsg, + memo: String, + funds: Vec, + ) -> Result { + let performance_contract_address = &self + .performance_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?; + + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); + + let signer_address = &self.signer_addresses()?[0]; + self.execute( + signer_address, + performance_contract_address, + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_performance_contract_common::ExecuteMsg; + + // it's enough that this compiles and clippy is happy about it + #[allow(dead_code)] + fn all_execute_variants_are_covered( + client: C, + msg: PerformanceExecuteMsg, + ) { + match msg { + PerformanceExecuteMsg::UpdateAdmin { admin } => { + client.update_admin(admin, None).ignore() + } + PerformanceExecuteMsg::Submit { epoch, data } => { + client.submit_performance(epoch, data, None).ignore() + } + PerformanceExecuteMsg::BatchSubmit { epoch, data } => { + client.batch_submit_performance(epoch, data, None).ignore() + } + PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => { + client.authorise_network_monitor(address, None).ignore() + } + PerformanceExecuteMsg::RetireNetworkMonitor { address } => { + client.retire_network_monitor(address, None).ignore() + } + ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client + .remove_node_measurements(epoch_id, node_id, None) + .ignore(), + ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client + .partial_remove_epoch_measurements(epoch_id, None) + .ignore(), + }; + } +} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 0a13537643..80af295644 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -28,7 +28,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{ QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse, }; use cosmrs::tendermint::{block, chain, Hash}; -use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; +use cosmrs::{AccountId, Coin as CosmosCoin}; use prost::Message; use serde::{Deserialize, Serialize}; @@ -556,23 +556,12 @@ pub trait CosmWasmClient: TendermintRpcClient { Ok(serde_json::from_slice(&res.data)?) } - // deprecation warning is due to the fact the protobuf files built were based on cosmos-sdk 0.44, - // where they prefer using tx_bytes directly. However, in 0.42, which we are using at the time - // of writing this, the option does not work - // TODO: we should really stop using the `tx` argument here and use `tx_bytes` exlusively, - // however, at the time of writing this update, while our QA and mainnet networks do support it, - // sandbox is still running old version of wasmd that lacks support for `tx_bytes` - #[allow(deprecated)] - async fn query_simulate( - &self, - tx: Option, - tx_bytes: Vec, - ) -> Result { + async fn query_simulate(&self, tx_bytes: Vec) -> Result { let path = Some("/cosmos.tx.v1beta1.Service/Simulate".to_owned()); let req = SimulateRequest { - tx: tx.map(Into::into), tx_bytes, + ..Default::default() }; let res = self diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index cec29e9c50..e59046af7b 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -81,17 +81,14 @@ where auth_info: single_unspecified_signer_auth(public_key, sequence_response.sequence), signatures: vec![Vec::new()], }; - self.query_simulate(Some(partial_tx), Vec::new()).await - // for completion sake, once we're able to transition into using `tx_bytes`, - // we might want to use something like this instead: - // let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw { - // body_bytes: partial_tx.body.into_bytes().unwrap(), - // auth_info_bytes: partial_tx.auth_info.into_bytes().unwrap(), - // signatures: partial_tx.signatures, - // } - // .into(); - // self.query_simulate(None, tx_raw.to_bytes().unwrap()).await + let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw { + body_bytes: partial_tx.body.into_bytes()?, + auth_info_bytes: partial_tx.auth_info.into_bytes()?, + signatures: partial_tx.signatures, + } + .into(); + self.query_simulate(tx_raw.to_bytes()?).await } async fn upload( diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 0c4c6c8dcb..e948dc9812 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -12,6 +12,8 @@ use tendermint_rpc::endpoint::broadcast; use tracing::error; pub use cosmrs::abci::MsgResponse; +use cosmwasm_std::from_json; +use serde::de::DeserializeOwned; pub fn parse_singleton_u32_from_contract_response(b: Vec) -> Result { if b.len() != 4 { @@ -73,6 +75,11 @@ pub fn parse_msg_responses(data: Bytes) -> Vec { // requires there's a single response message pub trait ContractResponseData: Sized { + fn parse_singleton_json_contract_response(&self) -> Result { + let b = self.to_singleton_contract_data()?; + from_json(&b).map_err(|err| err.into()) + } + fn parse_singleton_u32_contract_data(&self) -> Result { let b = self.to_singleton_contract_data()?; parse_singleton_u32_from_contract_response(b) diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 03746e305e..85b08ab12a 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -138,6 +138,24 @@ impl NyxdClient { config, }) } + + pub fn connect_with_network_details( + endpoint: U, + network_details: NymNetworkDetails, + ) -> Result + where + U: TryInto, + { + let config = Config::try_from_nym_network_details(&network_details)?; + Self::connect(config, endpoint) + } + + pub fn connect_to_default_env(endpoint: U) -> Result + where + U: TryInto, + { + Self::connect_with_network_details(endpoint, NymNetworkDetails::new_from_env()) + } } impl NyxdClient { @@ -268,6 +286,10 @@ impl NymContractsProvider for NyxdClient { self.config.contracts.vesting_contract_address.as_ref() } + fn performance_contract_address(&self) -> Option<&AccountId> { + self.config.contracts.performance_contract_address.as_ref() + } + fn ecash_contract_address(&self) -> Option<&AccountId> { self.config.contracts.ecash_contract_address.as_ref() } diff --git a/common/commands/src/ecash/generate_ticket.rs b/common/commands/src/ecash/generate_ticket.rs index 0da2a878c4..4752c9211c 100644 --- a/common/commands/src/ecash/generate_ticket.rs +++ b/common/commands/src/ecash/generate_ticket.rs @@ -86,7 +86,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { anyhow!("ticketbook got incorrectly imported - the master verification key is missing") })?; let expiration_signatures = persistent_storage - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await? .ok_or_else(|| { anyhow!( diff --git a/common/commands/src/ecash/issue_ticket_book.rs b/common/commands/src/ecash/issue_ticket_book.rs index e57cb579dc..fae59ad759 100644 --- a/common/commands/src/ecash/issue_ticket_book.rs +++ b/common/commands/src/ecash/issue_ticket_book.rs @@ -120,7 +120,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()> if args.include_expiration_date_signatures { let signatures = credentials_store - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await? .ok_or(anyhow!("missing expiration date signatures!"))?; diff --git a/common/commands/src/internal/mod.rs b/common/commands/src/internal/mod.rs index 44fa4d2659..fc929a2e4e 100644 --- a/common/commands/src/internal/mod.rs +++ b/common/commands/src/internal/mod.rs @@ -4,6 +4,7 @@ use clap::{Args, Subcommand}; pub mod ecash; +pub mod nyx; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] @@ -16,4 +17,6 @@ pub struct Internal { pub enum InternalCommands { /// Ecash related internal commands Ecash(ecash::InternalEcash), + + Nyx(nyx::InternalNyx), } diff --git a/common/commands/src/internal/nyx/force_advance_epoch.rs b/common/commands/src/internal/nyx/force_advance_epoch.rs new file mode 100644 index 0000000000..3c797ff01c --- /dev/null +++ b/common/commands/src/internal/nyx/force_advance_epoch.rs @@ -0,0 +1,116 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use anyhow::bail; +use clap::Parser; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::reward_params::NodeRewardingParameters; +use nym_mixnet_contract_common::{ + EpochRewardedSet, EpochState, NodeId, RewardingParams, RoleAssignment, +}; +use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use rand::prelude::*; +use rand::thread_rng; + +#[derive(Debug, Parser)] +pub struct Args {} + +fn choose_new_nodes( + params: &RewardingParams, + rewarded_set: &EpochRewardedSet, + role: Role, +) -> Vec { + let mut rng = thread_rng(); + + match role { + Role::EntryGateway => rewarded_set + .assignment + .entry_gateways + .choose_multiple(&mut rng, params.rewarded_set.entry_gateways as usize) + .copied() + .collect(), + Role::Layer1 => rewarded_set + .assignment + .layer1 + .choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3) + .copied() + .collect(), + Role::Layer2 => rewarded_set + .assignment + .layer2 + .choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3) + .copied() + .collect(), + Role::Layer3 => rewarded_set + .assignment + .layer3 + .choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3) + .copied() + .collect(), + Role::ExitGateway => rewarded_set + .assignment + .exit_gateways + .choose_multiple(&mut rng, params.rewarded_set.exit_gateways as usize) + .copied() + .collect(), + Role::Standby => rewarded_set + .assignment + .standby + .choose_multiple(&mut rng, params.rewarded_set.standby as usize) + .copied() + .collect(), + } +} + +pub async fn force_advance_epoch(_: Args, client: SigningClient) -> anyhow::Result<()> { + let current_epoch = client.get_current_interval_details().await?; + let epoch_status = client.get_current_epoch_status().await?; + if epoch_status.being_advanced_by.as_str() != client.address().to_string() { + bail!( + "this client is not authorised to perform any epoch operations. we need {}", + client.address() + ) + } + + let rewarding_params = client.get_rewarding_parameters().await?; + let current_rewarded_set = client.get_rewarded_set().await?; + + if !current_epoch.is_current_epoch_over { + println!("the current epoch is not over yet - there's nothing to do") + } + + // is this most efficient? no. but it's simple + loop { + let epoch_status = client.get_current_epoch_status().await?; + + match epoch_status.state { + EpochState::InProgress => break, + EpochState::Rewarding { final_node_id, .. } => { + println!("rewarding {final_node_id} with big fat 0..."); + client + .reward_node( + final_node_id, + NodeRewardingParameters::new(Default::default(), Default::default()), + None, + ) + .await?; + } + EpochState::ReconcilingEvents => { + println!("trying to reconcile events..."); + client.reconcile_epoch_events(None, None).await?; + } + EpochState::RoleAssignment { next } => { + let nodes = choose_new_nodes(&rewarding_params, ¤t_rewarded_set, next); + println!("assigning {nodes:?} as {next}"); + + client + .assign_roles(RoleAssignment { role: next, nodes }, None) + .await?; + } + } + } + + Ok(()) +} diff --git a/common/commands/src/internal/nyx/mod.rs b/common/commands/src/internal/nyx/mod.rs new file mode 100644 index 0000000000..0ffb24f3a3 --- /dev/null +++ b/common/commands/src/internal/nyx/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod force_advance_epoch; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct InternalNyx { + #[clap(subcommand)] + pub command: InternalNyxCommands, +} + +#[derive(Debug, Subcommand)] +pub enum InternalNyxCommands { + /// Attempt to force advance the current epoch + ForceAdvanceEpoch(force_advance_epoch::Args), +} diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 0d6e40ffbc..4223263e5a 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -49,14 +49,14 @@ pub fn show_error(e: E) where E: Display, { - error!("{}", e); + error!("{e}"); } pub fn show_error_passthrough(e: E) -> E where E: Error + Display, { - error!("{}", e); + error!("{e}"); e } diff --git a/common/commands/src/validator/account/balance.rs b/common/commands/src/validator/account/balance.rs index 4e109ad7b3..26ebd86a52 100644 --- a/common/commands/src/validator/account/balance.rs +++ b/common/commands/src/validator/account/balance.rs @@ -42,7 +42,7 @@ pub async fn query_balance( .address .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); - info!("Getting balance for {}...", address); + info!("Getting balance for {address}..."); match client.get_all_balances(&address).await { Ok(coins) => { diff --git a/common/commands/src/validator/account/pubkey.rs b/common/commands/src/validator/account/pubkey.rs index 6a7c3383d6..ed419c1101 100644 --- a/common/commands/src/validator/account/pubkey.rs +++ b/common/commands/src/validator/account/pubkey.rs @@ -57,17 +57,17 @@ pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip3 println!("{}", account.public_key().to_string()); } None => { - error!("Could not derive key that matches {}", address) + error!("Could not derive key that matches {address}") } }, Err(e) => { - error!("Failed to derive accounts. {}", e); + error!("Failed to derive accounts. {e}"); } } } pub async fn get_pubkey_from_chain(address: AccountId, client: &QueryClient) { - info!("Getting public key for address {} from chain...", address); + info!("Getting public key for address {address} from chain..."); match client.get_account(&address).await { Ok(Some(account)) => { if let Ok(base_account) = account.try_get_base_account() { diff --git a/common/commands/src/validator/account/send.rs b/common/commands/src/validator/account/send.rs index 7c5affd841..e02e649659 100644 --- a/common/commands/src/validator/account/send.rs +++ b/common/commands/src/validator/account/send.rs @@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) { "Nodesguru: https://nym.explorers.guru/transaction/{}", &res.hash ); - println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash); + println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash); println!("Transaction result code: {}", &res.tx_result.code.value()); println!("Transaction hash: {}", &res.hash); } diff --git a/common/commands/src/validator/account/send_multiple.rs b/common/commands/src/validator/account/send_multiple.rs index f19b1b6760..762efdb8ab 100644 --- a/common/commands/src/validator/account/send_multiple.rs +++ b/common/commands/src/validator/account/send_multiple.rs @@ -37,7 +37,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) { let rows = InputFileReader::new(&args.input); if let Err(e) = rows { - error!("Failed to read input file: {}", e); + error!("Failed to read input file: {e}"); return; } let rows = rows.unwrap(); @@ -67,7 +67,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) { .prompt(); if let Err(e) = ans { - info!("Aborting, {}...", e); + info!("Aborting, {e}..."); return; } if let Ok(false) = ans { @@ -95,18 +95,15 @@ pub async fn send_multiple(args: Args, client: &SigningClient) { "Nodesguru: https://nym.explorers.guru/transaction/{}", &res.hash ); - println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash); + println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash); println!("Transaction result code: {}", &res.tx_result.code.value()); println!("Transaction hash: {}", &res.hash); if let Some(output_filename) = args.output { - println!("\nWriting output log to {}", output_filename); + println!("\nWriting output log to {output_filename}"); if let Err(e) = write_output_file(rows, res, &output_filename) { - error!( - "Failed to write output file {} with error {}", - output_filename, e - ); + error!("Failed to write output file {output_filename} with error {e}"); } } } @@ -136,7 +133,7 @@ fn write_output_file( .collect::>() .join("\n"); - Ok(file.write_all(format!("{}\n", data).as_bytes())?) + Ok(file.write_all(format!("{data}\n").as_bytes())?) } #[derive(Debug)] @@ -171,7 +168,7 @@ impl InputFileReader { // multiply when a whole token amount, e.g. 50nym (50.123456nym is not allowed, that must be input as 50123456unym) let (amount, denom) = if !denom.starts_with('u') { - (amount * 1_000_000u128, format!("u{}", denom)) + (amount * 1_000_000u128, format!("u{denom}")) } else { (amount, denom) }; diff --git a/common/commands/src/validator/cosmwasm/execute_contract.rs b/common/commands/src/validator/cosmwasm/execute_contract.rs index 0dfc547430..e5faa5cd03 100644 --- a/common/commands/src/validator/cosmwasm/execute_contract.rs +++ b/common/commands/src/validator/cosmwasm/execute_contract.rs @@ -55,6 +55,6 @@ pub async fn execute(args: Args, client: SigningClient) { .await { Ok(res) => info!("SUCCESS ✅\n{}", json!(res)), - Err(e) => error!("FAILURE ❌\n{}", e), + Err(e) => error!("FAILURE ❌\n{e}"), } } diff --git a/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs b/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs index 3a80ff4ca5..5f1b57b019 100644 --- a/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs +++ b/common/commands/src/validator/cosmwasm/generators/coconut_dkg.rs @@ -43,7 +43,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let multisig_addr = args.multisig_addr.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) @@ -97,7 +97,7 @@ pub async fn generate(args: Args) { key_size: DEFAULT_DEALINGS as u32, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs index f13bb65151..52244d761b 100644 --- a/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs +++ b/common/commands/src/validator/cosmwasm/generators/ecash_bandwidth.rs @@ -28,7 +28,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let group_addr = args.group_addr.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::GROUP_CONTRACT_ADDRESS) @@ -51,7 +51,7 @@ pub async fn generate(args: Args) { deposit_amount: args.deposit_amount, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index a72c00b662..9226a02653 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -88,7 +88,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate mixnet contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let initial_rewarding_params = InitialRewardingParams { initial_reward_pool: Decimal::from_atomics(args.initial_reward_pool, 0) @@ -114,7 +114,7 @@ pub async fn generate(args: Args) { }, }; - debug!("initial_rewarding_params: {:?}", initial_rewarding_params); + debug!("initial_rewarding_params: {initial_rewarding_params:?}"); let rewarding_validator_address = args.rewarding_validator_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::REWARDING_VALIDATOR_ADDRESS) @@ -157,9 +157,10 @@ pub async fn generate(args: Args) { minimum: args.minimum_interval_operating_cost.amount.into(), maximum: args.maximum_interval_operating_cost.amount.into(), }, + key_validity_in_epochs: None, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/multisig.rs b/common/commands/src/validator/cosmwasm/generators/multisig.rs index 90abae9e28..8b0b79e4fe 100644 --- a/common/commands/src/validator/cosmwasm/generators/multisig.rs +++ b/common/commands/src/validator/cosmwasm/generators/multisig.rs @@ -31,7 +31,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let ecash_contract_address = args.ecash_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::ECASH_CONTRACT_ADDRESS) @@ -60,7 +60,7 @@ pub async fn generate(args: Args) { coconut_dkg_contract_address: coconut_dkg_contract_address.to_string(), }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/generators/vesting.rs b/common/commands/src/validator/cosmwasm/generators/vesting.rs index 94f5cda0b2..536520fa9c 100644 --- a/common/commands/src/validator/cosmwasm/generators/vesting.rs +++ b/common/commands/src/validator/cosmwasm/generators/vesting.rs @@ -21,7 +21,7 @@ pub struct Args { pub async fn generate(args: Args) { info!("Starting to generate vesting contract instantiate msg"); - debug!("Received arguments: {:?}", args); + debug!("Received arguments: {args:?}"); let mixnet_contract_address = args.mixnet_contract_address.unwrap_or_else(|| { let address = std::env::var(nym_network_defaults::var_names::MIXNET_CONTRACT_ADDRESS) @@ -39,7 +39,7 @@ pub async fn generate(args: Args) { mix_denom, }; - debug!("instantiate_msg: {:?}", instantiate_msg); + debug!("instantiate_msg: {instantiate_msg:?}"); let res = serde_json::to_string(&instantiate_msg).expect("failed to convert instantiate msg to json"); diff --git a/common/commands/src/validator/cosmwasm/init_contract.rs b/common/commands/src/validator/cosmwasm/init_contract.rs index 430440be44..ca239d8f1a 100644 --- a/common/commands/src/validator/cosmwasm/init_contract.rs +++ b/common/commands/src/validator/cosmwasm/init_contract.rs @@ -72,7 +72,7 @@ pub async fn init(args: Args, client: SigningClient, network_details: &NymNetwor .await .expect("failed to instantiate the contract!"); - info!("Init result: {:?}", res); + info!("Init result: {res:?}"); println!("{}", res.contract_address) } diff --git a/common/commands/src/validator/cosmwasm/migrate_contract.rs b/common/commands/src/validator/cosmwasm/migrate_contract.rs index f106ae8b17..2dd38ccdd8 100644 --- a/common/commands/src/validator/cosmwasm/migrate_contract.rs +++ b/common/commands/src/validator/cosmwasm/migrate_contract.rs @@ -47,5 +47,5 @@ pub async fn migrate(args: Args, client: SigningClient) { .expect("failed to migrate the contract!") }; - info!("Migrate result: {:?}", res); + info!("Migrate result: {res:?}"); } diff --git a/common/commands/src/validator/cosmwasm/upload_contract.rs b/common/commands/src/validator/cosmwasm/upload_contract.rs index 96d01741ee..5a17b66013 100644 --- a/common/commands/src/validator/cosmwasm/upload_contract.rs +++ b/common/commands/src/validator/cosmwasm/upload_contract.rs @@ -31,7 +31,7 @@ pub async fn upload(args: Args, client: SigningClient) { .await .expect("failed to upload the contract!"); - info!("Upload result: {:?}", res); + info!("Upload result: {res:?}"); println!("{}", res.code_id) } diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index dc8a2d591f..f3b99513f7 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -47,5 +47,5 @@ pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { .await .expect("failed to delegate to mixnode!"); - info!("delegating to mixnode: {:?}", res); + info!("delegating to mixnode: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs index 96bf95be04..79dc4347a0 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs @@ -196,7 +196,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { let records = match InputFileReader::new(&args.input) { Ok(records) => records, Err(e) => { - println!("Error reading input file: {}", e); + println!("Error reading input file: {e}"); return; } }; @@ -241,7 +241,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { let node_id = row.node_id.clone().parse::().unwrap(); let coins: Vec = vec![]; undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins)); - undelegation_table.add_row(&[row.node_id.clone()]); + undelegation_table.add_row(std::slice::from_ref(&row.node_id)); if row.amount.amount > 0 { delegation_msgs @@ -262,11 +262,11 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { } if !undelegation_msgs.is_empty() { - println!("Undelegation records : \n{}\n\n", undelegation_table); + println!("Undelegation records : \n{undelegation_table}\n\n"); } if !delegation_msgs.is_empty() { - println!("Delegation records : \n{}\n\n", delegation_table); + println!("Delegation records : \n{delegation_table}\n\n"); } let ans = inquire::Confirm::new("Do you want to continue with the shown operations?") @@ -275,7 +275,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { .prompt(); if let Err(e) = ans { - info!("Aborting, {}...", e); + info!("Aborting, {e}..."); return; } @@ -348,7 +348,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { if args.output.is_some() { if let Err(e) = write_to_csv(output_details, args.output) { - info!("Failed to write to CSV, {}", e); + info!("Failed to write to CSV, {e}"); } } } diff --git a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs index 8ac1ab9c07..66a2a5d659 100644 --- a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -38,5 +38,5 @@ pub async fn migrate_vested_delegation(args: Args, client: SigningClient) { .await .expect("failed to migrate delegation!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index 83426a316e..8b876831af 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -40,5 +40,5 @@ pub async fn claim_delegator_reward(args: Args, client: SigningClient) { .await .expect("failed to claim delegator-reward"); - info!("Claiming delegator reward: {:?}", res) + info!("Claiming delegator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index d36c4a7b17..b0e9df21bd 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -40,5 +40,5 @@ pub async fn vesting_claim_delegator_reward(args: Args, client: SigningClient) { .await .expect("failed to claim vesting delegator-reward"); - info!("Claiming vesting delegator reward: {:?}", res) + info!("Claiming vesting delegator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index 19593ed5ce..edcd646e90 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -40,5 +40,5 @@ pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { .await .expect("failed to remove stake from mixnode!"); - info!("removing stake from mixnode: {:?}", res) + info!("removing stake from mixnode: {res:?}") } diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index 3fa3fd7cee..28351541f5 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -53,5 +53,5 @@ pub async fn vesting_delegate_to_mixnode(args: Args, client: SigningClient) { .await .expect("failed to delegate to mixnode!"); - info!("vesting delegating to mixnode: {:?}", res); + info!("vesting delegating to mixnode: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 9daf8691d3..c02f87e055 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -45,5 +45,5 @@ pub async fn vesting_undelegate_from_mixnode(args: Args, client: SigningClient) .await .expect("failed to remove stake from vesting account on mixnode!"); - info!("removing stake from vesting mixnode: {:?}", res) + info!("removing stake from vesting mixnode: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs index 28fe160070..c11246c46e 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs @@ -73,5 +73,5 @@ pub async fn bond_gateway(args: Args, client: SigningClient) { .await .expect("failed to bond gateway!"); - info!("Bonding result: {:?}", res) + info!("Bonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs index a6b22a2d4b..c1189f1885 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs @@ -52,5 +52,5 @@ pub async fn migrate_to_nymnode(args: Args, client: SigningClient) { .await .expect("failed to migrate gateway!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs index c0cf879147..a1b5a8d4bd 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/settings/update_config.rs @@ -56,5 +56,5 @@ pub async fn update_config(args: Args, client: SigningClient) { .await .expect("updating gateway config"); - info!("gateway config updated: {:?}", res) + info!("gateway config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs b/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs index 346b652b2f..8ebc9279bf 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/settings/vesting_update_config.rs @@ -57,5 +57,5 @@ pub async fn vesting_update_config(args: Args, client: SigningClient) { .await .expect("updating vesting gateway config"); - info!("gateway config updated: {:?}", res) + info!("gateway config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs index af9c6c8bb4..cef6dbcf7f 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs @@ -17,5 +17,5 @@ pub async fn unbond_gateway(client: SigningClient) { .await .expect("failed to unbond gateway!"); - info!("Unbonding result: {:?}", res) + info!("Unbonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs index 91c2817be8..4f5f63d16b 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs @@ -73,5 +73,5 @@ pub async fn vesting_bond_gateway(args: Args, client: SigningClient) { .await .expect("failed to bond gateway!"); - info!("Vesting bonding gateway result: {:?}", res) + info!("Vesting bonding gateway result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs index 1c4312424c..72b9357dab 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs @@ -17,5 +17,5 @@ pub async fn vesting_unbond_gateway(client: SigningClient) { .await .expect("failed to unbond vesting gateway!"); - info!("Unbonding vesting result: {:?}", res) + info!("Unbonding vesting result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index 410ce91d28..b8de001f2d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -106,5 +106,5 @@ pub async fn bond_mixnode(args: Args, client: SigningClient) { .await .expect("failed to bond mixnode!"); - info!("Bonding result: {:?}", res) + info!("Bonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs index 556021dcf9..bd96aeb270 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/decrease_pledge.rs @@ -25,5 +25,5 @@ pub async fn decrease_pledge(args: Args, client: SigningClient) { .await .expect("failed to decrease pledge!"); - info!("decreasing pledge: {:?}", res); + info!("decreasing pledge: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs index 95bd9d0573..ae2ec7395d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs @@ -15,5 +15,5 @@ pub async fn migrate_vested_mixnode(_args: Args, client: SigningClient) { .await .expect("failed to migrate mixnode!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs index fe46e2c11a..3f736c86f4 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs @@ -15,5 +15,5 @@ pub async fn migrate_to_nymnode(_args: Args, client: SigningClient) { .await .expect("failed to migrate mixnode!"); - info!("migration result: {:?}", res) + info!("migration result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs b/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs index 1989725907..51ba9daa4d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/pledge_more.rs @@ -25,5 +25,5 @@ pub async fn pledge_more(args: Args, client: SigningClient) { .await .expect("failed to pledge more!"); - info!("pledging more: {:?}", res); + info!("pledging more: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs index a5f8d65236..f3e97d0df2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs @@ -17,5 +17,5 @@ pub async fn claim_operator_reward(_args: Args, client: SigningClient) { .await .expect("failed to claim operator reward"); - info!("Claiming operator reward: {:?}", res) + info!("Claiming operator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs index 3b88c6cf5c..38c2ccf41c 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs @@ -20,5 +20,5 @@ pub async fn vesting_claim_operator_reward(client: SigningClient) { .await .expect("failed to claim vesting operator reward"); - info!("Claiming vesting operator reward: {:?}", res) + info!("Claiming vesting operator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs index 733dea04a1..31db3435b2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs @@ -64,5 +64,5 @@ pub async fn update_config(args: Args, client: SigningClient) { .await .expect("updating mix-node config"); - info!("mixnode config updated: {:?}", res) + info!("mixnode config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs index a495c8f4b5..092aa54d82 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs @@ -65,5 +65,5 @@ pub async fn vesting_update_config(client: SigningClient, args: Args) { .await .expect("updating vesting mix-node config"); - info!("mixnode config updated: {:?}", res) + info!("mixnode config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs index fe6f3df223..bf6864ef5e 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs @@ -18,5 +18,5 @@ pub async fn unbond_mixnode(_args: Args, client: SigningClient) { .await .expect("failed to unbond mixnode!"); - info!("Unbonding result: {:?}", res) + info!("Unbonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index d55d6463e7..6813ea2dfd 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -106,5 +106,5 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str .await .expect("failed to bond vesting mixnode!"); - info!("Bonding vesting result: {:?}", res) + info!("Bonding vesting result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs index bb83e1ffaa..1ffb1de360 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_decrease_pledge.rs @@ -25,5 +25,5 @@ pub async fn vesting_decrease_pledge(args: Args, client: SigningClient) { .await .expect("failed to vesting decrease pledge!"); - info!("vesting decreasing pledge: {:?}", res); + info!("vesting decreasing pledge: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs index 9917e02860..e53c9b9cfe 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_pledge_more.rs @@ -26,5 +26,5 @@ pub async fn vesting_pledge_more(args: Args, client: SigningClient) { .await .expect("failed to pledge more!"); - info!("vesting pledge more: {:?}", res); + info!("vesting pledge more: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs index 3e63846430..b3d52bdb73 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs @@ -20,5 +20,5 @@ pub async fn vesting_unbond_mixnode(client: SigningClient) { .await .expect("failed to unbond vesting mixnode!"); - info!("Unbonding vesting result: {:?}", res) + info!("Unbonding vesting result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs index acde3d39e3..145e245863 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs @@ -85,5 +85,5 @@ pub async fn bond_nymnode(args: Args, client: SigningClient) { .await .expect("failed to bond nymnode!"); - info!("Bonding result: {:?}", res) + info!("Bonding result: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs index a299ddbe65..4bb20b4a66 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs @@ -25,5 +25,5 @@ pub async fn decrease_pledge(args: Args, client: SigningClient) { .await .expect("failed to decrease pledge!"); - info!("decreasing pledge: {:?}", res); + info!("decreasing pledge: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs index 09b94bc5d5..31a0eb7a8d 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs @@ -25,5 +25,5 @@ pub async fn increase_pledge(args: Args, client: SigningClient) { .await .expect("failed to pledge more!"); - info!("pledging more: {:?}", res); + info!("pledging more: {res:?}"); } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs index a8f157f661..005e81f069 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs @@ -17,5 +17,5 @@ pub async fn claim_operator_reward(_args: Args, client: SigningClient) { .await .expect("failed to claim operator reward"); - info!("Claiming operator reward: {:?}", res) + info!("Claiming operator reward: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs index 5ba5bf52fa..fb59924c9e 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs @@ -46,5 +46,5 @@ pub async fn update_config(args: Args, client: SigningClient) { .await .expect("updating nym node config"); - info!("nym node config updated: {:?}", res) + info!("nym node config updated: {res:?}") } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs index 5668c92e0d..a513067a02 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs @@ -68,6 +68,6 @@ pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Re .await .expect("failed to update cost params"); - info!("Cost params result: {:?}", res); + info!("Cost params result: {res:?}"); Ok(()) } diff --git a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs index fbcef6bfd7..d60266a12d 100644 --- a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs +++ b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs @@ -18,5 +18,5 @@ pub async fn unbond_nymnode(_args: Args, client: SigningClient) { .await .expect("failed to unbond Nym Node!"); - info!("Unbonding result: {:?}", res) + info!("Unbonding result: {res:?}") } diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs index 424dfd184b..7df1d12eb6 100644 --- a/common/commands/src/validator/signature/sign.rs +++ b/common/commands/src/validator/signature/sign.rs @@ -52,7 +52,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option) { println!("{}", json!(output)); } Err(e) => { - error!("Failed to sign message. {}", e); + error!("Failed to sign message. {e}"); } } } @@ -61,7 +61,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option) { } }, Err(e) => { - error!("Failed to derive accounts. {}", e); + error!("Failed to derive accounts. {e}"); } } } diff --git a/common/commands/src/validator/signature/verify.rs b/common/commands/src/validator/signature/verify.rs index 391c5105d8..7168b75b92 100644 --- a/common/commands/src/validator/signature/verify.rs +++ b/common/commands/src/validator/signature/verify.rs @@ -38,7 +38,7 @@ pub async fn verify(args: Args, client: &QueryClient) { let public_key = match AccountId::from_str(&args.public_key_or_address) { Ok(address) => { - info!("Found account address instead of public key, so looking up public key for {} from chain", address); + info!("Found account address instead of public key, so looking up public key for {address} from chain"); match client.get_account_public_key(&address).await.ok() { Some(public_key) => { if let Some(k) = public_key { @@ -48,8 +48,7 @@ pub async fn verify(args: Args, client: &QueryClient) { } None => { error!( - "Address {} does not have a public key recorded on the chain. This is probably because the account has never signed a transaction.", - address + "Address {address} does not have a public key recorded on the chain. This is probably because the account has never signed a transaction." ); None } @@ -58,7 +57,7 @@ pub async fn verify(args: Args, client: &QueryClient) { Err(_) => match PublicKey::from_json(&args.public_key_or_address) { Ok(parsed) => Some(parsed), Err(e) => { - error!("Public key should be JSON. Unable to parse: {}", e); + error!("Public key should be JSON. Unable to parse: {e}"); None } }, @@ -78,7 +77,7 @@ pub async fn verify(args: Args, client: &QueryClient) { ) { Ok(()) => println!("SUCCESS ✅ signature verified"), Err(e) => { - error!("FAILURE ❌ Signature verification failed: {}", e); + error!("FAILURE ❌ Signature verification failed: {e}"); } } } diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs index 35ee669dab..b166202af0 100644 --- a/common/commands/src/validator/vesting/create_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -86,6 +86,6 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw .await .unwrap(); - info!("Vesting result: {:?}", res); - info!("Coin send result: {:?}", send_coin_response); + info!("Vesting result: {res:?}"); + info!("Coin send result: {send_coin_response:?}"); } diff --git a/common/commands/src/validator/vesting/query_vesting_schedule.rs b/common/commands/src/validator/vesting/query_vesting_schedule.rs index 0baca858f1..df3736b94d 100644 --- a/common/commands/src/validator/vesting/query_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/query_vesting_schedule.rs @@ -29,7 +29,7 @@ pub async fn query(args: Args, client: QueryClient, address_from_mnemonic: Optio .address .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); - info!("Checking account {} for a vesting schedule...", account_id); + info!("Checking account {account_id} for a vesting schedule..."); let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/common/commands/src/validator/vesting/withdraw_vested.rs b/common/commands/src/validator/vesting/withdraw_vested.rs index 2c2bf254b9..b0a1c0ab1f 100644 --- a/common/commands/src/validator/vesting/withdraw_vested.rs +++ b/common/commands/src/validator/vesting/withdraw_vested.rs @@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) { &res.transaction_hash ); println!( - "Mintscan: https://www.mintscan.io/nyx/txs/{}", + "Mintscan: https://ping.pub/nyx/tx/{}", &res.transaction_hash ); println!("Transaction hash: {}", &res.transaction_hash); diff --git a/common/config/src/legacy_helpers.rs b/common/config/src/legacy_helpers.rs index 315b6c2b49..58c938a7fe 100644 --- a/common/config/src/legacy_helpers.rs +++ b/common/config/src/legacy_helpers.rs @@ -48,8 +48,7 @@ pub mod nym_config { log::trace!("Loading from file: {:#?}", filepath.as_ref().to_owned()); let config_contents = fs::read_to_string(filepath)?; - toml::from_str(&config_contents) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + toml::from_str(&config_contents).map_err(io::Error::other) } } } diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index 42dcea8610..78d72bc7f7 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -151,8 +151,7 @@ where let content = fs::read_to_string(path)?; // TODO: should we be preserving original error type instead? - deserialize_config_from_toml_str(&content) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + deserialize_config_from_toml_str(&content).map_err(io::Error::other) } // diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs index 2c0b1746eb..2b74c0c7a7 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -55,6 +55,14 @@ impl DealerDetailsResponse { } } +#[cw_serde] +pub struct PagedDealerAddressesResponse { + pub dealers: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + #[cw_serde] pub struct PagedDealerResponse { pub dealers: Vec, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index f9d1dc0bb0..151fb2f8bc 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -12,8 +12,8 @@ use cosmwasm_schema::cw_serde; #[cfg(feature = "schema")] use crate::{ dealer::{ - DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse, - RegisteredDealerDetails, + DealerDetailsResponse, PagedDealerAddressesResponse, PagedDealerIndexResponse, + PagedDealerResponse, RegisteredDealerDetails, }, dealing::{ DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse, @@ -84,6 +84,9 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(Epoch))] GetCurrentEpochState {}, + #[cfg_attr(feature = "schema", returns(Option))] + GetEpochStateAtHeight { height: u64 }, + #[cfg_attr(feature = "schema", returns(u64))] GetCurrentEpochThreshold {}, @@ -102,6 +105,20 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(DealerDetailsResponse))] GetDealerDetails { dealer_address: String }, + #[cfg_attr(feature = "schema", returns(PagedDealerAddressesResponse))] + GetEpochDealersAddresses { + epoch_id: EpochId, + limit: Option, + start_after: Option, + }, + + #[cfg_attr(feature = "schema", returns(PagedDealerResponse))] + GetEpochDealers { + epoch_id: EpochId, + limit: Option, + start_after: Option, + }, + #[cfg_attr(feature = "schema", returns(PagedDealerResponse))] GetCurrentDealers { limit: Option, diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml new file mode 100644 index 0000000000..212aa2d373 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "nym-contracts-common-testing" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } + +serde = { workspace = true } +rand_chacha = { workspace = true } +rand = { workspace = true } +cw-multi-test = { workspace = true } + +[lints] +workspace = true diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs new file mode 100644 index 0000000000..fa49db306d --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/helpers.rs @@ -0,0 +1,127 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{ + coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps, + Response, StdResult, Storage, +}; +use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey}; +use rand::{RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub const TEST_DENOM: &str = "unym"; +pub const TEST_PREFIX: &str = "n"; + +pub fn mock_api() -> MockApi { + MockApi::default().with_prefix(TEST_PREFIX) +} + +pub fn mock_dependencies() -> OwnedDeps> { + OwnedDeps { + storage: MockStorage::default(), + api: mock_api(), + querier: MockQuerier::default(), + custom_query_type: Default::default(), + } +} + +pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + rand_chacha::ChaCha20Rng::from_seed(dummy_seed) +} + +pub fn deps_with_balance(env: &Env) -> OwnedDeps> { + let mut deps = mock_dependencies(); + deps.querier = MockQuerier::::new(&[( + env.contract.address.as_str(), + coins(100000000000, TEST_DENOM).as_slice(), + )]); + deps +} + +pub fn generate_sorted_addresses(n: usize) -> Vec { + let mut rng = test_rng(); + let mut addrs = Vec::with_capacity(n); + for i in 0..n { + addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64()))); + } + addrs.sort(); + addrs +} + +pub fn addr>(raw: S) -> Addr { + mock_api().addr_make(raw.as_ref()) +} + +pub fn sender>(raw: S) -> MessageInfo { + message_info(&addr(raw), &[]) +} + +pub trait ExtractBankMsg { + fn unwrap_bank_msg(self) -> Option; +} + +impl ExtractBankMsg for Response { + fn unwrap_bank_msg(self) -> Option { + for msg in self.messages { + match msg.msg { + CosmosMsg::Bank(bank_msg) => return Some(bank_msg), + _ => continue, + } + } + + None + } +} + +pub trait FullReader<'a> { + type Key; + type Value: Serialize + DeserializeOwned; + + fn all_values(&self, store: &dyn Storage) -> StdResult>; + + fn all_key_values(&self, store: &dyn Storage) -> StdResult>; +} + +impl<'a, K, T> FullReader<'a> for Map +where + T: Serialize + DeserializeOwned, + K: PrimaryKey<'a> + KeyDeserialize, + K::Output: 'static, +{ + type Key = K::Output; + type Value = T; + + fn all_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending) + .map(|record| record.map(|r| r.1)) + .collect() + } + + fn all_key_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending).collect() + } +} + +impl<'a, K, T, B> FullReader<'a> for Prefix +where + K: KeyDeserialize + 'static, + T: Serialize + DeserializeOwned, + B: PrimaryKey<'a>, +{ + type Key = K::Output; + type Value = T; + + fn all_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending) + .map(|record| record.map(|r| r.1)) + .collect() + } + + fn all_key_values(&self, store: &dyn Storage) -> StdResult> { + self.range(store, None, None, Order::Ascending).collect() + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs new file mode 100644 index 0000000000..acb4292d86 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// those are all used exclusively for testing thus unwraps, et al. are allowed +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] +#![allow(clippy::panic)] + +pub mod helpers; +pub mod tester; + +pub use helpers::*; +pub use tester::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs new file mode 100644 index 0000000000..79b7607e4e --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/basic_traits.rs @@ -0,0 +1,239 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractTester, TestableNymContract}; +use cosmwasm_std::testing::{message_info, mock_env}; +use cosmwasm_std::{ + from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + Storage, Timestamp, +}; +use cw_multi_test::{next_block, AppResponse, Executor}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::any::type_name; +use std::fmt::Debug; + +pub trait ContractOpts { + type ExecuteMsg; + type QueryMsg; + type ContractError; + + fn deps(&self) -> Deps<'_>; + + fn deps_mut(&mut self) -> DepsMut<'_>; + + fn env(&self) -> Env; + + fn addr_make(&self, input: &str) -> Addr; + + fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) { + let env = self.env().clone(); + (self.deps_mut(), env) + } + + fn storage(&self) -> &dyn Storage; + + fn storage_mut(&mut self) -> &mut dyn Storage; + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option; + + fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>); + + fn unchecked_read_from_contract_storage( + &self, + key: impl AsRef<[u8]>, + ) -> T { + let typ = type_name::(); + self.read_from_contract_storage(key) + .unwrap_or_else(|| panic!("value of type {typ} not present in the storage")) + } + + fn execute_raw( + &mut self, + sender: Addr, + message: Self::ExecuteMsg, + ) -> Result { + self.execute_raw_with_balance(sender, &[], message) + } + + fn execute_raw_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: Self::ExecuteMsg, + ) -> Result; +} + +impl ContractOpts for ContractTester +where + C: TestableNymContract, +{ + type ExecuteMsg = C::ExecuteMsg; + type QueryMsg = C::QueryMsg; + type ContractError = C::ContractError; + + fn deps(&self) -> Deps<'_> { + Deps { + storage: &self.storage, + api: self.app.api(), + querier: self.app.wrap(), + } + } + + fn deps_mut(&mut self) -> DepsMut<'_> { + DepsMut { + storage: &mut self.storage, + api: self.app.api(), + querier: self.app.wrap(), + } + } + + fn env(&self) -> Env { + Env { + block: self.app.block_info(), + contract: ContractInfo { + address: self.contract_address.clone(), + }, + ..mock_env() + } + } + + fn addr_make(&self, input: &str) -> Addr { + self.app.api().addr_make(input) + } + + fn storage(&self) -> &dyn Storage { + &self.storage + } + + fn storage_mut(&mut self) -> &mut dyn Storage { + &mut self.storage + } + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option { + let raw = self.deps().storage.get(key.as_ref())?; + from_json(&raw).ok() + } + + fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { + self.deps_mut().storage.set(key.as_ref(), value.as_ref()); + } + + fn execute_raw_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: C::ExecuteMsg, + ) -> Result { + let env = self.env(); + let info = message_info(&sender, coins); + + C::execute()(self.deps_mut(), env, info, message) + } +} + +pub trait ChainOpts: ContractOpts { + fn set_contract_balance(&mut self, balance: Coin); + + fn next_block(&mut self); + + fn set_block_time(&mut self, time: Timestamp); + + fn execute_msg( + &mut self, + sender: Addr, + message: &Self::ExecuteMsg, + ) -> anyhow::Result { + self.execute_msg_with_balance(sender, &[], message) + } + + fn execute_msg_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: &Self::ExecuteMsg, + ) -> anyhow::Result; + + fn execute_arbitrary_contract( + &mut self, + contract: Addr, + sender: MessageInfo, + message: &T, + ) -> anyhow::Result; + + fn query_arbitrary_contract( + &self, + contract: Addr, + message: &Q, + ) -> StdResult; + + fn query(&self, message: &Self::QueryMsg) -> StdResult; +} + +impl ChainOpts for ContractTester +where + C: TestableNymContract, +{ + fn set_contract_balance(&mut self, balance: Coin) { + let contract_address = &self.contract_address; + self.app + .router() + .bank + .init_balance( + &mut self.storage.inner_storage(), + contract_address, + vec![balance], + ) + .unwrap(); + } + fn next_block(&mut self) { + self.app.update_block(next_block) + } + + fn set_block_time(&mut self, time: Timestamp) { + self.app.update_block(|b| b.time = time) + } + + fn execute_msg( + &mut self, + sender: Addr, + message: &C::ExecuteMsg, + ) -> anyhow::Result { + self.execute_msg_with_balance(sender, &[], message) + } + + fn execute_msg_with_balance( + &mut self, + sender: Addr, + coins: &[Coin], + message: &C::ExecuteMsg, + ) -> anyhow::Result { + self.app + .execute_contract(sender, self.contract_address.clone(), message, coins) + } + + fn execute_arbitrary_contract( + &mut self, + contract: Addr, + sender: MessageInfo, + message: &T, + ) -> anyhow::Result { + let coins = &sender.funds; + let sender = sender.sender; + self.app.execute_contract(sender, contract, message, coins) + } + + fn query_arbitrary_contract( + &self, + contract: Addr, + message: &Q, + ) -> StdResult { + self.app.wrap().query_wasm_smart(contract, message) + } + + fn query(&self, message: &C::QueryMsg) -> StdResult { + self.app + .wrap() + .query_wasm_smart(self.contract_address.as_str(), message) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs new file mode 100644 index 0000000000..75b226bd69 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/extensions.rs @@ -0,0 +1,305 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract, + TEST_DENOM, +}; +use cosmwasm_std::testing::message_info; +use cosmwasm_std::{ + coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage, +}; +use cw_multi_test::Executor; +use cw_storage_plus::{Key, Path, PrimaryKey}; +use rand::RngCore; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::any::type_name; +use std::ops::Deref; + +pub trait StorageReader { + fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>; + + fn read_common_value(&self, key: CommonStorageKeys) -> Option { + self.read_from_contract_storage(self.common_key(key)?) + } + + fn unchecked_read_common_value(&self, key: CommonStorageKeys) -> T { + self.unchecked_read_from_contract_storage( + self.common_key(key) + .unwrap_or_else(|| panic!("no key set for {key:?}")), + ) + } + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option; + + fn unchecked_read_from_contract_storage( + &self, + key: impl AsRef<[u8]>, + ) -> T { + let typ = type_name::(); + self.read_from_contract_storage(key) + .unwrap_or_else(|| panic!("value of type {typ} not present in the storage")) + } +} + +// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted +// but this makes it a tad easier and it's only testing code so it's fine +pub trait StorageWriter: StorageReader { + fn set_common_value( + &mut self, + key: CommonStorageKeys, + value: &T, + ) -> StdResult<()> { + let key = self + .common_key(key) + .ok_or(StdError::not_found("key not found"))? + .to_vec(); + self.set_storage_value(key, value) + } + + fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>); + + fn set_storage_value( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + self.set_storage(key, &to_json_vec(value)?); + Ok(()) + } +} + +pub trait ArbitraryContractStorageReader { + fn may_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> Option>; + + fn must_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> StdResult> { + let key = key.as_ref(); + self.may_read_from_contract_storage(address, key) + .ok_or(StdError::not_found(format!("no data under {key:?}"))) + } + + fn may_read_value_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> StdResult> { + let Some(bytes) = self.may_read_from_contract_storage(address, key) else { + return Ok(None); + }; + + from_json(&bytes).map(Some) + } + + fn must_read_value_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> StdResult { + let bytes = self.must_read_from_contract_storage(address, key)?; + from_json(&bytes) + } +} + +pub trait ArbitraryContractStorageWriter { + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ); + + fn set_contract_storage_value( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + self.set_contract_storage(address, key, &to_json_vec(value)?); + Ok(()) + } + + // attempts to write to an arbitrary contract `cw_storage_plus::Map` + fn set_contract_map_value<'a, K, T>( + &mut self, + address: impl Into, + namespace: impl AsRef<[u8]>, + key: K, + value: &T, + ) -> StdResult<()> + where + K: PrimaryKey<'a>, + T: Serialize + DeserializeOwned, + { + let key_path: Path = Path::new( + namespace.as_ref(), + &key.key().iter().map(Key::as_ref).collect::>(), + ); + let storage_key = key_path.deref(); + self.set_contract_storage_value(address, storage_key, value) + } +} + +// contract that has an admin +pub trait AdminExt: StorageReader + StorageWriter { + fn admin(&self) -> Option { + self.read_common_value(CommonStorageKeys::Admin) + } + + fn update_admin(&mut self, admin: &Option) -> StdResult<()> { + self.set_common_value(CommonStorageKeys::Admin, admin) + } + + fn admin_unchecked(&self) -> Addr { + self.admin().expect("no admin set") + } + + fn admin_msg(&self) -> MessageInfo { + message_info(&self.admin_unchecked(), &[]) + } +} + +// contract that operates on some specific coin denom +pub trait DenomExt: StorageReader { + fn denom(&self) -> String { + self.unchecked_read_common_value(CommonStorageKeys::Denom) + } + + fn coin(&self, amount: u128) -> Coin { + coin(amount, self.denom()) + } + + fn coins(&self, amount: u128) -> Vec { + coins(amount, self.denom()) + } +} + +pub trait RandExt { + fn raw_rng(&mut self) -> &mut ChaCha20Rng; + + fn generate_account(&mut self) -> Addr; + + fn generate_account_with_balance(&mut self) -> Addr + where + Self: BankExt; +} + +pub trait BankExt { + fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>; +} + +impl AdminExt for T where T: StorageReader + StorageWriter {} +impl DenomExt for T where T: StorageReader {} + +impl StorageReader for ContractTester { + fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> { + self.common_storage_keys.get(&key).map(|v| &**v) + } + + fn read_from_contract_storage(&self, key: impl AsRef<[u8]>) -> Option { + ::read_from_contract_storage(self, key) + } +} + +impl StorageWriter for ContractTester { + fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) { + ::set_contract_storage(self, key, value) + } +} + +impl BankExt for ContractTester { + fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> { + self.app + .send_tokens(self.master_address.clone(), to, &[amount])?; + Ok(()) + } +} + +impl RandExt for ContractTester { + fn raw_rng(&mut self) -> &mut ChaCha20Rng { + &mut self.rng + } + + fn generate_account(&mut self) -> Addr { + self.app + .api() + .addr_make(&format!("foomp{}", self.rng.next_u64())) + } + + fn generate_account_with_balance(&mut self) -> Addr + where + Self: BankExt, + { + let addr = self.generate_account(); + let million = 1_000_000_000_000; + self.send_tokens(addr.clone(), coin(million, TEST_DENOM)) + .unwrap(); + addr + } +} + +impl ArbitraryContractStorageReader for StorageWrapper { + fn may_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> Option> { + self.contract_storage_wrapper(&Addr::unchecked(address)) + .get(key.as_ref()) + } +} + +impl ArbitraryContractStorageWriter for StorageWrapper { + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + // yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine + let mut wrapped_storage = self + .clone() + .contract_storage_wrapper(&Addr::unchecked(address)); + wrapped_storage.set(key.as_ref(), value.as_ref()); + } +} + +impl ArbitraryContractStorageReader for ContractTester +where + C: TestableNymContract, +{ + fn may_read_from_contract_storage( + &self, + address: impl Into, + key: impl AsRef<[u8]>, + ) -> Option> { + self.storage + .as_inner_storage() + .may_read_from_contract_storage(address, key) + } +} + +impl ArbitraryContractStorageWriter for ContractTester +where + C: TestableNymContract, +{ + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + self.storage + .as_inner_storage_mut() + .set_contract_storage(address, key, value); + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs new file mode 100644 index 0000000000..d6b88bafc5 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/mod.rs @@ -0,0 +1,276 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{mock_api, test_rng, TEST_DENOM}; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper, + Record, Response, Storage, +}; +use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor}; +use rand_chacha::ChaCha20Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; + +pub use basic_traits::*; +pub use extensions::*; + +pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper}; + +mod basic_traits; +mod extensions; +mod storage_wrapper; +// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now) + +pub type ContractFn = + fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result; +pub type QueryFn = fn(deps: Deps, env: Env, msg: T) -> Result; +pub type PermissionedFn = fn(deps: DepsMut, env: Env, msg: T) -> Result; + +pub type ContractClosure = Box Result>; +pub type QueryClosure = Box Result>; + +pub trait TestableNymContract { + const NAME: &'static str; + + type InitMsg: DeserializeOwned + Serialize + Debug + 'static; + type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static; + type QueryMsg: DeserializeOwned + Serialize + Debug + 'static; + type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static; + type ContractError: Display + Debug + Send + Sync + 'static; + + fn instantiate() -> ContractFn; + fn execute() -> ContractFn; + fn query() -> QueryFn; + fn migrate() -> PermissionedFn; + + fn base_init_msg() -> Self::InitMsg; + + // // for now we don't care about custom queriers + // fn contract_wrapper() -> ContractWrapper< + // Self::ExecuteMsg, + // Self::InitMsg, + // Self::QueryMsg, + // Self::ContractError, + // anyhow::Error, + // anyhow::Error, + // Empty, + // Empty, + // Empty, + // Self::ContractError, + // Self::ContractError, + // Self::MigrateMsg, + // Self::ContractError, + // > { + // ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query()) + // .with_migrate(Self::migrate()) + // } + + fn dyn_contract() -> Box> { + Box::new( + ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query()) + .with_migrate(Self::migrate()), + ) + } + + fn init() -> ContractTester + where + Self: Sized, + { + ContractTesterBuilder::new() + .instantiate::(None) + .build() + } +} + +pub struct ContractTesterBuilder { + contract: PhantomData, + master_address: Addr, + app: App, + storage: StorageWrapper, + pub well_known_contracts: HashMap<&'static str, Addr>, +} + +impl ContractTesterBuilder { + #[allow(clippy::new_without_default)] + pub fn new() -> Self + where + C: TestableNymContract, + { + let storage = StorageWrapper::new(); + + let api = mock_api(); + let master_address = api.addr_make("master-owner"); + + let app = AppBuilder::new() + .with_api(api) + .with_storage(storage.clone()) + .build(|router, _api, storage| { + router + .bank + .init_balance( + storage, + &master_address, + coins(1000000000000000, TEST_DENOM), + ) + .unwrap() + }); + + ContractTesterBuilder { + contract: Default::default(), + master_address, + app, + storage, + well_known_contracts: Default::default(), + } + } + + pub fn instantiate( + mut self, + custom_init_msg: Option, + ) -> ContractTesterBuilder { + let code_id = self.app.store_code(D::dyn_contract()); + let contract_address = self + .app + .instantiate_contract( + code_id, + self.master_address.clone(), + &custom_init_msg.unwrap_or(D::base_init_msg()), + &[], + D::NAME, + Some(self.master_address.to_string()), + ) + .unwrap(); + + // send some tokens to the contract + self.app + .send_tokens( + self.master_address.clone(), + contract_address.clone(), + &[coin(100000000, TEST_DENOM)], + ) + .unwrap(); + + self.well_known_contracts.insert(D::NAME, contract_address); + self + } + + pub fn build(self) -> ContractTester + where + C: TestableNymContract, + { + if !self.well_known_contracts.contains_key(C::NAME) { + panic!("{} contract has not been instantiated", C::NAME); + } + + let contract_address = self.well_known_contracts[C::NAME].clone(); + + ContractTester { + contract: self.contract, + app: self.app, + rng: test_rng(), + master_address: self.master_address, + storage: self.storage.contract_storage_wrapper(&contract_address), + contract_address, + common_storage_keys: Default::default(), + well_known_contracts: self.well_known_contracts, + } + } + + pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { + self.storage.contract_storage_wrapper(contract) + } + + pub fn api(&self) -> MockApi { + *self.app.api() + } + + pub fn querier(&self) -> QuerierWrapper<'_> { + self.app.wrap() + } +} + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] +pub enum CommonStorageKeys { + Admin, + Denom, +} + +pub struct ContractTester { + contract: PhantomData, + pub app: App, + pub rng: ChaCha20Rng, + pub contract_address: Addr, + pub master_address: Addr, + pub(crate) storage: ContractStorageWrapper, + pub common_storage_keys: HashMap>, + + // TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being) + pub well_known_contracts: HashMap<&'static str, Addr>, +} + +impl ContractTester +where + C: TestableNymContract, +{ + pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) { + self.common_storage_keys + .insert(key, value.as_ref().to_vec()); + } + + pub fn with_common_storage_key( + mut self, + key: CommonStorageKeys, + value: impl AsRef<[u8]>, + ) -> Self { + self.insert_common_storage_key(key, value); + self + } +} + +impl Storage for ContractTester +where + C: TestableNymContract, +{ + fn get(&self, key: &[u8]) -> Option> { + self.storage.get(key) + } + + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + self.storage.range(start, end, order) + } + + fn range_keys<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + self.storage.range_keys(start, end, order) + } + + fn range_values<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box> + 'a> { + self.storage.range_values(start, end, order) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + self.storage.set(key, value) + } + + fn remove(&mut self, key: &[u8]) { + self.storage.remove(key) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs new file mode 100644 index 0000000000..ab60628a02 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common-testing/src/tester/storage_wrapper.rs @@ -0,0 +1,184 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::storage_keys::to_length_prefixed_nested; +use cosmwasm_std::testing::MockStorage; +use cosmwasm_std::{Addr, MemoryStorage, Order, Record, Storage}; +use std::cell::RefCell; +use std::rc::Rc; + +#[derive(Debug, Clone)] +pub struct StorageWrapper(Rc>); + +impl StorageWrapper { + pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper { + ContractStorageWrapper { + address: contract.clone(), + inner: self.clone(), + } + } + + pub(super) fn new() -> Self { + StorageWrapper(Rc::new(RefCell::new(MockStorage::new()))) + } +} + +#[derive(Debug, Clone)] +pub struct ContractStorageWrapper { + address: Addr, + inner: StorageWrapper, +} + +impl ContractStorageWrapper { + pub fn inner_storage(&self) -> StorageWrapper { + self.inner.clone() + } + + pub fn as_inner_storage(&self) -> &StorageWrapper { + &self.inner + } + + pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper { + &mut self.inner + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn change_contract(&self, contract: &Addr) -> Self { + ContractStorageWrapper { + address: contract.clone(), + inner: self.inner.clone(), + } + } +} + +impl Storage for StorageWrapper { + fn get(&self, key: &[u8]) -> Option> { + self.0.borrow().get(key) + } + + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + // hehe, that's nasty + let vals = self.0.borrow().range(start, end, order).collect::>(); + Box::new(vals.into_iter()) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + self.0.borrow_mut().set(key, value); + } + + fn remove(&mut self, key: &[u8]) { + self.0.borrow_mut().remove(key); + } +} + +impl ContractStorageWrapper { + fn contract_namespace(&self) -> Vec { + let mut name = b"contract_data/".to_vec(); + name.extend_from_slice(self.address.as_bytes()); + name + } + + fn prefix(&self) -> Vec { + to_length_prefixed_nested(&[b"wasm", &self.contract_namespace()]) + } + + #[inline] + fn trim(namespace: &[u8], key: &[u8]) -> Vec { + key[namespace.len()..].to_vec() + } + + /// Returns a new vec of same length and last byte incremented by one + /// If last bytes are 255, we handle overflow up the chain. + /// If all bytes are 255, this returns wrong data - but that is never possible as a namespace + fn namespace_upper_bound(input: &[u8]) -> Vec { + let mut copy = input.to_vec(); + // zero out all trailing 255, increment first that is not such + for i in (0..input.len()).rev() { + if copy[i] == 255 { + copy[i] = 0; + } else { + copy[i] += 1; + break; + } + } + copy + } + + #[inline] + fn concat(namespace: &[u8], key: &[u8]) -> Vec { + let mut k = namespace.to_vec(); + k.extend_from_slice(key); + k + } + + fn get_with_prefix(storage: &dyn Storage, namespace: &[u8], key: &[u8]) -> Option> { + storage.get(&Self::concat(namespace, key)) + } + + fn set_with_prefix(storage: &mut dyn Storage, namespace: &[u8], key: &[u8], value: &[u8]) { + storage.set(&Self::concat(namespace, key), value); + } + + fn remove_with_prefix(storage: &mut dyn Storage, namespace: &[u8], key: &[u8]) { + storage.remove(&Self::concat(namespace, key)); + } + + fn range_with_prefix<'a>( + storage: &'a dyn Storage, + namespace: &[u8], + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + // prepare start, end with prefix + let start = match start { + Some(s) => Self::concat(namespace, s), + None => namespace.to_vec(), + }; + let end = match end { + Some(e) => Self::concat(namespace, e), + // end is updating last byte by one + None => Self::namespace_upper_bound(namespace), + }; + + // get iterator from storage + let base_iterator = storage.range(Some(&start), Some(&end), order); + + // make a copy for the closure to handle lifetimes safely + let prefix = namespace.to_vec(); + let mapped = base_iterator.map(move |(k, v)| (Self::trim(&prefix, &k), v)); + Box::new(mapped) + } +} + +impl Storage for ContractStorageWrapper { + fn get(&self, key: &[u8]) -> Option> { + let prefix = self.prefix(); + Self::get_with_prefix(&self.inner, &prefix, key) + } + + fn range<'a>( + &'a self, + start: Option<&[u8]>, + end: Option<&[u8]>, + order: Order, + ) -> Box + 'a> { + let prefix = self.prefix(); + Self::range_with_prefix(&self.inner, &prefix, start, end, order) + } + + fn set(&mut self, key: &[u8], value: &[u8]) { + let prefix = self.prefix(); + Self::set_with_prefix(&mut self.inner, &prefix, key, value); + } + + fn remove(&mut self, key: &[u8]) { + let prefix = self.prefix(); + Self::remove_with_prefix(&mut self.inner, &prefix, key); + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 4a80db6afa..c13d29a7ec 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -18,6 +18,7 @@ serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } serde_json = { workspace = true } [build-dependencies] diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index e0f5845fa0..e8657fb265 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -35,7 +35,7 @@ pub enum ContractsCommonError { /// Percent represents a value between 0 and 100% /// (i.e. between 0.0 and 1.0) #[cw_serde] -#[derive(Copy, Default, PartialOrd)] +#[derive(Copy, Default, PartialOrd, Ord, Eq)] pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { @@ -80,6 +80,44 @@ impl Percent { pub fn checked_pow(&self, exp: u32) -> Result { self.0.checked_pow(exp).map(Percent) } + + // truncate provided percent to only have 2 decimal places, + // e.g. convert "0.1234567" into "0.12" + // the purpose of it is to reduce storage space, in particular for the performance contract + // since that extra precision gains us nothing + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn round_to_two_decimal_places(&self) -> Self { + let raw = self.0; + + const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18 + const THRESHOLD: Decimal = Decimal::permille(5); // 0.005 + + // in case it ever changes since it's not exposed in the public API + debug_assert_eq!( + DECIMAL_FRACTIONAL, + Uint128::new(10).pow(Decimal::DECIMAL_PLACES) + ); + + let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL; + + #[allow(clippy::unwrap_used)] + let floored = Decimal::from_atomics(int, 2).unwrap(); + let diff = raw - floored; + let rounded = if diff >= THRESHOLD { + // ceil + floored + Decimal::percent(1) + } else { + floored + }; + Percent(rounded) + } + + #[must_use = "this returns the result of the operation, without modifying the original"] + pub fn average(&self, other: &Self) -> Self { + let sum = self.0 + other.0; + let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2)); + Percent(inner) + } } impl Display for Percent { @@ -334,6 +372,7 @@ mod tests { } #[test] + #[cfg(feature = "naive_float")] fn naive_float_conversion() { // around 15 decimal places is the maximum precision we can handle // which is still way more than enough for what we use it for @@ -347,4 +386,41 @@ mod tests { assert!(converted.0 - converted.0 < epsilon); } + + #[test] + fn rounding_percent() { + let test_cases = vec![ + ("0", "0"), + ("0.1", "0.1"), + ("0.12", "0.12"), + ("0.12", "0.123"), + ("0.12", "0.123456789"), + ("0.13", "0.125"), + ("0.13", "0.126"), + ("0.13", "0.126436545676"), + ("0.99", "0.99"), + ("0.99", "0.994"), + ("1", "0.999"), + ("1", "0.995"), + ]; + for (expected, input) in test_cases { + let expected: Percent = expected.parse().unwrap(); + let pre_truncated: Percent = input.parse().unwrap(); + assert_eq!(expected, pre_truncated.round_to_two_decimal_places()) + } + } + + #[test] + fn calculating_average() -> anyhow::Result<()> { + fn p(raw: &str) -> Percent { + raw.parse().unwrap() + } + + assert_eq!(p("0.1").average(&p("0.1")), p("0.1")); + assert_eq!(p("0.1").average(&p("0.2")), p("0.15")); + assert_eq!(p("1").average(&p("0")), p("0.5")); + assert_eq!(p("0.123").average(&p("0.456")), p("0.2895")); + + Ok(()) + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 8800e72b20..d0a7b3b913 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -23,7 +23,6 @@ semver = { workspace = true, features = ["serde"] } schemars = { workspace = true } thiserror = { workspace = true } contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } -serde-json-wasm = { workspace = true } humantime-serde = { workspace = true } utoipa = { workspace = true, optional = true } @@ -40,3 +39,6 @@ contract-testing = [] utoipa = ["dep:utoipa"] schema = ["cw2"] generate-ts = ['ts-rs'] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index e8349d4eb0..8104a1c910 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -193,6 +193,9 @@ pub enum MixnetContractError { #[error("attempted to perform the operation with 0 coins. This is not allowed")] ZeroCoinAmount, + #[error("key rotation validity below minimum value")] + TooShortRotationInterval, + #[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")] RewardingValidatorMismatch { current_validator: Addr, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index deeb416887..72e848aef1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -3,7 +3,7 @@ use crate::{IdentityKey, NodeId, SphinxKey}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::{to_json_string, Addr, Coin}; use std::cmp::Ordering; use std::fmt::Display; @@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate { impl GatewayConfigUpdate { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index 125359d9f5..b5032e64be 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -57,7 +57,7 @@ pub trait NodeBond { fn is_unbonding(&self) -> bool; - fn identity(&self) -> IdentityKeyRef; + fn identity(&self) -> IdentityKeyRef<'_>; fn original_pledge(&self) -> &Coin; @@ -125,7 +125,7 @@ impl NodeBond for MixNodeBond { self.is_unbonding } - fn identity(&self) -> IdentityKeyRef { + fn identity(&self) -> IdentityKeyRef<'_> { self.identity() } @@ -178,7 +178,7 @@ impl NodeBond for NymNodeBond { self.is_unbonding } - fn identity(&self) -> IdentityKeyRef { + fn identity(&self) -> IdentityKeyRef<'_> { self.identity() } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 2259af4c7a..39cd180bf8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -358,7 +358,7 @@ impl Interval { self.total_elapsed_epochs } - pub const fn current_epoch_absolute_id(&self) -> u32 { + pub const fn current_epoch_absolute_id(&self) -> EpochId { // since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n self.total_elapsed_epochs } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/key_rotation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/key_rotation.rs new file mode 100644 index 0000000000..3c32b99e2f --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/key_rotation.rs @@ -0,0 +1,155 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::EpochId; +use cosmwasm_schema::cw_serde; + +pub type KeyRotationId = u32; + +#[cw_serde] +#[derive(Copy)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub struct KeyRotationState { + /// Defines how long each key rotation is valid for (in terms of epochs) + pub validity_epochs: u32, + + /// Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts). + /// It is used for determining when rotation is meant to advance. + #[cfg_attr(feature = "utoipa", schema(value_type = u32))] + pub initial_epoch_id: EpochId, +} + +impl KeyRotationState { + pub fn key_rotation_id(&self, current_epoch_id: EpochId) -> KeyRotationId { + let diff = current_epoch_id.saturating_sub(self.initial_epoch_id); + diff / self.validity_epochs + } + + pub fn next_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId { + self.current_rotation_starting_epoch_id(current_epoch_id) + self.validity_epochs + } + + pub fn current_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId { + let current_rotation_id = self.key_rotation_id(current_epoch_id); + + self.initial_epoch_id + self.validity_epochs * current_rotation_id + } +} + +#[cw_serde] +pub struct KeyRotationIdResponse { + pub rotation_id: KeyRotationId, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn key_rotation_id() { + let state = KeyRotationState { + validity_epochs: 24, + initial_epoch_id: 0, + }; + assert_eq!(0, state.key_rotation_id(0)); + assert_eq!(0, state.key_rotation_id(23)); + assert_eq!(1, state.key_rotation_id(24)); + assert_eq!(1, state.key_rotation_id(47)); + assert_eq!(2, state.key_rotation_id(48)); + + let state = KeyRotationState { + validity_epochs: 12, + initial_epoch_id: 0, + }; + assert_eq!(0, state.key_rotation_id(0)); + assert_eq!(0, state.key_rotation_id(11)); + assert_eq!(1, state.key_rotation_id(12)); + assert_eq!(1, state.key_rotation_id(23)); + assert_eq!(2, state.key_rotation_id(24)); + + let state = KeyRotationState { + validity_epochs: 24, + initial_epoch_id: 10000, + }; + assert_eq!(0, state.key_rotation_id(123)); + assert_eq!(0, state.key_rotation_id(10000)); + assert_eq!(0, state.key_rotation_id(10001)); + assert_eq!(0, state.key_rotation_id(10023)); + assert_eq!(1, state.key_rotation_id(10024)); + assert_eq!(1, state.key_rotation_id(10047)); + assert_eq!(2, state.key_rotation_id(10048)); + assert_eq!(2, state.key_rotation_id(10060)); + } + + #[test] + fn next_rotation_starting_epoch_id() { + let state = KeyRotationState { + validity_epochs: 24, + initial_epoch_id: 0, + }; + assert_eq!(24, state.next_rotation_starting_epoch_id(0)); + assert_eq!(24, state.next_rotation_starting_epoch_id(23)); + assert_eq!(48, state.next_rotation_starting_epoch_id(24)); + assert_eq!(48, state.next_rotation_starting_epoch_id(47)); + assert_eq!(72, state.next_rotation_starting_epoch_id(48)); + + let state = KeyRotationState { + validity_epochs: 12, + initial_epoch_id: 0, + }; + assert_eq!(12, state.next_rotation_starting_epoch_id(0)); + assert_eq!(12, state.next_rotation_starting_epoch_id(11)); + assert_eq!(24, state.next_rotation_starting_epoch_id(12)); + assert_eq!(24, state.next_rotation_starting_epoch_id(23)); + assert_eq!(36, state.next_rotation_starting_epoch_id(24)); + + let state = KeyRotationState { + validity_epochs: 24, + initial_epoch_id: 10000, + }; + assert_eq!(10024, state.next_rotation_starting_epoch_id(123)); + assert_eq!(10024, state.next_rotation_starting_epoch_id(10000)); + assert_eq!(10024, state.next_rotation_starting_epoch_id(10001)); + assert_eq!(10024, state.next_rotation_starting_epoch_id(10023)); + assert_eq!(10048, state.next_rotation_starting_epoch_id(10024)); + assert_eq!(10048, state.next_rotation_starting_epoch_id(10047)); + assert_eq!(10072, state.next_rotation_starting_epoch_id(10048)); + assert_eq!(10072, state.next_rotation_starting_epoch_id(10060)); + } + + #[test] + fn current_rotation_starting_epoch_id() { + let state = KeyRotationState { + validity_epochs: 24, + initial_epoch_id: 0, + }; + assert_eq!(0, state.current_rotation_starting_epoch_id(0)); + assert_eq!(0, state.current_rotation_starting_epoch_id(23)); + assert_eq!(24, state.current_rotation_starting_epoch_id(24)); + assert_eq!(24, state.current_rotation_starting_epoch_id(47)); + assert_eq!(48, state.current_rotation_starting_epoch_id(48)); + + let state = KeyRotationState { + validity_epochs: 12, + initial_epoch_id: 0, + }; + assert_eq!(0, state.current_rotation_starting_epoch_id(0)); + assert_eq!(0, state.current_rotation_starting_epoch_id(11)); + assert_eq!(12, state.current_rotation_starting_epoch_id(12)); + assert_eq!(12, state.current_rotation_starting_epoch_id(23)); + assert_eq!(24, state.current_rotation_starting_epoch_id(24)); + + let state = KeyRotationState { + validity_epochs: 24, + initial_epoch_id: 10000, + }; + assert_eq!(10000, state.current_rotation_starting_epoch_id(123)); + assert_eq!(10000, state.current_rotation_starting_epoch_id(10000)); + assert_eq!(10000, state.current_rotation_starting_epoch_id(10001)); + assert_eq!(10000, state.current_rotation_starting_epoch_id(10023)); + assert_eq!(10024, state.current_rotation_starting_epoch_id(10024)); + assert_eq!(10024, state.current_rotation_starting_epoch_id(10047)); + assert_eq!(10048, state.current_rotation_starting_epoch_id(10048)); + assert_eq!(10048, state.current_rotation_starting_epoch_id(10060)); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 4f9d7a088f..eb1d533270 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -1,10 +1,6 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] -#![warn(clippy::todo)] - mod config_score; pub mod constants; pub mod delegation; @@ -13,6 +9,7 @@ pub mod events; pub mod gateway; pub mod helpers; pub mod interval; +pub mod key_rotation; pub mod mixnode; pub mod msg; pub mod nym_node; @@ -37,6 +34,7 @@ pub use gateway::{ pub use interval::{ CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId, }; +pub use key_rotation::*; pub use mixnode::{ LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 763082a1f1..91b4222fbe 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -16,7 +16,7 @@ use crate::{ Percent, ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; +use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -170,6 +170,11 @@ impl NodeRewarding { } } + // we panic here as opposed to returning an error as this is undefined behaviour, + // because the pledge amount has decreased (i.e. slashing has occurred) which + // should not be possible under any situation. at this point we don't know how many other things + // might have failed so we have to bail + #[allow(clippy::panic)] pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult { let initial_dec = original_pledge.amount.into_base_decimal()?; if initial_dec > self.operator { @@ -189,6 +194,11 @@ impl NodeRewarding { Ok(truncate_reward(delegator_reward, &delegation.amount.denom)) } + // we panic here as opposed to returning an error as this is undefined behaviour, + // because the pledge amount has decreased (i.e. slashing has occurred) which + // should not be possible under any situation. at this point we don't know how many other things + // might have failed so we have to bail + #[allow(clippy::panic)] pub fn withdraw_operator_reward( &mut self, original_pledge: &Coin, @@ -594,7 +604,7 @@ pub struct NodeCostParams { impl NodeCostParams { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } @@ -763,7 +773,7 @@ pub struct MixNodeConfigUpdate { impl MixNodeConfigUpdate { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 1f32e71441..0fedbd84e9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -35,6 +35,7 @@ use crate::{ PreassignedGatewayIdsResponse, }, interval::{CurrentIntervalResponse, EpochStatus}, + key_rotation::{KeyRotationIdResponse, KeyRotationState}, mixnode::{ MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, @@ -81,6 +82,18 @@ pub struct InstantiateMsg { #[serde(default)] pub interval_operating_cost: OperatingCostRange, + + #[serde(default)] + pub key_validity_in_epochs: Option, +} + +impl InstantiateMsg { + // needs to give us enough time to pre-announce key for following epoch + // and have an overlap with the preceding epoch + pub const MIN_KEY_ROTATION_VALIDITY: u32 = 3; + pub fn key_validity_in_epochs(&self) -> u32 { + self.key_validity_in_epochs.unwrap_or(24) + } } #[cw_serde] @@ -857,6 +870,15 @@ pub enum QueryMsg { /// Cosmos address used for the query of the signing nonce. address: String, }, + + // sphinx key rotation-related + #[cfg_attr(feature = "schema", returns(KeyRotationState))] + /// Gets the current state config of the key rotation (i.e. starting epoch id and validity duration) + GetKeyRotationState {}, + + /// Gets the current key rotation id + #[cfg_attr(feature = "schema", returns(KeyRotationIdResponse))] + GetKeyRotationId {}, } #[cw_serde] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs index cecbd4392f..6fc172db09 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -58,7 +58,7 @@ impl<'a> PrimaryKey<'a> for Role { type Suffix = >::Suffix; type SuperSuffix = >::SuperSuffix; - fn key(&self) -> Vec { + fn key(&self) -> Vec> { // I'm not sure why it wasn't possible to delegate the call to // `(*self as u8).key()` directly... // I guess because of the `Key::Ref(&'a [u8])` variant? diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 55bba8f9a6..6f9d01eb6f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal; use crate::nym_node::Role; use crate::{error::MixnetContractError, Percent}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Decimal; +use cosmwasm_std::{to_json_string, Decimal}; pub type Performance = Percent; pub type WorkFactor = Decimal; @@ -84,7 +84,26 @@ pub struct IntervalRewardParams { impl IntervalRewardParams { pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) + } + + pub fn active_node_work(&self, standby_node_work: Decimal) -> WorkFactor { + self.active_set_work_factor * standby_node_work + } + + pub fn standby_node_work( + &self, + rewarded_set_size: Decimal, + standby_set_size: Decimal, + ) -> WorkFactor { + let f = self.active_set_work_factor; + let k = rewarded_set_size; + let one = Decimal::one(); + + // nodes in reserve + let k_r = standby_set_size; + + one / (f * k - (f - one) * k_r) } } @@ -109,18 +128,15 @@ pub struct RewardingParams { impl RewardingParams { pub fn active_node_work(&self) -> WorkFactor { - self.interval.active_set_work_factor * self.standby_node_work() + let standby_work = self.standby_node_work(); + self.interval.active_node_work(standby_work) } pub fn standby_node_work(&self) -> WorkFactor { - let f = self.interval.active_set_work_factor; - let k = self.dec_rewarded_set_size(); - let one = Decimal::one(); - - // nodes in reserve - let k_r = self.dec_standby_set_size(); - - one / (f * k - (f - one) * k_r) + let rewarded_set_size = self.dec_rewarded_set_size(); + let standby_set_size = self.dec_standby_set_size(); + self.interval + .standby_node_work(rewarded_set_size, standby_set_size) } pub fn rewarded_set_size(&self) -> u32 { @@ -410,7 +426,7 @@ impl IntervalRewardingParamsUpdate { } pub fn to_inline_json(&self) -> String { - serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + to_json_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index ce54caab33..374b30bf0c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -44,6 +44,17 @@ pub struct RewardEstimate { pub operating_cost: Decimal, } +impl RewardEstimate { + pub const fn zero() -> RewardEstimate { + RewardEstimate { + total_node_reward: Decimal::zero(), + operator: Decimal::zero(), + delegates: Decimal::zero(), + operating_cost: Decimal::zero(), + } + } +} + #[cw_serde] #[derive(Copy, Default)] pub struct RewardDistribution { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs index 9ad2a3d02a..25f588b476 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -151,6 +151,9 @@ impl Simulator { } } + // this code is not meant to be used in production systems, only in tests + // so a panic due to inconsistent arguments is fine + #[allow(clippy::panic)] pub fn simulate_epoch( &mut self, node_params: &BTreeMap, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index b3c4e5b2be..9281e35bbb 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -3,6 +3,7 @@ use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams}; use crate::nym_node::Role; +use crate::reward_params::RewardedSetParams; use crate::EpochId; use contracts_common::Percent; use cosmwasm_schema::cw_serde; @@ -85,7 +86,11 @@ impl RewardedSet { } pub fn rewarded_set_size(&self) -> usize { - self.active_set_size() + self.standby.len() + self.active_set_size() + self.standby_set_size() + } + + pub fn standby_set_size(&self) -> usize { + self.standby.len() } pub fn get_role(&self, node_id: NodeId) -> Option { @@ -110,6 +115,13 @@ impl RewardedSet { } None } + + pub fn matches_parameters(&self, params: RewardedSetParams) -> bool { + self.entry_gateways.len() <= params.entry_gateways as usize + && self.exit_gateways.len() <= params.exit_gateways as usize + && self.layer1.len() + self.layer2.len() + self.layer3.len() <= params.mixnodes as usize + && self.standby.len() <= params.standby as usize + } } #[cw_serde] diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml b/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml new file mode 100644 index 0000000000..078351a348 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "nym-performance-contract-common" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true } +schemars = { workspace = true } + +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } + +nym-contracts-common = { path = "../contracts-common" } + + +[features] +schema = [] + +[lints] +workspace = true diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs new file mode 100644 index 0000000000..5f98a8e439 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/constants.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod storage_keys { + pub const CONTRACT_ADMIN: &str = "contract-admin"; + pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id"; + pub const LAST_SUBMISSION: &str = "last-submission"; + pub const MIXNET_CONTRACT: &str = "mixnet-contract"; + pub const AUTHORISED_COUNT: &str = "authorised-count"; + pub const AUTHORISED: &str = "authorised"; + pub const RETIRED: &str = "retired"; + pub const PERFORMANCE_RESULTS: &str = "performance-results"; + pub const SUBMISSION_METADATA: &str = "submission-metadata"; +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs new file mode 100644 index 0000000000..68d4e8832b --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/error.rs @@ -0,0 +1,39 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{EpochId, NodeId}; +use cosmwasm_std::Addr; +use cw_controllers::AdminError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum NymPerformanceContractError { + #[error("could not perform contract migration: {comment}")] + FailedMigration { comment: String }, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error(transparent)] + StdErr(#[from] cosmwasm_std::StdError), + + #[error("{address} is already an authorised network monitor")] + AlreadyAuthorised { address: Addr }, + + #[error("{address} is not an authorised network monitor")] + NotAuthorised { address: Addr }, + + #[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")] + StalePerformanceSubmission { + epoch_id: EpochId, + node_id: NodeId, + last_epoch_id: EpochId, + last_node_id: NodeId, + }, + + #[error("the batch performance data has not been sorted")] + UnsortedBatchSubmission, + + #[error("node {node_id} does not appear to be bonded")] + NodeNotBonded { node_id: NodeId }, +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/helpers.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs new file mode 100644 index 0000000000..4fc43b8bfe --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod constants; +pub mod error; +pub mod helpers; +pub mod msg; +pub mod types; + +pub use error::*; +pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs new file mode 100644 index 0000000000..74f512d891 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/msg.rs @@ -0,0 +1,125 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{EpochId, NodeId, NodePerformance}; +use cosmwasm_schema::cw_serde; + +#[cfg(feature = "schema")] +use crate::types::{ + EpochMeasurementsPagedResponse, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, LastSubmission, NetworkMonitorResponse, + NetworkMonitorsPagedResponse, NodeMeasurementsResponse, NodePerformancePagedResponse, + NodePerformanceResponse, RetiredNetworkMonitorsPagedResponse, +}; + +#[cw_serde] +pub struct InstantiateMsg { + pub mixnet_contract_address: String, + pub authorised_network_monitors: Vec, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { admin: String }, + + /// Attempt to submit performance data of a particular node for given epoch + Submit { + epoch: EpochId, + data: NodePerformance, + }, + + /// Attempt to submit performance data of a batch of nodes for given epoch + BatchSubmit { + epoch: EpochId, + data: Vec, + }, + + /// Attempt to authorise new network monitor for submitting performance data + AuthoriseNetworkMonitor { address: String }, + + /// Attempt to retire an existing network monitor and forbid it from submitting any future performance data + RetireNetworkMonitor { address: String }, + + /// An admin method to remove submitted node measurements. Used as an escape hatch should + /// the data stored get too unwieldy. + RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + + /// An admin method to remove submitted nodes measurements. Used as an escape hatch should + /// the data stored get too unwieldy. Note: it is expected to get called multiple times + /// until the response indicates all the epoch data has been removed. + RemoveEpochMeasurements { epoch_id: EpochId }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] + Admin {}, + + /// Returns performance of particular node for the provided epoch + #[cfg_attr(feature = "schema", returns(NodePerformanceResponse))] + NodePerformance { epoch_id: EpochId, node_id: NodeId }, + + /// Returns historical performance for particular node + #[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))] + NodePerformancePaged { + node_id: NodeId, + start_after: Option, + limit: Option, + }, + + /// Returns all submitted measurements for the particular node + #[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))] + NodeMeasurements { epoch_id: EpochId, node_id: NodeId }, + + /// Returns (paged) measurements for particular epoch + #[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))] + EpochMeasurementsPaged { + epoch_id: EpochId, + start_after: Option, + limit: Option, + }, + + /// Returns (paged) performance for particular epoch + #[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))] + EpochPerformancePaged { + epoch_id: EpochId, + start_after: Option, + limit: Option, + }, + + /// Returns full (paged) historical performance of the whole network + #[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))] + FullHistoricalPerformancePaged { + start_after: Option<(EpochId, NodeId)>, + limit: Option, + }, + + /// Returns information about particular network monitor + #[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))] + NetworkMonitor { address: String }, + + /// Returns information about all network monitors + #[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))] + NetworkMonitorsPaged { + start_after: Option, + limit: Option, + }, + + /// Returns information about all retired network monitors + #[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))] + RetiredNetworkMonitorsPaged { + start_after: Option, + limit: Option, + }, + + /// Returns information regarding the latest submitted performance data + #[cfg_attr(feature = "schema", returns(LastSubmission))] + LastSubmittedMeasurement {}, +} + +#[cw_serde] +pub struct MigrateMsg { + // +} diff --git a/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs new file mode 100644 index 0000000000..383fb08280 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-performance-contract/src/types.rs @@ -0,0 +1,258 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Env, Timestamp}; +use nym_contracts_common::Percent; + +pub type EpochId = u32; +pub type NodeId = u32; + +#[cw_serde] +pub struct LastSubmission { + pub block_height: u64, + pub block_time: Timestamp, + + // not as relevant, but might as well store it + pub data: Option, +} + +#[cw_serde] +pub struct LastSubmittedData { + pub sender: Addr, + pub epoch_id: EpochId, + pub data: NodePerformance, +} + +#[cw_serde] +pub struct NetworkMonitorDetails { + pub address: Addr, + pub authorised_by: Addr, + pub authorised_at_height: u64, +} + +impl NetworkMonitorDetails { + pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor { + RetiredNetworkMonitor { + details: self, + retired_by: sender.clone(), + retired_at_height: env.block.height, + } + } +} + +#[cw_serde] +pub struct RetiredNetworkMonitor { + pub details: NetworkMonitorDetails, + pub retired_by: Addr, + pub retired_at_height: u64, +} + +#[cw_serde] +#[derive(Copy)] +pub struct NodePerformance { + #[serde(rename = "n")] + pub node_id: NodeId, + + // note: value is rounded to 2 decimal places. + #[serde(rename = "p")] + pub performance: Percent, +} + +#[cw_serde] +pub struct NetworkMonitorSubmissionMetadata { + pub last_submitted_epoch_id: EpochId, + pub last_submitted_node_id: NodeId, +} + +// the internal values are always sorted +#[cw_serde] +pub struct NodeResults(Vec); + +impl NodeResults { + pub fn new(initial: Percent) -> NodeResults { + NodeResults(vec![initial.round_to_two_decimal_places()]) + } + + // ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values + // to insert new one and resave is cheap + pub fn insert_new(&mut self, result: Percent) { + let result = result.round_to_two_decimal_places(); + let pos = self.0.binary_search(&result).unwrap_or_else(|e| e); + self.0.insert(pos, result); + } + + // SAFETY: there are no codepaths that allow constructing empty struct + pub fn median(&self) -> Percent { + let len = self.0.len(); + if len % 2 == 1 { + // odd number of elements: return the middle one + self.0[len / 2] + } else { + // even number: average the two middle elements + let mid1 = self.0[len / 2 - 1]; + let mid2 = self.0[len / 2]; + mid1.average(&mid2).round_to_two_decimal_places() + } + } + + pub fn inner(&self) -> &[Percent] { + &self.0 + } +} + +#[cw_serde] +pub struct NodePerformanceResponse { + pub performance: Option, +} + +#[cw_serde] +pub struct NodeMeasurementsResponse { + pub measurements: Option, +} + +#[cw_serde] +#[derive(Copy)] +pub struct EpochNodePerformance { + pub epoch: EpochId, + pub performance: Option, +} + +#[cw_serde] +pub struct NodePerformancePagedResponse { + pub node_id: NodeId, + pub performance: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct EpochPerformancePagedResponse { + pub epoch_id: EpochId, + pub performance: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct NodeMeasurement { + pub node_id: NodeId, + pub measurements: NodeResults, +} + +#[cw_serde] +pub struct EpochMeasurementsPagedResponse { + pub epoch_id: EpochId, + pub measurements: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +#[derive(Copy)] +pub struct HistoricalPerformance { + pub epoch_id: EpochId, + pub node_id: NodeId, + pub performance: Percent, +} + +#[cw_serde] +pub struct FullHistoricalPerformancePagedResponse { + pub performance: Vec, + pub start_next_after: Option<(EpochId, NodeId)>, +} + +#[cw_serde] +pub struct NetworkMonitorInformation { + pub details: NetworkMonitorDetails, + pub current_submission_metadata: NetworkMonitorSubmissionMetadata, +} + +#[cw_serde] +pub struct NetworkMonitorResponse { + pub info: Option, +} + +#[cw_serde] +pub struct NetworkMonitorsPagedResponse { + pub info: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct RetiredNetworkMonitorsPagedResponse { + pub info: Vec, + pub start_next_after: Option, +} + +#[cw_serde] +pub struct RemoveEpochMeasurementsResponse { + pub additional_entries_to_remove_remaining: bool, +} + +#[cw_serde] +#[derive(Default)] +pub struct BatchSubmissionResult { + pub accepted_scores: u64, + pub non_existent_nodes: Vec, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn p(raw: impl AsRef) -> Percent { + raw.as_ref().parse().unwrap() + } + + fn ps(raw: &[&str]) -> Vec { + raw.iter().map(p).collect() + } + + #[test] + fn node_results_insertion() { + let initial = NodeResults::new(p("0.5")); + + let mut smaller = initial.clone(); + let mut greater = initial.clone(); + + smaller.insert_new(p("0.4")); + greater.insert_new(p("0.6")); + + assert_eq!(smaller.0, ps(&["0.4", "0.5"])); + assert_eq!(greater.0, ps(&["0.5", "0.6"])); + + let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"])); + another.insert_new(p("0.6")); + another.insert_new(p("0.2")); + another.insert_new(p("0.7")); + another.insert_new(p("0.3")); + another.insert_new(p("0.3")); + another.insert_new(p("0.55")); + + assert_eq!( + another.0, + ps(&[ + "0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0" + ]) + ); + } + + #[test] + fn node_results_median() { + let results = NodeResults(ps(&["0.1"])); + assert_eq!(results.median(), p("0.1")); + + let results = NodeResults(ps(&["0.1", "0.2"])); + assert_eq!(results.median(), p("0.15")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3"])); + assert_eq!(results.median(), p("0.2")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"])); + assert_eq!(results.median(), p("0.25")); + + let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"])); + assert_eq!(results.median(), p("0.3")); + + let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"])); + assert_eq!(results.median(), p("1")); + } +} diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/Cargo.toml b/common/cosmwasm-smart-contracts/nym-pool-contract/Cargo.toml new file mode 100644 index 0000000000..38d7659ece --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nym-pool-contract-common" +version = "0.1.0" +description = "Common library for the Nym Pool contract" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +thiserror = { workspace = true } +serde = { workspace = true } +schemars = { workspace = true } + +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } + +[dev-dependencies] +time = { workspace = true, features = ["macros"] } + +[features] +schema = [] diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/src/constants.rs b/common/cosmwasm-smart-contracts/nym-pool-contract/src/constants.rs new file mode 100644 index 0000000000..a71397ab2b --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/src/constants.rs @@ -0,0 +1,11 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod storage_keys { + pub const CONTRACT_ADMIN: &str = "contract-admin"; + pub const POOL_DENOMINATION: &str = "pool_denom"; + pub const GRANTERS: &str = "granters"; + pub const GRANTS: &str = "grants"; + pub const TOTAL_LOCKED: &str = "total_locked"; + pub const LOCKED_GRANTEES: &str = "locked_grantees"; +} diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/src/error.rs b/common/cosmwasm-smart-contracts/nym-pool-contract/src/error.rs new file mode 100644 index 0000000000..35ad7ec745 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/src/error.rs @@ -0,0 +1,98 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, Uint128}; +use cw_controllers::AdminError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum NymPoolContractError { + #[error("could not perform contract migration: {comment}")] + FailedMigration { comment: String }, + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error(transparent)] + StdErr(#[from] cosmwasm_std::StdError), + + #[error("this sender is not authorised to revoke this grant. its neither the admin or the original (and still whitelisted) granter")] + UnauthorizedGrantRevocation, + + #[error("the specified address is already a whitelisted granter")] + AlreadyAGranter, + + #[error("{addr} is not a permitted granter")] + InvalidGranter { addr: String }, + + #[error("invalid coin denomination. got {got}, but expected {expected}")] + InvalidDenom { expected: String, got: String }, + + #[error("there already exists an active grant for {grantee}. it was granted by {granter} at block height {created_at_height}")] + GrantAlreadyExist { + granter: String, + grantee: String, + created_at_height: u64, + }, + + #[error("could not find any active grants for {grantee}")] + GrantNotFound { grantee: String }, + + #[error("the provided timestamp value ({timestamp}) is set in the past. the current block timestamp is {current_block_timestamp}")] + TimestampInThePast { + timestamp: u64, + current_block_timestamp: u64, + }, + + #[error("there are not enough tokens to process this request. {available} are available, but {required} is needed.")] + InsufficientTokens { available: Coin, required: Coin }, + + #[error("the period length can't be zero")] + ZeroAllowancePeriod, + + #[error("the provided coin value is zero")] + ZeroAmount, + + #[error("the periodic spend limit of {periodic} was set to be higher than the total spend limit {total_limit}")] + PeriodicGrantOverSpendLimit { periodic: Coin, total_limit: Coin }, + + #[error("the accumulation spend limit of {accumulation} was set to be lower than the periodic grant amount of {periodic_grant}")] + AccumulationBelowGrantAmount { + accumulation: Coin, + periodic_grant: Coin, + }, + + #[error("the accumulation spend limit of {accumulation} was set to be higher than the total spend limit of {total_limit}")] + AccumulationOverSpendLimit { + accumulation: Coin, + total_limit: Coin, + }, + + #[error("the specified delayed allowance would never be available. it would become active at {available_timestamp} yet it expires at {expiration_timestamp}")] + UnattainableDelayedAllowance { + expiration_timestamp: u64, + available_timestamp: u64, + }, + + #[error("could not unlock {requested} tokens from {grantee}. it only has {locked} locked")] + InsufficientLockedTokens { + grantee: String, + locked: Uint128, + requested: Uint128, + }, + + #[error("attempted to spend more tokens than permitted by the current allowance")] + SpendingAboveAllowance, + + #[error("attempted to send an empty allowance usage request")] + EmptyUsageRequest, + + #[error("the associated grant has already expired")] + GrantExpired, + + #[error("the associated grant hasn't expired yet")] + GrantNotExpired, + + #[error("this grant is not available yet. it will become usable at {available_at_timestamp}")] + GrantNotYetAvailable { available_at_timestamp: u64 }, +} diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/src/lib.rs b/common/cosmwasm-smart-contracts/nym-pool-contract/src/lib.rs new file mode 100644 index 0000000000..faa1b98b99 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/src/lib.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod constants; +pub mod error; +pub mod msg; +pub mod types; +mod utils; + +pub use error::*; +pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/src/msg.rs b/common/cosmwasm-smart-contracts/nym-pool-contract/src/msg.rs new file mode 100644 index 0000000000..734cb46d28 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/src/msg.rs @@ -0,0 +1,125 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{Allowance, TransferRecipient}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Coin; +use std::collections::HashMap; + +#[cfg(feature = "schema")] +use crate::types::{ + AvailableTokensResponse, GrantResponse, GranterResponse, GrantersPagedResponse, + GrantsPagedResponse, LockedTokensPagedResponse, LockedTokensResponse, + TotalLockedTokensResponse, +}; + +#[cw_serde] +pub struct InstantiateMsg { + pub pool_denomination: String, + + /// Initial map of grants to be created at instantiation + pub grants: HashMap, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { + admin: String, + // flag to determine whether old admin should be removed from the granter set + // and new one should be included instead + // the reason it's provided as an option is to make it possible to skip this field + // when creating transaction directly with nyxd + update_granter_set: Option, + }, + + /// Attempt to grant new allowance to the specified grantee + GrantAllowance { + grantee: String, + allowance: Box, + }, + + /// Attempt to revoke previously granted allowance + RevokeAllowance { grantee: String }, + + /// Attempt to use allowance + UseAllowance { recipients: Vec }, + + /// Attempt to withdraw the specified amount into the grantee's account + WithdrawAllowance { amount: Coin }, + + /// Attempt to lock part of existing allowance for future use + LockAllowance { amount: Coin }, + + /// Attempt to unlock previously locked allowance + UnlockAllowance { amount: Coin }, + + /// Attempt to use part of the locked allowance + UseLockedAllowance { recipients: Vec }, + + /// Attempt to withdraw the specified amount of locked tokens into the grantee's account + WithdrawLockedAllowance { amount: Coin }, + + /// Attempt to add a new account to the permitted set of grant granters + AddNewGranter { granter: String }, + + /// Revoke the provided account from the permitted set of granters + RevokeGranter { granter: String }, + + /// Attempt to remove expired grant from the storage and unlock (if any) locked tokens + RemoveExpiredGrant { grantee: String }, +} + +#[cw_serde] +#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))] +pub enum QueryMsg { + #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] + Admin {}, + + #[cfg_attr(feature = "schema", returns(AvailableTokensResponse))] + GetAvailableTokens {}, + + #[cfg_attr(feature = "schema", returns(TotalLockedTokensResponse))] + GetTotalLockedTokens {}, + + #[cfg_attr(feature = "schema", returns(LockedTokensResponse))] + GetLockedTokens { grantee: String }, + + #[cfg_attr(feature = "schema", returns(GrantResponse))] + GetGrant { grantee: String }, + + #[cfg_attr(feature = "schema", returns(GranterResponse))] + GetGranter { granter: String }, + + #[cfg_attr(feature = "schema", returns(LockedTokensPagedResponse))] + GetLockedTokensPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + #[cfg_attr(feature = "schema", returns(GrantersPagedResponse))] + GetGrantersPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + #[cfg_attr(feature = "schema", returns(GrantsPagedResponse))] + GetGrantsPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, +} + +#[cw_serde] +pub struct MigrateMsg { + // +} diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/src/types.rs b/common/cosmwasm-smart-contracts/nym-pool-contract/src/types.rs new file mode 100644 index 0000000000..8bffe7dc6a --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/src/types.rs @@ -0,0 +1,1279 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin}; + +pub type GranterAddress = Addr; +pub type GranteeAddress = Addr; + +pub use grants::*; +pub use query_responses::*; + +#[cw_serde] +pub struct TransferRecipient { + pub recipient: String, + pub amount: Coin, +} + +pub mod grants { + use crate::utils::ensure_unix_timestamp_not_in_the_past; + use crate::{GranteeAddress, GranterAddress, NymPoolContractError}; + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Addr, Coin, Env, Timestamp, Uint128}; + use std::cmp::min; + + #[cw_serde] + pub struct GranterInformation { + // realistically this is always going to be the contract admin, + // but let's keep this metadata regardless just in case it ever changes, + // such as we create a granter controlled by validator multisig or governance + pub created_by: Addr, + pub created_at_height: u64, + } + + #[cw_serde] + pub struct Grant { + pub granter: GranterAddress, + pub grantee: GranteeAddress, + pub granted_at_height: u64, + pub allowance: Allowance, + } + + #[cw_serde] + pub enum Allowance { + Basic(BasicAllowance), + ClassicPeriodic(ClassicPeriodicAllowance), + CumulativePeriodic(CumulativePeriodicAllowance), + Delayed(DelayedAllowance), + } + + impl From for Allowance { + fn from(value: BasicAllowance) -> Self { + Allowance::Basic(value) + } + } + + impl From for Allowance { + fn from(value: ClassicPeriodicAllowance) -> Self { + Allowance::ClassicPeriodic(value) + } + } + + impl From for Allowance { + fn from(value: CumulativePeriodicAllowance) -> Self { + Allowance::CumulativePeriodic(value) + } + } + + impl From for Allowance { + fn from(value: DelayedAllowance) -> Self { + Allowance::Delayed(value) + } + } + + impl Allowance { + pub fn expired(&self, env: &Env) -> bool { + self.basic().expired(env) + } + + pub fn basic(&self) -> &BasicAllowance { + match self { + Allowance::Basic(allowance) => allowance, + Allowance::ClassicPeriodic(allowance) => &allowance.basic, + Allowance::CumulativePeriodic(allowance) => &allowance.basic, + Allowance::Delayed(allowance) => &allowance.basic, + } + } + + pub fn basic_mut(&mut self) -> &mut BasicAllowance { + match self { + Allowance::Basic(ref mut allowance) => allowance, + Allowance::ClassicPeriodic(ref mut allowance) => &mut allowance.basic, + Allowance::CumulativePeriodic(ref mut allowance) => &mut allowance.basic, + Allowance::Delayed(ref mut allowance) => &mut allowance.basic, + } + } + + pub fn expiration(&self) -> Option { + let expiration_unix = match self { + Allowance::Basic(allowance) => allowance.expiration_unix_timestamp, + Allowance::ClassicPeriodic(allowance) => allowance.basic.expiration_unix_timestamp, + Allowance::CumulativePeriodic(allowance) => { + allowance.basic.expiration_unix_timestamp + } + Allowance::Delayed(allowance) => allowance.basic.expiration_unix_timestamp, + }; + + expiration_unix.map(Timestamp::from_seconds) + } + + /// Perform validation of a new grant that's to be created + pub fn validate_new(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> { + // 1. perform validation on the inner, basic, allowance + self.basic().validate(env, denom)?; + + // 2. perform additional validation specific to each variant + match self { + // we already validated basic allowance + Allowance::Basic(_) => Ok(()), + Allowance::ClassicPeriodic(allowance) => allowance.validate_new_inner(denom), + Allowance::CumulativePeriodic(allowance) => allowance.validate_new_inner(denom), + Allowance::Delayed(allowance) => allowance.validate_new_inner(env), + } + } + + /// Updates initial state of this allowance settings things such as period reset timestamps. + pub fn set_initial_state(&mut self, env: &Env) { + match self { + // nothing to do for the basic allowance + Allowance::Basic(_) => {} + Allowance::ClassicPeriodic(allowance) => allowance.set_initial_state(env), + Allowance::CumulativePeriodic(allowance) => allowance.set_initial_state(env), + // nothing to do for the delayed allowance + Allowance::Delayed(_) => {} + } + } + + pub fn try_update_state(&mut self, env: &Env) { + match self { + // nothing to do for the basic allowance + Allowance::Basic(_) => {} + Allowance::ClassicPeriodic(allowance) => allowance.try_update_state(env), + Allowance::CumulativePeriodic(allowance) => allowance.try_update_state(env), + // nothing to do for the delayed allowance + Allowance::Delayed(_) => {} + } + } + + pub fn within_spendable_limits(&self, amount: &Coin) -> bool { + match self { + Allowance::Basic(allowance) => allowance.within_spendable_limits(amount), + Allowance::ClassicPeriodic(allowance) => allowance.within_spendable_limits(amount), + Allowance::CumulativePeriodic(allowance) => { + allowance.within_spendable_limits(amount) + } + Allowance::Delayed(allowance) => allowance.within_spendable_limits(amount), + } + } + + // check whether given the current allowance state, the provided amount could be spent + // note: it's responsibility of the caller to call `try_update_state` before the call. + pub fn ensure_can_spend( + &self, + env: &Env, + amount: &Coin, + ) -> Result<(), NymPoolContractError> { + match self { + Allowance::Basic(allowance) => allowance.ensure_can_spend(env, amount), + Allowance::ClassicPeriodic(allowance) => allowance.ensure_can_spend(env, amount), + Allowance::CumulativePeriodic(allowance) => allowance.ensure_can_spend(env, amount), + Allowance::Delayed(allowance) => allowance.ensure_can_spend(env, amount), + } + } + + pub fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + self.try_update_state(env); + + match self { + Allowance::Basic(allowance) => allowance.try_spend(env, amount), + Allowance::ClassicPeriodic(allowance) => allowance.try_spend(env, amount), + Allowance::CumulativePeriodic(allowance) => allowance.try_spend(env, amount), + Allowance::Delayed(allowance) => allowance.try_spend(env, amount), + } + } + + pub fn increase_spend_limit(&mut self, amount: Uint128) { + if let Some(ref mut limit) = self.basic_mut().spend_limit { + limit.amount += amount + } + } + + pub fn is_used_up(&self) -> bool { + let Some(ref limit) = self.basic().spend_limit else { + return false; + }; + limit.amount.is_zero() + } + } + + /// BasicAllowance is an allowance with a one-time grant of coins + /// that optionally expires. The grantee can use up to SpendLimit to cover fees. + #[cw_serde] + pub struct BasicAllowance { + /// spend_limit specifies the maximum amount of coins that can be spent + /// by this allowance and will be updated as coins are spent. If it is + /// empty, there is no spend limit and any amount of coins can be spent. + pub spend_limit: Option, + + /// expiration specifies an optional time when this allowance expires + pub expiration_unix_timestamp: Option, + } + + impl BasicAllowance { + pub fn unlimited() -> BasicAllowance { + BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: None, + } + } + + pub fn validate(&self, env: &Env, denom: &str) -> Result<(), NymPoolContractError> { + // expiration shouldn't be in the past. + if let Some(expiration) = self.expiration_unix_timestamp { + ensure_unix_timestamp_not_in_the_past(expiration, env)?; + } + + // if spend limit is set, it must use the same denomination as the underlying pool + if let Some(ref spend_limit) = self.spend_limit { + if spend_limit.denom != denom { + return Err(NymPoolContractError::InvalidDenom { + expected: denom.to_string(), + got: spend_limit.denom.to_string(), + }); + } + + if spend_limit.amount.is_zero() { + return Err(NymPoolContractError::ZeroAmount); + } + } + + Ok(()) + } + + pub fn expired(&self, env: &Env) -> bool { + let Some(expiration) = self.expiration_unix_timestamp else { + return false; + }; + let current_unix_timestamp = env.block.time.seconds(); + + expiration < current_unix_timestamp + } + + fn within_spendable_limits(&self, amount: &Coin) -> bool { + let Some(ref spend_limit) = self.spend_limit else { + // if there's no spend limit then whatever the amount is, it's spendable + return true; + }; + + spend_limit.amount >= amount.amount + } + + fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + if self.expired(env) { + return Err(NymPoolContractError::GrantExpired); + } + if !self.within_spendable_limits(amount) { + return Err(NymPoolContractError::SpendingAboveAllowance); + } + Ok(()) + } + + fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + self.ensure_can_spend(env, amount)?; + + if let Some(ref mut spend_limit) = self.spend_limit { + spend_limit.amount -= amount.amount; + } + Ok(()) + } + } + + /// ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, + /// as well as a limit per time period. + #[cw_serde] + pub struct ClassicPeriodicAllowance { + /// basic specifies a struct of `BasicAllowance` + pub basic: BasicAllowance, + + /// period_duration_secs specifies the time duration in which period_spend_limit coins can + /// be spent before that allowance is reset + pub period_duration_secs: u64, + + /// period_spend_limit specifies the maximum number of coins that can be spent + /// in the period + pub period_spend_limit: Coin, + + /// period_can_spend is the number of coins left to be spent before the period_reset time + // set by the contract during initialisation of the grant + #[serde(default)] + pub period_can_spend: Option, + + /// period_reset is the time at which this period resets and a new one begins, + /// it is calculated from the start time of the first transaction after the + /// last period ended + // set by the contract during initialisation of the grant + #[serde(default)] + pub period_reset_unix_timestamp: u64, + } + + impl ClassicPeriodicAllowance { + pub(super) fn validate_new_inner(&self, denom: &str) -> Result<(), NymPoolContractError> { + // period duration shouldn't be zero + if self.period_duration_secs == 0 { + return Err(NymPoolContractError::ZeroAllowancePeriod); + } + + // the denom for period spend limit must match the expected value + if self.period_spend_limit.denom != denom { + return Err(NymPoolContractError::InvalidDenom { + expected: denom.to_string(), + got: self.period_spend_limit.denom.to_string(), + }); + } + + if self.period_spend_limit.amount.is_zero() { + return Err(NymPoolContractError::ZeroAmount); + } + + // if the basic spend limit is set, the period spend limit cannot be larger than it + if let Some(ref basic_limit) = self.basic.spend_limit { + if basic_limit.amount < self.period_spend_limit.amount { + return Err(NymPoolContractError::PeriodicGrantOverSpendLimit { + periodic: self.period_spend_limit.clone(), + total_limit: basic_limit.clone(), + }); + } + } + + Ok(()) + } + + /// The value that can be spent in the period is the lesser of the basic spend limit + /// and the period spend limit + /// + /// ```go + /// if _, isNeg := a.Basic.SpendLimit.SafeSub(a.PeriodSpendLimit...); isNeg && !a.Basic.SpendLimit.Empty() { + /// a.PeriodCanSpend = a.Basic.SpendLimit + /// } else { + /// a.PeriodCanSpend = a.PeriodSpendLimit + /// } + /// ``` + fn determine_period_can_spend(&self) -> Coin { + let Some(ref basic_limit) = self.basic.spend_limit else { + // if there's no spend limit, there's nothing to compare against + return self.period_spend_limit.clone(); + }; + + if basic_limit.amount < self.period_spend_limit.amount { + basic_limit.clone() + } else { + self.period_spend_limit.clone() + } + } + + pub(super) fn set_initial_state(&mut self, env: &Env) { + self.try_update_state(env); + } + + /// try_update_state will check if the period_reset_unix_timestamp has been hit. If not, it is a no-op. + /// If we hit the reset period, it will top up the period_can_spend amount to + /// min(period_spend_limit, basic.spend_limit) so it is never more than the maximum allowed. + /// It will also update the period_reset_unix_timestamp. + /// + /// If we are within one period, it will update from the + /// last period_reset (eg. if you always do one tx per day, it will always reset the same time) + /// If we are more than one period out (eg. no activity in a week), reset is one period from the execution of this method + pub fn try_update_state(&mut self, env: &Env) { + if env.block.time.seconds() < self.period_reset_unix_timestamp { + // we haven't yet reached the reset time + return; + } + self.period_can_spend = Some(self.determine_period_can_spend()); + + // If we are within the period, step from expiration (eg. if you always do one tx per day, + // it will always reset the same time) + // If we are more then one period out (eg. no activity in a week), + // reset is one period from this time + self.period_reset_unix_timestamp += self.period_duration_secs; + if env.block.time.seconds() > self.period_duration_secs { + self.period_reset_unix_timestamp = + env.block.time.seconds() + self.period_duration_secs; + } + } + + fn within_spendable_limits(&self, amount: &Coin) -> bool { + let Some(ref available) = self.period_can_spend else { + return false; + }; + available.amount >= amount.amount + } + + fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + if self.basic.expired(env) { + return Err(NymPoolContractError::GrantExpired); + } + if !self.within_spendable_limits(amount) { + return Err(NymPoolContractError::SpendingAboveAllowance); + } + Ok(()) + } + + fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + self.ensure_can_spend(env, amount)?; + + // deduct from both the current period and the max amount + if let Some(ref mut spend_limit) = self.basic.spend_limit { + spend_limit.amount -= amount.amount; + } + + // SAFETY: initial `period_can_spend` value is always unconditionally set by the contract during + // grant creation + #[allow(clippy::unwrap_used)] + let period_can_spend = self.period_can_spend.as_mut().unwrap(); + period_can_spend.amount -= amount.amount; + + Ok(()) + } + } + + #[cw_serde] + pub struct CumulativePeriodicAllowance { + /// basic specifies a struct of `BasicAllowance` + pub basic: BasicAllowance, + + /// period_duration_secs specifies the time duration in which spendable coins can + /// be spent before that allowance is incremented + pub period_duration_secs: u64, + + /// period_grant specifies the maximum number of coins that is granted per period + pub period_grant: Coin, + + /// accumulation_limit is the maximum value the grants and accumulate to + pub accumulation_limit: Option, + + /// spendable is the number of coins left to be spent before additional grant is applied + // set by the contract during initialisation of the grant + #[serde(default)] + pub spendable: Option, + + /// last_grant_applied is the time at which last transaction associated with this allowance + /// has been sent and `spendable` value has been adjusted + // set by the contract during initialisation of the grant + #[serde(default)] + pub last_grant_applied_unix_timestamp: u64, + } + + impl CumulativePeriodicAllowance { + pub(super) fn validate_new_inner(&self, denom: &str) -> Result<(), NymPoolContractError> { + // period duration shouldn't be zero + if self.period_duration_secs == 0 { + return Err(NymPoolContractError::ZeroAllowancePeriod); + } + + // the denom for period grant must match the expected value + if self.period_grant.denom != denom { + return Err(NymPoolContractError::InvalidDenom { + expected: denom.to_string(), + got: self.period_grant.denom.to_string(), + }); + } + + if self.period_grant.amount.is_zero() { + return Err(NymPoolContractError::ZeroAmount); + } + + // the period grant must not be larger than the total spend limit, if set + if let Some(ref basic_limit) = self.basic.spend_limit { + if basic_limit.amount < self.period_grant.amount { + return Err(NymPoolContractError::PeriodicGrantOverSpendLimit { + periodic: self.period_grant.clone(), + total_limit: basic_limit.clone(), + }); + } + } + + if let Some(ref accumulation_limit) = self.accumulation_limit { + // if set, the accumulation limit must not be smaller than the period grant + if accumulation_limit.amount < self.period_grant.amount { + return Err(NymPoolContractError::AccumulationBelowGrantAmount { + accumulation: accumulation_limit.clone(), + periodic_grant: self.period_grant.clone(), + }); + } + + // if set, the denom for accumulation limit must match the expected value + if accumulation_limit.denom != denom { + return Err(NymPoolContractError::InvalidDenom { + expected: denom.to_string(), + got: accumulation_limit.denom.to_string(), + }); + } + + // if set, the accumulation limit must not be larger than the total spend limit + if let Some(ref basic_limit) = self.basic.spend_limit { + if basic_limit.amount < accumulation_limit.amount { + return Err(NymPoolContractError::AccumulationOverSpendLimit { + accumulation: accumulation_limit.clone(), + total_limit: basic_limit.clone(), + }); + } + } + } + + Ok(()) + } + + pub(super) fn set_initial_state(&mut self, env: &Env) { + self.last_grant_applied_unix_timestamp = env.block.time.seconds(); + + // initially we can spend equivalent of a single grant + self.spendable = Some(self.period_grant.clone()) + } + + #[inline] + fn missed_periods(&self, env: &Env) -> u64 { + (env.block.time.seconds() - self.last_grant_applied_unix_timestamp) + % self.period_duration_secs + } + + /// The value that can be spent is the last of the basic spend limit, the accumulation limit + /// and number of missed periods multiplied by the period grant + fn determine_spendable(&self, env: &Env) -> Coin { + // SAFETY: initial `spendable` value is always unconditionally set by the contract during + // grant creation + #[allow(clippy::unwrap_used)] + let spendable = self.spendable.as_ref().unwrap(); + + let missed_periods = self.missed_periods(env); + let mut max_spendable = spendable.clone(); + max_spendable.amount += Uint128::new(missed_periods as u128) * self.period_grant.amount; + + match (&self.basic.spend_limit, &self.accumulation_limit) { + (Some(spend_limit), Some(accumulation_limit)) => { + let limit = min(spend_limit.amount, accumulation_limit.amount); + let amount = min(limit, max_spendable.amount); + Coin::new(amount, max_spendable.denom) + } + (None, Some(accumulation_limit)) => { + let amount = min(accumulation_limit.amount, max_spendable.amount); + Coin::new(amount, max_spendable.denom) + } + (Some(spend_limit), None) => { + let amount = min(spend_limit.amount, max_spendable.amount); + Coin::new(amount, max_spendable.denom) + } + (None, None) => max_spendable, + } + } + + /// try_update_state will check if we've rolled over into the next grant period. If not, it is a no-op. + /// If we hit the next period, it will top up the spendable amount to + /// min(accumulation_limit, basic.spend_limit, spendable + period_grant * num_missed_periods) so it is never more than the maximum allowed. + /// It will also update the last_grant_applied_unix_timestamp. + pub fn try_update_state(&mut self, env: &Env) { + let missed_periods = self.missed_periods(env); + + if missed_periods == 0 { + // we haven't yet reached the next grant time + return; + } + + self.spendable = Some(self.determine_spendable(env)) + } + + fn within_spendable_limits(&self, amount: &Coin) -> bool { + let Some(ref available) = self.spendable else { + return false; + }; + available.amount >= amount.amount + } + + fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + if self.basic.expired(env) { + return Err(NymPoolContractError::GrantExpired); + } + if !self.within_spendable_limits(amount) { + return Err(NymPoolContractError::SpendingAboveAllowance); + } + Ok(()) + } + + fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + self.ensure_can_spend(env, amount)?; + + // deduct from both the current period and the max amount + if let Some(ref mut spend_limit) = self.basic.spend_limit { + spend_limit.amount -= amount.amount; + } + + // SAFETY: initial `spendable` value is always unconditionally set by the contract during + // grant creation + #[allow(clippy::unwrap_used)] + let spendable = self.spendable.as_mut().unwrap(); + spendable.amount -= amount.amount; + + Ok(()) + } + } + + /// Create a grant to allow somebody to withdraw from the pool only after the specified time. + /// For example, we could create a grant for mixnet rewarding/testing/etc + /// However, if the required work has not been completed, the grant could be revoked before it's withdrawn + #[cw_serde] + pub struct DelayedAllowance { + /// basic specifies a struct of `BasicAllowance` + pub basic: BasicAllowance, + + /// available_at specifies when this allowance is going to become usable + pub available_at_unix_timestamp: u64, + } + + impl DelayedAllowance { + pub(super) fn validate_new_inner(&self, env: &Env) -> Result<(), NymPoolContractError> { + // available at must be set in the future + ensure_unix_timestamp_not_in_the_past(self.available_at_unix_timestamp, env)?; + + // and it must become available before the underlying allowance expires + if let Some(expiration) = self.basic.expiration_unix_timestamp { + if expiration < self.available_at_unix_timestamp { + return Err(NymPoolContractError::UnattainableDelayedAllowance { + expiration_timestamp: expiration, + available_timestamp: self.available_at_unix_timestamp, + }); + } + } + + Ok(()) + } + + fn within_spendable_limits(&self, amount: &Coin) -> bool { + self.basic.within_spendable_limits(amount) + } + + fn ensure_can_spend(&self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + if self.basic.expired(env) { + return Err(NymPoolContractError::GrantExpired); + } + if !self.within_spendable_limits(amount) { + return Err(NymPoolContractError::SpendingAboveAllowance); + } + if self.available_at_unix_timestamp < env.block.time.seconds() { + return Err(NymPoolContractError::GrantNotYetAvailable { + available_at_timestamp: self.available_at_unix_timestamp, + }); + } + + Ok(()) + } + + fn try_spend(&mut self, env: &Env, amount: &Coin) -> Result<(), NymPoolContractError> { + self.ensure_can_spend(env, amount)?; + + if let Some(ref mut spend_limit) = self.basic.spend_limit { + spend_limit.amount -= amount.amount; + } + + Ok(()) + } + } +} + +pub mod query_responses { + use crate::{Grant, GranteeAddress, GranterAddress, GranterInformation}; + use cosmwasm_schema::cw_serde; + use cosmwasm_std::Coin; + + #[cw_serde] + pub struct AvailableTokensResponse { + pub available: Coin, + } + + #[cw_serde] + pub struct TotalLockedTokensResponse { + pub locked: Coin, + } + + #[cw_serde] + pub struct LockedTokensResponse { + pub grantee: GranteeAddress, + + pub locked: Option, + } + + #[cw_serde] + pub struct GrantInformation { + pub grant: Grant, + pub expired: bool, + } + + #[cw_serde] + pub struct GrantResponse { + pub grantee: GranteeAddress, + pub grant: Option, + } + + #[cw_serde] + pub struct GranterResponse { + pub granter: GranterAddress, + pub information: Option, + } + + #[cw_serde] + pub struct GrantsPagedResponse { + pub grants: Vec, + pub start_next_after: Option, + } + + #[cw_serde] + pub struct GranterDetails { + pub granter: GranterAddress, + pub information: GranterInformation, + } + + impl From<(GranterAddress, GranterInformation)> for GranterDetails { + fn from((granter, information): (GranterAddress, GranterInformation)) -> Self { + GranterDetails { + granter, + information, + } + } + } + + #[cw_serde] + pub struct GrantersPagedResponse { + pub granters: Vec, + pub start_next_after: Option, + } + + #[cw_serde] + pub struct LockedTokens { + pub grantee: GranteeAddress, + pub locked: Coin, + } + + #[cw_serde] + pub struct LockedTokensPagedResponse { + pub locked: Vec, + pub start_next_after: Option, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::{coin, Uint128}; + + const TEST_DENOM: &str = "unym"; + + fn mock_basic_allowance() -> BasicAllowance { + BasicAllowance { + spend_limit: Some(coin(100000, TEST_DENOM)), + expiration_unix_timestamp: Some(1643652000), + } + } + + fn mock_classic_periodic_allowance() -> ClassicPeriodicAllowance { + ClassicPeriodicAllowance { + basic: mock_basic_allowance(), + period_duration_secs: 10, + period_spend_limit: coin(1000, TEST_DENOM), + period_can_spend: None, + period_reset_unix_timestamp: 0, + } + } + + fn mock_cumulative_periodic_allowance() -> CumulativePeriodicAllowance { + CumulativePeriodicAllowance { + basic: mock_basic_allowance(), + period_duration_secs: 10, + period_grant: coin(1000, TEST_DENOM), + accumulation_limit: Some(coin(10000, TEST_DENOM)), + spendable: None, + last_grant_applied_unix_timestamp: 0, + } + } + + fn mock_delayed_allowance() -> DelayedAllowance { + DelayedAllowance { + basic: mock_basic_allowance(), + available_at_unix_timestamp: 1643650000, + } + } + + #[test] + fn increasing_spend_limit() { + // no-op if there's no limit + let mut basic = mock_basic_allowance(); + basic.spend_limit = None; + let mut basic = Allowance::Basic(basic); + + let mut classic = mock_classic_periodic_allowance(); + classic.basic.spend_limit = None; + let mut classic = Allowance::ClassicPeriodic(classic); + + let mut cumulative = mock_cumulative_periodic_allowance(); + cumulative.basic.spend_limit = None; + let mut cumulative = Allowance::CumulativePeriodic(cumulative); + + let mut delayed = mock_delayed_allowance(); + delayed.basic.spend_limit = None; + let mut delayed = Allowance::Delayed(delayed); + + let basic_og = basic.clone(); + let classic_og = classic.clone(); + let cumulative_og = cumulative.clone(); + let delayed_og = delayed.clone(); + + basic.increase_spend_limit(Uint128::new(100)); + classic.increase_spend_limit(Uint128::new(100)); + cumulative.increase_spend_limit(Uint128::new(100)); + delayed.increase_spend_limit(Uint128::new(100)); + + assert_eq!(basic, basic_og); + assert_eq!(classic, classic_og); + assert_eq!(cumulative, cumulative_og); + assert_eq!(delayed, delayed_og); + + // adds to spend limit otherwise + let limit = coin(1000, TEST_DENOM); + let mut basic = mock_basic_allowance(); + basic.spend_limit = Some(limit.clone()); + let mut basic = Allowance::Basic(basic); + + let mut classic = mock_classic_periodic_allowance(); + classic.basic.spend_limit = Some(limit.clone()); + let mut classic = Allowance::ClassicPeriodic(classic); + + let mut cumulative = mock_cumulative_periodic_allowance(); + cumulative.basic.spend_limit = Some(limit.clone()); + let mut cumulative = Allowance::CumulativePeriodic(cumulative); + + let mut delayed = mock_delayed_allowance(); + delayed.basic.spend_limit = Some(limit.clone()); + let mut delayed = Allowance::Delayed(delayed); + + basic.increase_spend_limit(Uint128::new(100)); + classic.increase_spend_limit(Uint128::new(100)); + cumulative.increase_spend_limit(Uint128::new(100)); + delayed.increase_spend_limit(Uint128::new(100)); + + assert_eq!( + basic.basic().spend_limit.as_ref().unwrap().amount, + limit.amount + Uint128::new(100) + ); + assert_eq!( + classic.basic().spend_limit.as_ref().unwrap().amount, + limit.amount + Uint128::new(100) + ); + assert_eq!( + cumulative.basic().spend_limit.as_ref().unwrap().amount, + limit.amount + Uint128::new(100) + ); + assert_eq!( + delayed.basic().spend_limit.as_ref().unwrap().amount, + limit.amount + Uint128::new(100) + ); + } + + #[cfg(test)] + mod validating_new_allowances { + use super::*; + + #[cfg(test)] + mod basic_allowance { + use super::*; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Timestamp; + + #[test] + fn doesnt_allow_expirations_in_the_past() { + let mut allowance = mock_basic_allowance(); + + let mut env = mock_env(); + + // allowance expiration is in the past + env.block.time = + Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() + 1); + assert!(allowance.validate(&env, TEST_DENOM).is_err()); + + // allowance expiration is equal to the current block time + env.block.time = + Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap()); + assert!(allowance.validate(&env, TEST_DENOM).is_ok()); + + // allowance expiration is in the future + env.block.time = + Timestamp::from_seconds(allowance.expiration_unix_timestamp.unwrap() - 1); + assert!(allowance.validate(&env, TEST_DENOM).is_ok()); + + // no explicit expiration + allowance.expiration_unix_timestamp = None; + assert!(allowance.validate(&env, TEST_DENOM).is_ok()); + } + + #[test] + fn spend_limit_must_match_expected_denom() { + let mut allowance = mock_basic_allowance(); + + let env = mock_env(); + + // mismatched denom + assert!(allowance.validate(&env, "baddenom").is_err()); + + // matched denom + assert!(allowance.validate(&env, TEST_DENOM).is_ok()); + + // no spend limit + allowance.spend_limit = None; + assert!(allowance.validate(&env, TEST_DENOM).is_ok()); + } + + #[test] + fn spend_limit_must_be_non_zero() { + let mut allowance = mock_basic_allowance(); + + let env = mock_env(); + + // zero amount + allowance.spend_limit = Some(coin(0, TEST_DENOM)); + assert!(allowance.validate(&env, TEST_DENOM).is_err()); + + // non-zero amount + allowance.spend_limit = Some(coin(69, TEST_DENOM)); + assert!(allowance.validate(&env, TEST_DENOM).is_ok()); + } + } + + #[cfg(test)] + mod classic_periodic_allowance { + use super::*; + use crate::NymPoolContractError; + + #[test] + fn period_duration_must_be_nonzero() { + let mut allowance = mock_classic_periodic_allowance(); + + allowance.period_duration_secs = 0; + assert_eq!( + allowance.validate_new_inner(TEST_DENOM).unwrap_err(), + NymPoolContractError::ZeroAllowancePeriod + ); + + allowance.period_duration_secs = 1; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn spend_limit_must_match_expected_denom() { + let allowance = mock_classic_periodic_allowance(); + + // mismatched denom + assert!(allowance.validate_new_inner("baddenom").is_err()); + + // matched denom + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn spend_limit_must_be_non_zero() { + let mut allowance = mock_classic_periodic_allowance(); + + // zero amount + allowance.period_spend_limit = coin(0, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_err()); + + // non-zero amount + allowance.period_spend_limit = coin(69, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn period_spend_limit_must_be_smaller_than_total_limit() { + let mut allowance = mock_classic_periodic_allowance(); + + let total_limit = coin(1000, TEST_DENOM); + allowance.basic.spend_limit = Some(total_limit); + + // above total spend limit + allowance.period_spend_limit = coin(1001, TEST_DENOM); + assert!(matches!( + allowance.validate_new_inner(TEST_DENOM).unwrap_err(), + NymPoolContractError::PeriodicGrantOverSpendLimit { .. } + )); + + // below total spend limit + allowance.period_spend_limit = coin(999, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // equal to total spend limit + allowance.period_spend_limit = coin(1000, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // no total spend limit + allowance.basic.spend_limit = None; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + } + + #[cfg(test)] + mod cumulative_periodic_allowance { + use super::*; + use crate::NymPoolContractError; + + #[test] + fn period_duration_must_be_nonzero() { + let mut allowance = mock_cumulative_periodic_allowance(); + + allowance.period_duration_secs = 0; + assert_eq!( + allowance.validate_new_inner(TEST_DENOM).unwrap_err(), + NymPoolContractError::ZeroAllowancePeriod + ); + + allowance.period_duration_secs = 1; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn grant_must_match_expected_denom() { + let allowance = mock_cumulative_periodic_allowance(); + + // mismatched denom + assert!(allowance.validate_new_inner("baddenom").is_err()); + + // matched denom + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn grant_must_be_non_zero() { + let mut allowance = mock_cumulative_periodic_allowance(); + + // zero amount + allowance.period_grant = coin(0, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_err()); + + // non-zero amount + allowance.period_grant = coin(69, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn grant_amount_must_be_smaller_than_total_limit() { + let mut allowance = mock_cumulative_periodic_allowance(); + + let total_limit = coin(1000, TEST_DENOM); + allowance.basic.spend_limit = Some(total_limit); + allowance.accumulation_limit = None; + + // above total spend limit + allowance.period_grant = coin(1001, TEST_DENOM); + assert!(matches!( + allowance.validate_new_inner(TEST_DENOM).unwrap_err(), + NymPoolContractError::PeriodicGrantOverSpendLimit { .. } + )); + + // below total spend limit + allowance.period_grant = coin(999, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // equal to total spend limit + allowance.period_grant = coin(1000, TEST_DENOM); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // no total spend limit + allowance.basic.spend_limit = None; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn accumulation_limit_must_be_smaller_than_total_limit() { + let mut allowance = mock_cumulative_periodic_allowance(); + + let total_limit = coin(1000, TEST_DENOM); + allowance.basic.spend_limit = Some(total_limit.clone()); + allowance.period_grant = coin(500, TEST_DENOM); + + // above total spend limit + allowance.accumulation_limit = Some(coin(1001, TEST_DENOM)); + assert!(matches!( + allowance.validate_new_inner(TEST_DENOM).unwrap_err(), + NymPoolContractError::AccumulationOverSpendLimit { .. } + )); + + // below total spend limit + allowance.accumulation_limit = Some(coin(999, TEST_DENOM)); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // equal to total spend limit + allowance.accumulation_limit = Some(coin(1000, TEST_DENOM)); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // no total spend limit + allowance.basic.spend_limit = None; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // no accumulation limit + allowance.accumulation_limit = None; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // no accumulation limit but with spend limit + allowance.basic.spend_limit = Some(total_limit); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn accumulation_limit_must_not_be_smaller_than_grant_amount() { + let mut allowance = mock_cumulative_periodic_allowance(); + + let total_limit = coin(1000, TEST_DENOM); + allowance.basic.spend_limit = Some(total_limit); + allowance.period_grant = coin(500, TEST_DENOM); + + // below grant amount + allowance.accumulation_limit = Some(coin(499, TEST_DENOM)); + assert!(matches!( + allowance.validate_new_inner(TEST_DENOM).unwrap_err(), + NymPoolContractError::AccumulationBelowGrantAmount { .. } + )); + + // above grant amount + allowance.accumulation_limit = Some(coin(501, TEST_DENOM)); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // equal to grant amount + allowance.accumulation_limit = Some(coin(500, TEST_DENOM)); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + + // no accumulation limit + allowance.accumulation_limit = None; + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + + #[test] + fn accumulation_limit_must_match_expected_denom() { + let mut allowance = mock_cumulative_periodic_allowance(); + allowance.accumulation_limit = Some(coin(1000, "baddenom")); + + // mismatched denom + assert!(allowance.validate_new_inner(TEST_DENOM).is_err()); + + // matched denom + allowance.accumulation_limit = Some(coin(1000, TEST_DENOM)); + assert!(allowance.validate_new_inner(TEST_DENOM).is_ok()); + } + } + + #[cfg(test)] + mod delayed_allowance { + use super::*; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Timestamp; + + #[test] + fn doesnt_allow_availability_in_the_past() { + let allowance = mock_delayed_allowance(); + let mut env = mock_env(); + + // availability is in the past + env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp + 1); + assert!(allowance.validate_new_inner(&env).is_err()); + + // availability is equal to the current block time + env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp); + assert!(allowance.validate_new_inner(&env).is_ok()); + + // availability is in the future + env.block.time = Timestamp::from_seconds(allowance.available_at_unix_timestamp - 1); + assert!(allowance.validate_new_inner(&env).is_ok()); + } + + #[test] + fn must_have_available_before_allowance_expiration() { + let mut allowance = mock_delayed_allowance(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + allowance.basic.expiration_unix_timestamp = Some(1000); + + // after expiration + allowance.available_at_unix_timestamp = 1001; + assert!(allowance.validate_new_inner(&env).is_err()); + + // equal to expiration + allowance.available_at_unix_timestamp = 1000; + assert!(allowance.validate_new_inner(&env).is_ok()); + + // before expiration + allowance.available_at_unix_timestamp = 999; + assert!(allowance.validate_new_inner(&env).is_ok()); + + // with no explicit expiration + allowance.basic.expiration_unix_timestamp = None; + assert!(allowance.validate_new_inner(&env).is_ok()); + } + } + } + + #[cfg(test)] + mod setting_initial_state { + use super::*; + use cosmwasm_std::testing::mock_env; + + #[test] + fn basic_allowance() { + let mut basic = Allowance::Basic(mock_basic_allowance()); + + let og = basic.clone(); + + // this is a no-op + let env = mock_env(); + basic.set_initial_state(&env); + assert_eq!(basic, og); + } + + #[test] + fn classic_periodic_allowance() { + let mut inner = mock_classic_periodic_allowance(); + let mut cumulative = Allowance::ClassicPeriodic(inner.clone()); + + let env = mock_env(); + + let mut expected = inner.clone(); + + // sets the spendable amount to min(basic_limit, period_limit) + expected.period_can_spend = Some(expected.period_spend_limit.clone()); + + // set period reset to current block time + period duration + expected.period_reset_unix_timestamp = + env.block.time.seconds() + expected.period_duration_secs; + + inner.set_initial_state(&env); + assert_eq!(inner, expected); + + cumulative.set_initial_state(&env); + assert_eq!(cumulative, Allowance::ClassicPeriodic(inner)); + } + + #[test] + fn cumulative_periodic_allowance() { + let mut inner = mock_cumulative_periodic_allowance(); + let mut cumulative = Allowance::CumulativePeriodic(inner.clone()); + + let env = mock_env(); + + // sets the last applied grant to current time and spendable to a single grant value + let mut expected = inner.clone(); + expected.last_grant_applied_unix_timestamp = env.block.time.seconds(); + expected.spendable = Some(expected.period_grant.clone()); + + inner.set_initial_state(&env); + assert_eq!(inner, expected); + + cumulative.set_initial_state(&env); + assert_eq!(cumulative, Allowance::CumulativePeriodic(inner)); + } + + #[test] + fn delayed_allowance() { + let mut delayed = Allowance::Delayed(mock_delayed_allowance()); + + let og = delayed.clone(); + + // this is a no-op + let env = mock_env(); + delayed.set_initial_state(&env); + assert_eq!(delayed, og); + } + } +} diff --git a/common/cosmwasm-smart-contracts/nym-pool-contract/src/utils.rs b/common/cosmwasm-smart-contracts/nym-pool-contract/src/utils.rs new file mode 100644 index 0000000000..0b6f5ba1f4 --- /dev/null +++ b/common/cosmwasm-smart-contracts/nym-pool-contract/src/utils.rs @@ -0,0 +1,77 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::NymPoolContractError; +use cosmwasm_std::Env; + +pub fn ensure_unix_timestamp_not_in_the_past( + unix_timestamp: u64, + env: &Env, +) -> Result<(), NymPoolContractError> { + if unix_timestamp < env.block.time.seconds() { + return Err(NymPoolContractError::TimestampInThePast { + timestamp: unix_timestamp, + current_block_timestamp: env.block.time.seconds(), + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Timestamp; + use time::macros::datetime; + + #[test] + fn ensuring_unix_timestamp_not_in_the_past() { + let unix_epoch = 0; + + let date_in_the_past = datetime!(1984-01-02 3:45 UTC); + let sane_block_time = datetime!(2025-01-28 12:15 UTC); + + let before_block = datetime!(2025-01-28 12:00 UTC); + let after_block = datetime!(2025-01-28 12:30 UTC); + + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(sane_block_time.unix_timestamp() as u64); + + let res = ensure_unix_timestamp_not_in_the_past(unix_epoch, &env).unwrap_err(); + assert_eq!( + NymPoolContractError::TimestampInThePast { + timestamp: unix_epoch, + current_block_timestamp: env.block.time.seconds(), + }, + res + ); + + let res = + ensure_unix_timestamp_not_in_the_past(date_in_the_past.unix_timestamp() as u64, &env) + .unwrap_err(); + assert_eq!( + NymPoolContractError::TimestampInThePast { + timestamp: date_in_the_past.unix_timestamp() as u64, + current_block_timestamp: env.block.time.seconds(), + }, + res + ); + + let res = ensure_unix_timestamp_not_in_the_past(before_block.unix_timestamp() as u64, &env) + .unwrap_err(); + assert_eq!( + NymPoolContractError::TimestampInThePast { + timestamp: before_block.unix_timestamp() as u64, + current_block_timestamp: env.block.time.seconds(), + }, + res + ); + + let res = + ensure_unix_timestamp_not_in_the_past(sane_block_time.unix_timestamp() as u64, &env); + assert!(res.is_ok()); + + let res = ensure_unix_timestamp_not_in_the_past(after_block.unix_timestamp() as u64, &env); + assert!(res.is_ok()); + } +} diff --git a/common/credential-proxy/Cargo.toml b/common/credential-proxy/Cargo.toml new file mode 100644 index 0000000000..9fb4248a05 --- /dev/null +++ b/common/credential-proxy/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "nym-credential-proxy-lib" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true } +bip39 = { workspace = true, features = ["zeroize"] } +bs58 = { workspace = true } +futures = { workspace = true } +humantime = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +time = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +tokio-util = { workspace = true, features = ["rt"] } +tracing = { workspace = true } +uuid = { workspace = true, features = ["serde"] } +url = { workspace = true } +zeroize = { workspace = true } + +nym-credentials = { path = "../credentials" } +nym-crypto = { path = "../crypto", features = ["asymmetric", "rand", "serde"] } +nym-credentials-interface = { path = "../credentials-interface" } +nym-credential-proxy-requests = { path = "../../nym-credential-proxy/nym-credential-proxy-requests" } +nym-ecash-signer-check = { path = "../ecash-signer-check" } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-validator-client = { path = "../client-libs/validator-client" } +nym-network-defaults = { path = "../network-defaults" } + +[dev-dependencies] +tempfile = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + +[lints] +workspace = true diff --git a/nym-credential-proxy/nym-credential-proxy/build.rs b/common/credential-proxy/build.rs similarity index 54% rename from nym-credential-proxy/nym-credential-proxy/build.rs rename to common/credential-proxy/build.rs index bdbdcf0d22..bedf911da7 100644 --- a/nym-credential-proxy/nym-credential-proxy/build.rs +++ b/common/credential-proxy/build.rs @@ -1,22 +1,31 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql b/common/credential-proxy/migrations/01_initial.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql rename to common/credential-proxy/migrations/01_initial.sql diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql b/common/credential-proxy/migrations/02_cherry_picking_chaos.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql rename to common/credential-proxy/migrations/02_cherry_picking_chaos.sql diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql b/common/credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql rename to common/credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql diff --git a/common/credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql b/common/credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql new file mode 100644 index 0000000000..78a3fad093 --- /dev/null +++ b/common/credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql @@ -0,0 +1,18 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +DROP TABLE global_expiration_date_signatures; + +CREATE TABLE global_expiration_date_signatures +( + expiration_date DATE NOT NULL, + epoch_id INTEGER NOT NULL, + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +) \ No newline at end of file diff --git a/common/credential-proxy/migrations/05_buffered_deposits.sql b/common/credential-proxy/migrations/05_buffered_deposits.sql new file mode 100644 index 0000000000..88b875a2a9 --- /dev/null +++ b/common/credential-proxy/migrations/05_buffered_deposits.sql @@ -0,0 +1,81 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE ecash_deposit +( + -- id assigned [by the contract] to the deposit + deposit_id INTEGER PRIMARY KEY NOT NULL, + + -- associated tx hash + deposit_tx_hash TEXT NOT NULL, + + -- indication of when the deposit request has been created + -- (so that based on block timestamp we could potentially determine latency) + requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + -- the amount put in the deposit (informative, as we expect this to change in the future) + deposit_amount TEXT NOT NULL, + + -- the private key generated for the purposes of the deposit (the public component has been put in the transaction) + ed25519_deposit_private_key BLOB NOT NULL +); + + +INSERT INTO ecash_deposit(deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key) +SELECT deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key +FROM ticketbook_deposit; + + +CREATE TABLE ecash_deposit_usage +( + deposit_id INTEGER PRIMARY KEY REFERENCES ecash_deposit (deposit_id), + ticketbooks_requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + client_pubkey BLOB NOT NULL, + request_uuid TEXT UNIQUE NOT NULL, + + -- this has to be improved later on to resume issuance or something, but for now it's fine + ticketbook_request_error TEXT +); + +INSERT INTO ecash_deposit_usage(deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid) +SELECT deposit_id, 0, client_pubkey, request_uuid +FROM ticketbook_deposit; + + +CREATE TABLE partial_blinded_wallet_new +( + corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + blinded_signature BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet_failure_new +( + corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + failure_message TEXT NOT NULL +); + +INSERT INTO partial_blinded_wallet_new +SELECT * +FROM partial_blinded_wallet; +INSERT INTO partial_blinded_wallet_failure_new +SELECT * +FROM partial_blinded_wallet_failure; + +DROP TABLE partial_blinded_wallet; +DROP TABLE partial_blinded_wallet_failure; +DROP TABLE ticketbook_deposit; + +ALTER TABLE partial_blinded_wallet_new + RENAME TO partial_blinded_wallet; +ALTER TABLE partial_blinded_wallet_failure_new + RENAME TO partial_blinded_wallet_failure; diff --git a/common/credential-proxy/src/deposits_buffer/helpers.rs b/common/credential-proxy/src/deposits_buffer/helpers.rs new file mode 100644 index 0000000000..d2d0814e01 --- /dev/null +++ b/common/credential-proxy/src/deposits_buffer/helpers.rs @@ -0,0 +1,101 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::storage::models::StorableEcashDeposit; +use nym_compact_ecash::WithdrawalRequest; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nyxd::{Coin, Hash}; +use time::OffsetDateTime; +use zeroize::Zeroizing; + +pub struct BufferedDeposit { + pub deposit_id: u32, + + // note: this type implements `ZeroizeOnDrop` + pub ed25519_private_key: ed25519::PrivateKey, +} + +impl TryFrom for BufferedDeposit { + type Error = CredentialProxyError; + + fn try_from(deposit: StorableEcashDeposit) -> Result { + let ed25519_private_key = ed25519::PrivateKey::from_bytes( + deposit.ed25519_deposit_private_key.as_ref(), + ) + .map_err(|err| CredentialProxyError::DatabaseInconsistency { + reason: format!("one of the stored deposit ed25519 private keys is malformed: {err}"), + })?; + + Ok(BufferedDeposit { + deposit_id: deposit.deposit_id, + ed25519_private_key, + }) + } +} + +impl BufferedDeposit { + pub fn new(deposit_id: u32, ed25519_private_key: ed25519::PrivateKey) -> Self { + BufferedDeposit { + deposit_id, + ed25519_private_key, + } + } + + pub fn sign_ticketbook_plaintext( + &self, + withdrawal_request: &WithdrawalRequest, + ) -> ed25519::Signature { + let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id); + self.ed25519_private_key.sign(plaintext) + } +} + +pub struct PerformedDeposits { + pub deposits_data: Vec, + + // shared by all performed deposits as they were included in the same tx + pub tx_hash: Hash, + pub requested_on: OffsetDateTime, + pub deposit_amount: Coin, +} + +impl PerformedDeposits { + pub(crate) fn to_storable(&self) -> Vec { + self.deposits_data + .iter() + .map(|d| StorableEcashDeposit { + deposit_id: d.deposit_id, + deposit_tx_hash: self.tx_hash.to_string(), + requested_on: self.requested_on, + deposit_amount: self.deposit_amount.to_string(), + ed25519_deposit_private_key: Zeroizing::new(d.ed25519_private_key.to_bytes()), + }) + .collect() + } +} + +pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator { + (0..total) + .step_by(max_request_size) + .map(move |start| std::cmp::min(max_request_size, total - start)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_sizes_test() { + assert_eq!( + request_sizes(100, 32).collect::>(), + vec![32, 32, 32, 4] + ); + + assert_eq!(request_sizes(10, 32).collect::>(), vec![10]); + assert_eq!(request_sizes(32, 32).collect::>(), vec![32]); + assert_eq!(request_sizes(33, 32).collect::>(), vec![32, 1]); + assert_eq!(request_sizes(1, 32).collect::>(), vec![1]); + } +} diff --git a/common/credential-proxy/src/deposits_buffer/mod.rs b/common/credential-proxy/src/deposits_buffer/mod.rs new file mode 100644 index 0000000000..59cbe51e7b --- /dev/null +++ b/common/credential-proxy/src/deposits_buffer/mod.rs @@ -0,0 +1,308 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::helpers::request_sizes; +use crate::deposits_buffer::refill_task::RefillTask; +use crate::error::CredentialProxyError; +use crate::shared_state::nyxd_client::ChainClient; +use crate::shared_state::required_deposit_cache::RequiredDepositCache; +use crate::storage::CredentialProxyStorage; +use nym_compact_ecash::PublicKeyUser; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; +use nym_validator_client::nyxd::Coin; +use rand::rngs::OsRng; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex as AsyncMutex; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, instrument, warn}; +use uuid::Uuid; + +pub use helpers::{BufferedDeposit, PerformedDeposits}; + +pub(crate) mod helpers; +mod refill_task; + +// TODO: I guess make it configurable +const DEPOSITS_THRESHOLD_P: f32 = 0.1; + +struct DepositsBufferInner { + client: ChainClient, + + required_deposit_cache: RequiredDepositCache, + + storage: CredentialProxyStorage, + target_amount: usize, + max_concurrent_deposits: usize, + unused_deposits: AsyncMutex>, + + deposits_refill_task: RefillTask, + short_sha: &'static str, + cancellation_token: CancellationToken, +} + +#[derive(Clone)] +pub struct DepositsBuffer { + inner: Arc, +} + +impl DepositsBuffer { + pub async fn new( + storage: CredentialProxyStorage, + client: ChainClient, + required_deposit_cache: RequiredDepositCache, + short_sha: &'static str, + target_amount: usize, + max_concurrent_deposits: usize, + cancellation_token: CancellationToken, + ) -> Result { + let unused_deposits = storage.load_unused_deposits().await?; + info!("managed to load {} deposits", unused_deposits.len()); + + Ok(DepositsBuffer { + inner: Arc::new(DepositsBufferInner { + client, + required_deposit_cache, + storage, + target_amount, + max_concurrent_deposits, + unused_deposits: AsyncMutex::new(unused_deposits), + deposits_refill_task: RefillTask::default(), + short_sha, + cancellation_token, + }), + }) + } + + async fn deposit_amount(&self) -> Result { + self.inner + .required_deposit_cache + .get_or_update(&self.inner.client) + .await + } + + #[instrument(skip(self), err(Display))] + async fn make_deposits_request( + &self, + amount: usize, + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); + let chain_write_permit = self.inner.client.start_chain_tx().await; + let mut rng = OsRng; + + let deposit_amount = self.deposit_amount().await?; + let keys = (0..amount) + .map(|_| ed25519::PrivateKey::new(&mut rng)) + .collect::>(); + + info!("starting {amount} deposits"); + let mut contents = Vec::new(); + for key in &keys { + let public_key: ed25519::PublicKey = key.into(); + contents.push((public_key.to_base58_string(), deposit_amount.clone())); + } + + let execute_res = chain_write_permit + .make_deposits(self.inner.short_sha, contents) + .await?; + + let tx_hash = execute_res.transaction_hash; + info!("{amount} deposits made in transaction: {tx_hash}"); + + let contract_data = match execute_res.to_contract_data() { + Ok(contract_data) => contract_data, + Err(err) => { + // that one is tricky. deposits technically got made, but we somehow failed to parse response, + // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted + // because it requires some serious MANUAL intervention + error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + }; + + if contract_data.len() != amount { + // another critical failure, that one should be quite impossible and thus has to be manually inspected + error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len()); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + + let mut deposits_data = Vec::new(); + for (key, response) in keys.into_iter().zip(contract_data) { + let response_index = response.message_index; + let deposit_id = match response.parse_singleton_u32_contract_data() { + Ok(deposit_id) => deposit_id, + Err(err) => { + // another impossibility + error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + }; + + deposits_data.push(BufferedDeposit::new(deposit_id, key)); + } + + Ok(PerformedDeposits { + deposits_data, + tx_hash, + requested_on, + deposit_amount, + }) + } + + async fn insert_new_deposits( + &self, + mut deposits: PerformedDeposits, + ) -> Result<(), CredentialProxyError> { + // 1. insert into the db + self.inner.storage.insert_new_deposits(&deposits).await?; + + // 2. update the buffer + self.inner + .unused_deposits + .lock() + .await + .append(&mut deposits.deposits_data); + Ok(()) + } + + /// Start refilling our deposit buffer. + /// It chunks the amount required based on the configured maximum request size + /// and updates global state after each successful transaction. + async fn refill_deposits(&self) -> Result<(), CredentialProxyError> { + let available = self.inner.unused_deposits.lock().await.len(); + + let target = self.deposits_upper_threshold(); + let to_request = target - available; + + for request_chunk in request_sizes(to_request, self.inner.max_concurrent_deposits) { + // note: we check for cancellation between individual requests + // as opposed to wrapping that in tokio::select! so that we would never abandon chain operations + // as we wouldn't want to lose funds + if self.inner.cancellation_token.is_cancelled() { + info!("received cancellation during deposits refilling"); + return Ok(()); + } + + // make sure to insert deposits into db/vec as we get them so on initial run, + // we'd start trickling down data as soon as possible + let deposits = self.make_deposits_request(request_chunk).await?; + self.insert_new_deposits(deposits).await?; + } + + Ok(()) + } + + // if we're here, we know we're below the threshold + fn maybe_refill_deposits(&self) { + if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() { + let this = self.clone(); + *guard = Some(tokio::spawn(async move { this.refill_deposits().await })); + } + } + + fn deposits_lower_threshold(&self) -> usize { + self.inner.target_amount - (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize + } + + fn deposits_upper_threshold(&self) -> usize { + self.inner.target_amount + (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize + } + + async fn mark_deposit_as_used( + &self, + deposit_id: DepositId, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + request_uuid: Uuid, + ) -> Result<(), CredentialProxyError> { + self.inner + .storage + .insert_deposit_usage(deposit_id, requested_on, client_pubkey, request_uuid) + .await + } + + async fn wait_for_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + loop { + tokio::time::sleep(Duration::from_millis(500)).await; + if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() { + // if the db call fails, we technically don't lose the deposit (we'll 'recover' it on restart) + self.mark_deposit_as_used( + buffered_deposit.deposit_id, + requested_on, + client_pubkey, + request_uuid, + ) + .await?; + return Ok(buffered_deposit); + } else { + // make sure there's always a task working in the background in case deposits get used up too quickly + self.maybe_refill_deposits() + } + } + } + + pub async fn get_valid_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + let mut deposits_guard = self.inner.unused_deposits.lock().await; + let deposits_available = deposits_guard.len(); + + debug!("we have {deposits_available} unused deposits available"); + + let maybe_deposit = deposits_guard.pop(); + drop(deposits_guard); + + if deposits_available < self.deposits_lower_threshold() { + // if we're below threshold, start refill task + self.maybe_refill_deposits() + } + + match maybe_deposit { + None => { + warn!("we currently don't have any usable deposits! are we using them up faster than we request them?"); + + // we have to wait until refill task has completed (either initiated by this or another fn call) + self.wait_for_deposit(request_uuid, requested_on, client_pubkey) + .await + } + Some(buffered_deposit) => { + self.mark_deposit_as_used( + buffered_deposit.deposit_id, + requested_on, + client_pubkey, + request_uuid, + ) + .await?; + Ok(buffered_deposit) + } + } + } + + pub async fn wait_for_shutdown(&self) { + let task_handle = self.inner.deposits_refill_task.take_task_join_handle(); + if let Some(task_handle) = task_handle { + if !task_handle.is_finished() { + info!("the deposit refill task is currently in progress - waiting for the current transaction to finish before concluding shutdown"); + let _ = task_handle.await; + } + } + } +} + +impl DepositsBufferInner { + // +} diff --git a/common/credential-proxy/src/deposits_buffer/refill_task.rs b/common/credential-proxy/src/deposits_buffer/refill_task.rs new file mode 100644 index 0000000000..4f09df55b0 --- /dev/null +++ b/common/credential-proxy/src/deposits_buffer/refill_task.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex as StdMutex, MutexGuard}; +use tokio::task::JoinHandle; +use tracing::{debug, error}; + +pub(super) type RefillTaskResult = Result<(), CredentialProxyError>; + +#[derive(Default)] +pub(super) struct RefillTask { + // note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers) + // if we find that we're using up deposits more quickly than we're refilling them, + // we'll have to increase the number of deposits per transaction + join_handle: StdMutex>>, + + in_progress: AtomicBool, +} + +impl RefillTask { + /// Attempt to set the `in_progress` value to `true` if it's not already `true`. + /// Returns boolean indicating whether it was successful + fn try_set_in_progress(&self) -> bool { + self.in_progress + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + } + + pub(super) fn try_get_new_task_guard( + &self, + ) -> Option>>> { + // sanity check for concurrent request + if !self.try_set_in_progress() { + debug!("another task has already started deposit refill request"); + return None; + } + + #[allow(clippy::expect_used)] + let guard = self.join_handle.lock().expect("mutex got poisoned"); + + if let Some(existing_handle) = guard.as_ref() { + if !existing_handle.is_finished() { + error!("CRITICAL BUG: there was already a deposit refill task spawned that hasn't yet finished") + } + } + + Some(guard) + } + + pub(super) fn take_task_join_handle(&self) -> Option> { + #[allow(clippy::expect_used)] + self.join_handle.lock().expect("mutex got poisoned").take() + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/common/credential-proxy/src/error.rs similarity index 75% rename from nym-credential-proxy/nym-credential-proxy/src/error.rs rename to common/credential-proxy/src/error.rs index 56e18a0673..6dff079b09 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/common/credential-proxy/src/error.rs @@ -1,6 +1,7 @@ -// Copyright 2024 Nym Technologies SA +// Copyright 2025 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::error::NyxdError; @@ -10,7 +11,7 @@ use thiserror::Error; use time::OffsetDateTime; #[derive(Debug, Error)] -pub enum VpnApiError { +pub enum CredentialProxyError { #[error("encountered an internal io error: {source}")] IoError { #[from] @@ -118,11 +119,44 @@ pub enum VpnApiError { #[error("failed to create deposit")] DepositFailure, + + #[error("can't obtain sufficient number of credential shares due to unavailable quorum")] + UnavailableSigningQuorum, + + #[error("failed to perform quorum check: {source}")] + QuorumCheckFailure { + #[from] + source: SignerCheckError, + }, + + #[error( + "this operation couldn't be completed as the program is in the process of shutting down" + )] + ShutdownInProgress, + + #[error("failed to obtain wallet shares with id {id}: {message}")] + ShareByIdLoadError { message: String, id: i64 }, + + #[error("failed to obtain wallet shares with device_id {device_id} and credential_id: {credential_id}: {message}")] + ShareByDeviceLoadError { + message: String, + device_id: String, + credential_id: String, + }, + + #[error("could not find shares with id {id}")] + SharesByIdNotFound { id: i64 }, + + #[error("could not find shares with device_id {device_id} and credential_id: {credential_id}")] + SharesByDeviceNotFound { + device_id: String, + credential_id: String, + }, } -impl VpnApiError { - pub fn database_inconsistency>(reason: S) -> VpnApiError { - VpnApiError::DatabaseInconsistency { +impl CredentialProxyError { + pub fn database_inconsistency>(reason: S) -> CredentialProxyError { + CredentialProxyError::DatabaseInconsistency { reason: reason.into(), } } diff --git a/common/credential-proxy/src/helpers.rs b/common/credential-proxy/src/helpers.rs new file mode 100644 index 0000000000..8f45ea99e4 --- /dev/null +++ b/common/credential-proxy/src/helpers.rs @@ -0,0 +1,67 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use rand::rngs::OsRng; +use rand::RngCore; +use time::OffsetDateTime; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +pub fn random_uuid() -> Uuid { + let mut bytes = [0u8; 16]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + Uuid::from_bytes(bytes) +} + +pub struct LockTimer { + created: OffsetDateTime, + message: String, +} + +impl LockTimer { + pub fn new>(message: S) -> Self { + LockTimer { + message: message.into(), + ..Default::default() + } + } +} + +impl Drop for LockTimer { + fn drop(&mut self) { + let time_taken = OffsetDateTime::now_utc() - self.created; + let time_taken_formatted = humantime::format_duration(time_taken.unsigned_abs()); + if time_taken > time::Duration::SECOND * 10 { + warn!(time_taken = %time_taken_formatted, "{}", self.message) + } else if time_taken > time::Duration::SECOND * 5 { + info!(time_taken = %time_taken_formatted, "{}", self.message) + } else { + debug!(time_taken = %time_taken_formatted, "{}", self.message) + }; + } +} + +impl Default for LockTimer { + fn default() -> Self { + LockTimer { + created: OffsetDateTime::now_utc(), + message: "released the lock".to_string(), + } + } +} + +// #[allow(clippy::panic)] +// fn build_sha_short() -> &'static str { +// let bin_info = bin_info!(); +// if bin_info.commit_sha.len() < 7 { +// panic!("unavailable build commit sha") +// } +// +// if bin_info.commit_sha == "VERGEN_IDEMPOTENT_OUTPUT" { +// error!("the binary hasn't been built correctly. it doesn't have a commit sha information"); +// return "unknown"; +// } +// +// &bin_info.commit_sha[..7] +// } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs b/common/credential-proxy/src/http_helpers.rs similarity index 68% rename from nym-credential-proxy/nym-credential-proxy/src/http/types.rs rename to common/credential-proxy/src/http_helpers.rs index 6538c5a9a7..129fcfa86b 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs +++ b/common/credential-proxy/src/http_helpers.rs @@ -1,11 +1,12 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use nym_credential_proxy_requests::api::v1::ErrorResponse; +use tracing::warn; use uuid::Uuid; #[derive(Debug, Clone)] @@ -35,7 +36,11 @@ impl RequestError { } } - pub fn new_server_error(err: VpnApiError, uuid: Uuid) -> Self { + pub fn new_plain_error(err: CredentialProxyError) -> Self { + Self::from_err(err, StatusCode::INTERNAL_SERVER_ERROR) + } + + pub fn new_server_error(err: CredentialProxyError, uuid: Uuid) -> Self { RequestError::new_with_uuid(err.to_string(), uuid, StatusCode::INTERNAL_SERVER_ERROR) } @@ -59,3 +64,12 @@ impl IntoResponse for RequestError { (self.status, Json(self.inner)).into_response() } } + +pub fn db_failure(err: CredentialProxyError, uuid: Uuid) -> Result { + warn!("db failure: {err}"); + Err(RequestError::new_with_uuid( + format!("oh no, something went wrong {err}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )) +} diff --git a/common/credential-proxy/src/lib.rs b/common/credential-proxy/src/lib.rs new file mode 100644 index 0000000000..fedd68dfb4 --- /dev/null +++ b/common/credential-proxy/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod deposits_buffer; +pub mod error; +pub mod helpers; +pub mod http_helpers; +pub mod nym_api_helpers; +pub mod quorum_checker; +pub mod shared_state; +pub mod storage; +pub mod ticketbook_manager; +pub mod webhook; diff --git a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs b/common/credential-proxy/src/nym_api_helpers.rs similarity index 85% rename from nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs rename to common/credential-proxy/src/nym_api_helpers.rs index 3268ddb50c..c762b33f98 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs +++ b/common/credential-proxy/src/nym_api_helpers.rs @@ -4,7 +4,7 @@ // TODO: this was just copied from nym-api; // it should have been therefore extracted to a common crate instead and imported as dependency -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use futures::{stream, StreamExt}; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_validator_client::nym_api::EpochId; @@ -19,9 +19,9 @@ use time::{Date, OffsetDateTime}; use tokio::sync::{Mutex, RwLock, RwLockReadGuard}; use tracing::warn; -pub(crate) struct CachedEpoch { +pub struct CachedEpoch { valid_until: OffsetDateTime, - pub(crate) current_epoch: Epoch, + pub current_epoch: Epoch, } impl Default for CachedEpoch { @@ -34,11 +34,11 @@ impl Default for CachedEpoch { } impl CachedEpoch { - pub(crate) fn is_valid(&self) -> bool { + pub fn is_valid(&self) -> bool { self.valid_until > OffsetDateTime::now_utc() } - pub(crate) fn update(&mut self, epoch: Epoch) { + pub fn update(&mut self, epoch: Epoch) { let now = OffsetDateTime::now_utc(); let validity_duration = if let Some(epoch_finish) = epoch.deadline { @@ -58,13 +58,13 @@ impl CachedEpoch { } // a map of items that never change for given key -pub(crate) struct CachedImmutableItems { +pub struct CachedImmutableItems { // I wonder if there's a more efficient structure with OnceLock or OnceCell or something inner: RwLock>, } // an item that stays constant throughout given epoch -pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; +pub type CachedImmutableEpochItem = CachedImmutableItems; impl Default for CachedImmutableItems { fn default() -> Self { @@ -86,7 +86,7 @@ impl CachedImmutableItems where K: Eq + Hash, { - pub(crate) async fn get_or_init(&self, key: K, f: F) -> Result, E> + pub async fn get_or_init(&self, key: K, f: F) -> Result, E> where F: FnOnce() -> U, U: Future>, @@ -125,29 +125,29 @@ where } } -pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), VpnApiError> { +pub fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), CredentialProxyError> { let today = ecash_today(); if expiration_date < today.date() { // what's the point of signatures with expiration in the past? - return Err(VpnApiError::ExpirationDateTooEarly); + return Err(CredentialProxyError::ExpirationDateTooEarly); } if expiration_date > cred_exp_date().ecash_date() { - return Err(VpnApiError::ExpirationDateTooLate); + return Err(CredentialProxyError::ExpirationDateTooLate); } Ok(()) } -pub(crate) async fn query_all_threshold_apis( +pub async fn query_all_threshold_apis( all_apis: Vec, threshold: u64, f: F, -) -> Result, VpnApiError> +) -> Result, CredentialProxyError> where F: Fn(EcashApiClient) -> U, - U: Future>, + U: Future>, { let shares = Mutex::new(Vec::with_capacity(all_apis.len())); @@ -168,7 +168,7 @@ where let shares = shares.into_inner(); if shares.len() < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfSigners { + return Err(CredentialProxyError::InsufficientNumberOfSigners { threshold, available: shares.len(), }); diff --git a/common/credential-proxy/src/quorum_checker.rs b/common/credential-proxy/src/quorum_checker.rs new file mode 100644 index 0000000000..6940a90749 --- /dev/null +++ b/common/credential-proxy/src/quorum_checker.rs @@ -0,0 +1,102 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::shared_state::nyxd_client::ChainClient; +use nym_ecash_signer_check::{check_known_dealers, dkg_details_with_client}; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +#[derive(Clone)] +pub struct QuorumState { + available: Arc, +} + +impl QuorumState { + pub fn available(&self) -> bool { + self.available.load(Ordering::Acquire) + } +} + +pub struct QuorumStateChecker { + client: ChainClient, + cancellation_token: CancellationToken, + check_interval: Duration, + quorum_state: QuorumState, +} + +impl QuorumStateChecker { + pub async fn new( + client: ChainClient, + check_interval: Duration, + cancellation_token: CancellationToken, + ) -> Result { + let this = QuorumStateChecker { + client, + cancellation_token, + check_interval, + quorum_state: QuorumState { + available: Arc::new(Default::default()), + }, + }; + + // first check MUST succeed, otherwise we shouldn't start + let quorum_available = this.check_quorum_state().await?; + this.quorum_state + .available + .store(quorum_available, Ordering::Relaxed); + Ok(this) + } + + pub fn quorum_state_ref(&self) -> QuorumState { + self.quorum_state.clone() + } + + async fn check_quorum_state(&self) -> Result { + let client_guard = self.client.query_chain().await; + + // split the operation as we only need to hold the reference to chain client for the first part + // and the second half doesn't rely on it (and takes way longer) + let dkg_details = dkg_details_with_client(client_guard.deref()).await?; + drop(client_guard); + + let res = check_known_dealers(dkg_details).await?; + + let Some(signing_threshold) = res.threshold else { + warn!("signing threshold is currently unavailable and we have not yet implemented credential issuance during DKG transition"); + return Ok(false); + }; + + let mut working_issuer = 0; + + for result in res.results { + if result.chain_available() && result.signing_available() { + working_issuer += 1; + } + } + + Ok((working_issuer as u64) >= signing_threshold) + } + + pub async fn run_forever(self) { + info!("starting quorum state checker"); + loop { + tokio::select! { + biased; + _ = self.cancellation_token.cancelled() => { + break + } + _ = tokio::time::sleep(self.check_interval) => { + match self.check_quorum_state().await { + Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst), + Err(err) => error!("failed to check current quorum state: {err}"), + } + } + } + } + } +} diff --git a/common/credential-proxy/src/shared_state/ecash_state.rs b/common/credential-proxy/src/shared_state/ecash_state.rs new file mode 100644 index 0000000000..011643216f --- /dev/null +++ b/common/credential-proxy/src/shared_state/ecash_state.rs @@ -0,0 +1,49 @@ +// Copyright 2025 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::nym_api_helpers::{CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems}; +use crate::quorum_checker::QuorumState; +use crate::shared_state::required_deposit_cache::RequiredDepositCache; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures}; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::EcashApiClient; +use time::Date; +use tokio::sync::RwLock; + +pub struct EcashState { + pub required_deposit_cache: RequiredDepositCache, + + pub quorum_state: QuorumState, + + pub cached_epoch: RwLock, + + pub master_verification_key: CachedImmutableEpochItem, + + pub threshold_values: CachedImmutableEpochItem, + + pub epoch_clients: CachedImmutableEpochItem>, + + pub coin_index_signatures: CachedImmutableEpochItem, + + pub expiration_date_signatures: + CachedImmutableItems<(EpochId, Date), AggregatedExpirationDateSignatures>, +} + +impl EcashState { + pub fn new( + required_deposit_cache: RequiredDepositCache, + quorum_state: QuorumState, + ) -> EcashState { + EcashState { + required_deposit_cache, + quorum_state, + cached_epoch: Default::default(), + master_verification_key: Default::default(), + threshold_values: Default::default(), + epoch_clients: Default::default(), + coin_index_signatures: Default::default(), + expiration_date_signatures: Default::default(), + } + } +} diff --git a/common/credential-proxy/src/shared_state/mod.rs b/common/credential-proxy/src/shared_state/mod.rs new file mode 100644 index 0000000000..310b0c6cc5 --- /dev/null +++ b/common/credential-proxy/src/shared_state/mod.rs @@ -0,0 +1,495 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::{BufferedDeposit, DepositsBuffer}; +use crate::error::CredentialProxyError; +use crate::nym_api_helpers::{ensure_sane_expiration_date, query_all_threshold_apis}; +use crate::shared_state::ecash_state::EcashState; +use crate::shared_state::nyxd_client::ChainClient; +use crate::storage::CredentialProxyStorage; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::{Base58, PublicKeyUser, VerificationKeyAuth}; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, + GlobalDataParams, MasterVerificationKeyResponse, +}; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, +}; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; +use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; +use nym_validator_client::nyxd::Coin; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use std::sync::Arc; +use std::time::Duration; +use time::{Date, OffsetDateTime}; +use tokio::sync::RwLockReadGuard; +use tokio::time::Instant; +use tracing::{debug, error, info, warn}; +use uuid::Uuid; + +pub mod ecash_state; +pub mod nyxd_client; +pub mod required_deposit_cache; + +#[derive(Clone)] +pub struct CredentialProxyState { + inner: Arc, +} + +impl CredentialProxyState { + pub fn new( + storage: CredentialProxyStorage, + client: ChainClient, + deposits_buffer: DepositsBuffer, + ecash_state: EcashState, + ) -> Self { + CredentialProxyState { + inner: Arc::new(CredentialProxyStateInner { + storage, + client, + deposits_buffer, + ecash_state, + }), + } + } + + pub fn storage(&self) -> &CredentialProxyStorage { + &self.inner.storage + } + + pub async fn deposit_amount(&self) -> Result { + self.ecash_state() + .required_deposit_cache + .get_or_update(self.client()) + .await + } + + pub fn client(&self) -> &ChainClient { + &self.inner.client + } + + pub fn deposits_buffer(&self) -> &DepositsBuffer { + &self.inner.deposits_buffer + } + + pub fn ecash_state(&self) -> &EcashState { + &self.inner.ecash_state + } + + pub(crate) async fn query_chain(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> { + self.inner.client.query_chain().await + } + + pub async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> { + let epoch = self.current_epoch().await?; + + if epoch.state.is_final() { + Ok(()) + } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { + // SAFETY: the timestamp values in our DKG contract should be valid timestamps, + // otherwise it means the chain is seriously misbehaving + #[allow(clippy::unwrap_used)] + let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); + + Err(CredentialProxyError::CredentialsNotYetIssuable { + availability: finish_dt, + }) + } else if epoch.state.is_waiting_initialisation() { + Err(CredentialProxyError::UninitialisedDkg) + } else { + Err(CredentialProxyError::UnknownEcashFailure) + } + } + + pub async fn get_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + let start = Instant::now(); + let deposit = self + .deposits_buffer() + .get_valid_deposit(request_uuid, requested_on, client_pubkey) + .await; + + let time_taken = start.elapsed(); + let formatted = humantime::format_duration(time_taken); + if time_taken > Duration::from_secs(10) { + warn!("attempting to get buffered deposit took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + } else { + debug!("attempting to get buffered deposit took {formatted}") + }; + + deposit + } + + pub async fn insert_deposit_usage_error(&self, deposit_id: DepositId, error: String) { + if let Err(err) = self + .storage() + .insert_deposit_usage_error(deposit_id, error) + .await + { + error!("failed to insert information about deposit (id: {deposit_id}) usage failure: {err}") + } + } + + pub async fn current_epoch_id(&self) -> Result { + let read_guard = self.inner.ecash_state.cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch.epoch_id); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch.epoch_id) + } + + pub async fn current_epoch(&self) -> Result { + let read_guard = self.ecash_state().cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch); + } + + // update cache + drop(read_guard); + let mut write_guard = self.ecash_state().cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch) + } + + pub async fn global_data( + &self, + global_data: GlobalDataParams, + epoch_id: EpochId, + expiration_date: Date, + ) -> Result< + ( + Option, + Option, + Option, + ), + CredentialProxyError, + > { + let master_verification_key = if global_data.include_master_verification_key { + debug!("including master verification key in the response"); + Some( + self.master_verification_key(Some(epoch_id)) + .await + .map(|key| MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_expiration_date_signatures = + if global_data.include_expiration_date_signatures { + debug!("including expiration date signatures in the response"); + Some( + self.master_expiration_date_signatures(epoch_id, expiration_date) + .await + .map(|signatures| AggregatedExpirationDateSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_coin_index_signatures = if global_data.include_coin_index_signatures { + debug!("including coin index signatures in the response"); + Some( + self.master_coin_index_signatures(Some(epoch_id)) + .await + .map(|signatures| AggregatedCoinIndicesSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + Ok(( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + )) + } + + pub async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result, CredentialProxyError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .inner + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored.key); + } + + info!("attempting to establish master verification key for epoch {epoch_id}..."); + + // 2. perform actual aggregation + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(CredentialProxyError::InsufficientNumberOfSigners { + threshold, + available: all_apis.len(), + }); + } + + let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?; + + let epoch = EpochVerificationKey { + epoch_id, + key: master_key, + }; + + // 3. save the key in the storage for when we reboot + self.inner + .storage + .insert_master_verification_key(&epoch) + .await?; + + Ok(epoch.key) + }) + .await + } + + pub async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result, CredentialProxyError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .inner + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(master_sigs); + } + + info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + let sigs = AggregatedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }; + + // 3. save the signatures in the storage for when we reboot + self.inner + .storage + .insert_master_coin_index_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } + + pub async fn master_expiration_date_signatures( + &self, + epoch_id: EpochId, + expiration_date: Date, + ) -> Result, CredentialProxyError> { + self.inner.ecash_state + .expiration_date_signatures + .get_or_init((epoch_id, expiration_date), || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .storage() + .get_master_expiration_date_signatures(expiration_date, epoch_id) + .await? + { + return Ok(master_sigs); + } + + + info!( + "attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..." + ); + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.current_epoch_id().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) + .await? + .signatures; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let sigs = AggregatedExpirationDateSignatures { + epoch_id, + expiration_date, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.inner.storage + .insert_master_expiration_date_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } + + pub async fn ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result>, CredentialProxyError> { + self.inner + .ecash_state + .epoch_clients + .get_or_init(epoch_id, || async { + Ok(self + .query_chain() + .await + .get_all_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + }) + .await + } + + pub async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.inner + .ecash_state + .threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = self + .query_chain() + .await + .get_epoch_threshold(epoch_id) + .await? + { + Ok(threshold) + } else { + Err(CredentialProxyError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } +} + +struct CredentialProxyStateInner { + storage: CredentialProxyStorage, + + client: ChainClient, + + deposits_buffer: DepositsBuffer, + + ecash_state: EcashState, +} diff --git a/common/credential-proxy/src/shared_state/nyxd_client.rs b/common/credential-proxy/src/shared_state/nyxd_client.rs new file mode 100644 index 0000000000..fee9d4333e --- /dev/null +++ b/common/credential-proxy/src/shared_state/nyxd_client.rs @@ -0,0 +1,126 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +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, CosmWasmClient, NyxdClient}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tracing::{instrument, warn}; + +#[derive(Clone)] +pub struct ChainClient(Arc>); + +impl ChainClient { + pub fn new(mnemonic: bip39::Mnemonic) -> Result { + let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + + let nyxd_url = network_details + .endpoints + .first() + .ok_or_else(|| CredentialProxyError::NoNyxEndpointsAvailable)? + .nyxd_url + .as_str(); + + let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; + + if client.ecash_contract_address().is_none() { + return Err(CredentialProxyError::UnavailableEcashContract); + } + + if client.dkg_contract_address().is_none() { + return Err(CredentialProxyError::UnavailableDKGContract); + } + + Ok(ChainClient(Arc::new(RwLock::new(client)))) + } + + pub async fn query_chain(&self) -> ChainReadPermit<'_> { + let _acquire_timer = LockTimer::new("acquire chain query permit"); + self.0.read().await + } + + pub async fn start_chain_tx(&self) -> ChainWritePermit<'_> { + let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + + ChainWritePermit { + lock_timer: LockTimer::new("exclusive chain access permit"), + inner: self.0.write().await, + } + } +} + +pub type ChainReadPermit<'a> = RwLockReadGuard<'a, DirectSigningHttpRpcNyxdClient>; + +// explicitly wrap the WriteGuard for extra information regarding time taken +pub struct ChainWritePermit<'a> { + // it's not really dead, we only care about it being dropped + #[allow(dead_code)] + lock_timer: LockTimer, + inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, +} + +impl ChainWritePermit<'_> { + #[instrument(skip(self, short_sha, info), err(Display))] + pub async fn make_deposits( + self, + short_sha: &'static str, + info: Vec<(String, Coin)>, + ) -> Result { + let address = self.inner.address(); + let starting_sequence = self.inner.get_sequence(&address).await?.sequence; + + let deposits = info.len(); + + let ecash_contract = self + .inner + .ecash_contract_address() + .ok_or(CredentialProxyError::UnavailableEcashContract)?; + let deposit_messages = info + .into_iter() + .map(|(identity_key, amount)| { + ( + ExecuteMsg::DepositTicketBookFunds { identity_key }, + vec![amount], + ) + }) + .collect::>(); + + let res = self + .inner + .execute_multiple( + ecash_contract, + deposit_messages, + None, + format!("cp-{short_sha}: performing {deposits} deposits"), + ) + .await?; + + loop { + let updated_sequence = self.inner.get_sequence(&address).await?.sequence; + + if updated_sequence > starting_sequence { + break; + } + warn!("wrong sequence number... waiting before releasing chain lock"); + tokio::time::sleep(Duration::from_millis(50)).await; + } + + Ok(res) + } +} + +impl Deref for ChainWritePermit<'_> { + type Target = DirectSigningHttpRpcNyxdClient; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} diff --git a/common/credential-proxy/src/shared_state/required_deposit_cache.rs b/common/credential-proxy/src/shared_state/required_deposit_cache.rs new file mode 100644 index 0000000000..9b45f5da4b --- /dev/null +++ b/common/credential-proxy/src/shared_state/required_deposit_cache.rs @@ -0,0 +1,71 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::shared_state::nyxd_client::ChainClient; +use nym_validator_client::nyxd::contract_traits::EcashQueryClient; +use nym_validator_client::nyxd::Coin; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +pub struct CachedDeposit { + valid_until: OffsetDateTime, + required_amount: Coin, +} + +impl CachedDeposit { + const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; + + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + fn update(&mut self, required_amount: Coin) { + self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; + self.required_amount = required_amount; + } +} + +impl Default for CachedDeposit { + fn default() -> Self { + CachedDeposit { + valid_until: OffsetDateTime::UNIX_EPOCH, + required_amount: Coin { + amount: u128::MAX, + denom: "unym".to_string(), + }, + } + } +} + +#[derive(Clone, Default)] +pub struct RequiredDepositCache { + inner: Arc>, +} + +impl RequiredDepositCache { + pub async fn get_or_update( + &self, + chain_client: &ChainClient, + ) -> Result { + let read_guard = self.inner.read().await; + if read_guard.is_valid() { + return Ok(read_guard.required_amount.clone()); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.write().await; + let deposit_amount = chain_client + .query_chain() + .await + .get_required_deposit_amount() + .await?; + + let nym_coin: Coin = deposit_amount.into(); + + write_guard.update(nym_coin.clone()); + Ok(nym_coin) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/common/credential-proxy/src/storage/manager.rs similarity index 79% rename from nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs rename to common/credential-proxy/src/storage/manager.rs index 5942d4e9cc..f72341cd08 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/common/credential-proxy/src/storage/manager.rs @@ -1,13 +1,13 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; use crate::storage::models::{ BlindedShares, BlindedSharesStatus, MinimalWalletShare, RawCoinIndexSignatures, - RawExpirationDateSignatures, RawVerificationKey, + RawExpirationDateSignatures, RawVerificationKey, StorableEcashDeposit, }; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use time::{Date, OffsetDateTime}; +use tracing::error; #[derive(Clone)] pub(crate) struct SqliteStorageManager { @@ -42,7 +42,7 @@ impl SqliteStorageManager { r#" SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as "expiration_date!: Date" FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 + JOIN ecash_deposit_usage as t2 on t1.corresponding_deposit = t2.deposit_id JOIN blinded_shares as t3 ON t2.request_uuid = t3.request_uuid @@ -106,7 +106,7 @@ impl SqliteStorageManager { t1.epoch_id as "epoch_id!", t1.expiration_date as "expiration_date!: Date" FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 + JOIN ecash_deposit_usage as t2 on t1.corresponding_deposit = t2.deposit_id JOIN blinded_shares as t3 ON t2.request_uuid = t3.request_uuid @@ -169,7 +169,7 @@ impl SqliteStorageManager { available_shares: i64, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { let now = OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -196,7 +196,7 @@ impl SqliteStorageManager { device_id: &str, credential_id: &str, error: &str, - ) -> Result { + ) -> Result { let now = time::OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -221,7 +221,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_blinded_shares( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM blinded_shares WHERE created < ? @@ -236,7 +236,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallets( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet WHERE created < ? @@ -251,7 +251,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallet_failures( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet_failure WHERE created < ? @@ -332,18 +332,20 @@ impl SqliteStorageManager { pub(crate) async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" - SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + SELECT serialised_signatures, serialization_revision as "serialization_revision: u8" FROM global_expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) - .fetch_optional(&self.connection_pool) - .await + .fetch_optional(&self.connection_pool) + .await } pub(crate) async fn insert_master_expiration_date_signatures( @@ -368,32 +370,87 @@ impl SqliteStorageManager { Ok(()) } - #[allow(clippy::too_many_arguments)] - pub(crate) async fn insert_deposit_data( + pub(crate) async fn insert_new_deposits( + &self, + deposits: Vec, + ) -> Result<(), sqlx::Error> { + if deposits.is_empty() { + // this should NEVER happen + error!("attempted to insert empty list of deposits"); + return Ok(()); + } + + let mut query_builder = + sqlx::QueryBuilder::new("INSERT INTO ecash_deposit (deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key) "); + + query_builder.push_values(&deposits, |mut b, deposit| { + b.push_bind(deposit.deposit_id) + .push_bind(deposit.deposit_tx_hash.clone()) + .push_bind(deposit.requested_on) + .push_bind(deposit.deposit_amount.clone()) + .push_bind(deposit.ed25519_deposit_private_key.as_ref()); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn load_unused_deposits( + &self, + ) -> Result, sqlx::Error> { + // select all entries from ecash_deposit where there is NO associated marked usage + sqlx::query_as( + r#" + SELECT ecash_deposit.* + FROM ecash_deposit ecash_deposit + LEFT JOIN ecash_deposit_usage deposit_usage + ON ecash_deposit.deposit_id = deposit_usage.deposit_id + WHERE deposit_usage.deposit_id IS NULL; + "#, + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_deposit_usage( &self, deposit_id: DepositId, - deposit_tx_hash: String, requested_on: OffsetDateTime, + client_pubkey: Vec, request_uuid: String, - deposit_amount: String, - client_pubkey: &[u8], - deposit_ed25519_private_key: &[u8], ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO ticketbook_deposit(deposit_id, deposit_tx_hash, requested_on, request_uuid, deposit_amount, client_pubkey, ed25519_deposit_private_key) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO ecash_deposit_usage (deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid) + VALUES (?, ?, ?, ?) "#, - deposit_id, - deposit_tx_hash, - requested_on, - request_uuid, - deposit_amount, - client_pubkey, - deposit_ed25519_private_key, + deposit_id, + requested_on, + client_pubkey, + request_uuid ) - .execute(&self.connection_pool) - .await?; + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_deposit_usage_error( + &self, + deposit_id: DepositId, + error: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE ecash_deposit_usage + SET ticketbook_request_error = ? + WHERE deposit_id = ? + "#, + error, + deposit_id + ) + .execute(&self.connection_pool) + .await?; Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/common/credential-proxy/src/storage/mod.rs similarity index 67% rename from nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs rename to common/credential-proxy/src/storage/mod.rs index 5480f96d5b..bc20fc85cc 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/common/credential-proxy/src/storage/mod.rs @@ -1,21 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::credentials::ticketbook::NodeId; -use crate::error::VpnApiError; +use crate::deposits_buffer::helpers::{BufferedDeposit, PerformedDeposits}; +use crate::error::CredentialProxyError; use crate::storage::manager::SqliteStorageManager; use crate::storage::models::{BlindedShares, MinimalWalletShare}; use nym_compact_ecash::PublicKeyUser; -use nym_credentials::ecash::bandwidth::issuance::Hash; use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; -use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindedSignatureResponse; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; -use nym_validator_client::nyxd::Coin; use sqlx::sqlite::{SqliteAutoVacuum, SqliteSynchronous}; use sqlx::ConnectOptions; use std::fmt::Debug; @@ -25,19 +22,24 @@ use time::{Date, OffsetDateTime}; use tracing::log::LevelFilter; use tracing::{debug, error, info, instrument}; use uuid::Uuid; -use zeroize::Zeroizing; mod manager; pub mod models; +pub(crate) mod pruner; + +// TODO: proper import +type NodeId = u64; #[derive(Clone)] -pub struct VpnApiStorage { +pub struct CredentialProxyStorage { pub(crate) storage_manager: SqliteStorageManager, } -impl VpnApiStorage { +impl CredentialProxyStorage { #[instrument] - pub async fn init + Debug>(database_path: P) -> Result { + pub async fn init + Debug>( + database_path: P, + ) -> Result { debug!("Attempting to connect to database"); let opts = sqlx::sqlite::SqliteConnectOptions::new() @@ -69,36 +71,36 @@ impl VpnApiStorage { info!("Database migration finished!"); - Ok(VpnApiStorage { + Ok(CredentialProxyStorage { storage_manager: SqliteStorageManager { connection_pool }, }) } #[allow(dead_code)] - pub(crate) async fn load_blinded_shares_status_by_shares_id( + pub async fn load_blinded_shares_status_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_blinded_shares_status_by_shares_id(id) .await?) } - pub(crate) async fn load_wallet_shares_by_shares_id( + pub async fn load_wallet_shares_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_wallet_shares_by_shares_id(id) .await?) } - pub(crate) async fn load_shares_error_by_shares_id( + pub async fn load_shares_error_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_shares_error_by_device_by_shares_id(id) @@ -106,84 +108,86 @@ impl VpnApiStorage { } #[allow(dead_code)] - pub(crate) async fn load_blinded_shares_status_by_device_and_credential_id( + pub async fn load_blinded_shares_status_by_device_and_credential_id( &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_blinded_shares_status_by_device_and_credential_id(device_id, credential_id) .await?) } - pub(crate) async fn load_wallet_shares_by_device_and_credential_id( + pub async fn load_wallet_shares_by_device_and_credential_id( &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_wallet_shares_by_device_and_credential_id(device_id, credential_id) .await?) } - pub(crate) async fn load_shares_error_by_device_and_credential_id( + pub async fn load_shares_error_by_device_and_credential_id( &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_shares_error_by_device_and_credential_id(device_id, credential_id) .await?) } - pub(crate) async fn insert_new_pending_async_shares_request( + pub async fn insert_new_pending_async_shares_request( &self, request: Uuid, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { Ok(self .storage_manager .insert_new_pending_async_shares_request(request.to_string(), device_id, credential_id) .await?) } - pub(crate) async fn update_pending_async_blinded_shares_issued( + pub async fn update_pending_async_blinded_shares_issued( &self, available_shares: usize, device_id: &str, credential_id: &str, - ) -> Result { - self.storage_manager + ) -> Result { + Ok(self + .storage_manager .update_pending_async_blinded_shares_issued( available_shares as i64, device_id, credential_id, ) - .await + .await?) } - pub(crate) async fn update_pending_async_blinded_shares_error( + pub async fn update_pending_async_blinded_shares_error( &self, available_shares: usize, device_id: &str, credential_id: &str, error: &str, - ) -> Result { - self.storage_manager + ) -> Result { + Ok(self + .storage_manager .update_pending_async_blinded_shares_error( available_shares as i64, device_id, credential_id, error, ) - .await + .await?) } - pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), VpnApiError> { + pub async fn prune_old_blinded_shares(&self) -> Result<(), CredentialProxyError> { let max_age = OffsetDateTime::now_utc() - time::Duration::days(31); self.storage_manager @@ -192,46 +196,70 @@ impl VpnApiStorage { self.storage_manager .prune_old_partial_blinded_wallet_failures(max_age) .await?; - self.storage_manager.prune_old_blinded_shares(max_age).await + self.storage_manager + .prune_old_blinded_shares(max_age) + .await?; + Ok(()) } - #[allow(clippy::too_many_arguments)] - pub(crate) async fn insert_deposit_data( + pub async fn insert_new_deposits( &self, - deposit_id: DepositId, - deposit_tx_hash: Hash, - requested_on: OffsetDateTime, - request: Uuid, - deposit_amount: Coin, - client_ecash_pubkey: &PublicKeyUser, - ed22519_keypair: &ed25519::KeyPair, - ) -> Result<(), VpnApiError> { - debug!("inserting deposit data"); - - let private_key_bytes = Zeroizing::new(ed22519_keypair.private_key().to_bytes()); + deposits: &PerformedDeposits, + ) -> Result<(), CredentialProxyError> { + debug!("inserting {} deposits data", deposits.deposits_data.len()); self.storage_manager - .insert_deposit_data( + .insert_new_deposits(deposits.to_storable()) + .await?; + Ok(()) + } + + pub async fn load_unused_deposits(&self) -> Result, CredentialProxyError> { + self.storage_manager + .load_unused_deposits() + .await? + .into_iter() + .map(|deposit| deposit.try_into()) + .collect() + } + + pub async fn insert_deposit_usage( + &self, + deposit_id: DepositId, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + request_uuid: Uuid, + ) -> Result<(), CredentialProxyError> { + self.storage_manager + .insert_deposit_usage( deposit_id, - deposit_tx_hash.to_string(), requested_on, - request.to_string(), - deposit_amount.to_string(), - &client_ecash_pubkey.to_bytes(), - private_key_bytes.as_ref(), + client_pubkey.to_bytes(), + request_uuid.to_string(), ) .await?; Ok(()) } - pub(crate) async fn insert_partial_wallet_share( + pub async fn insert_deposit_usage_error( + &self, + deposit_id: DepositId, + error: String, + ) -> Result<(), CredentialProxyError> { + self.storage_manager + .insert_deposit_usage_error(deposit_id, error) + .await?; + Ok(()) + } + + pub async fn insert_partial_wallet_share( &self, deposit_id: DepositId, epoch_id: EpochId, expiration_date: Date, node_id: NodeId, - res: &Result, - ) -> Result<(), VpnApiError> { + res: &Result, + ) -> Result<(), CredentialProxyError> { debug!("inserting partial wallet share"); let now = OffsetDateTime::now_utc(); @@ -264,10 +292,10 @@ impl VpnApiStorage { Ok(()) } - pub(crate) async fn get_master_verification_key( + pub async fn get_master_verification_key( &self, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_verification_key(epoch_id as i64) @@ -278,14 +306,14 @@ impl VpnApiStorage { let deserialised = EpochVerificationKey::try_unpack(&raw.serialised_key, raw.serialization_revision) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } - pub(crate) async fn insert_master_verification_key( + pub async fn insert_master_verification_key( &self, key: &EpochVerificationKey, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = key.pack(); Ok(self .storage_manager @@ -293,10 +321,10 @@ impl VpnApiStorage { .await?) } - pub(crate) async fn get_master_coin_index_signatures( + pub async fn get_master_coin_index_signatures( &self, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_coin_index_signatures(epoch_id as i64) @@ -309,14 +337,14 @@ impl VpnApiStorage { &raw.serialised_signatures, raw.serialization_revision, ) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } - pub(crate) async fn insert_master_coin_index_signatures( + pub async fn insert_master_coin_index_signatures( &self, signatures: &AggregatedCoinIndicesSignatures, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = signatures.pack(); self.storage_manager .insert_master_coin_index_signatures( @@ -328,13 +356,14 @@ impl VpnApiStorage { Ok(()) } - pub(crate) async fn get_master_expiration_date_signatures( + pub async fn get_master_expiration_date_signatures( &self, expiration_date: Date, - ) -> Result, VpnApiError> { + epoch_id: EpochId, + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); @@ -344,14 +373,14 @@ impl VpnApiStorage { &raw.serialised_signatures, raw.serialization_revision, ) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } - pub(crate) async fn insert_master_expiration_date_signatures( + pub async fn insert_master_expiration_date_signatures( &self, signatures: &AggregatedExpirationDateSignatures, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = signatures.pack(); self.storage_manager .insert_master_expiration_date_signatures( @@ -370,9 +399,11 @@ impl VpnApiStorage { #[cfg(test)] mod tests { use super::*; - use crate::http::helpers; + use crate::helpers::random_uuid; use crate::storage::models::BlindedSharesStatus; use nym_compact_ecash::scheme::keygen::KeyPairUser; + use nym_crypto::asymmetric::ed25519; + use nym_validator_client::nyxd::{Coin, Hash}; use rand::rngs::OsRng; use rand::RngCore; use std::ops::Deref; @@ -380,7 +411,7 @@ mod tests { // create the wrapper so the underlying file gets deleted when it's no longer needed struct StorageTestWrapper { - inner: VpnApiStorage, + inner: CredentialProxyStorage, _path: TempPath, } @@ -389,15 +420,15 @@ mod tests { let file = NamedTempFile::new()?; let path = file.into_temp_path(); - println!("Creating database at {:?}...", path); + println!("Creating database at {path:?}..."); Ok(StorageTestWrapper { - inner: VpnApiStorage::init(&path).await?, + inner: CredentialProxyStorage::init(&path).await?, _path: path, }) } - async fn insert_dummy_deposit(&self, uuid: Uuid) -> anyhow::Result { + async fn insert_dummy_used_deposit(&self, uuid: Uuid) -> anyhow::Result { let mut rng = OsRng; let deposit_id = rng.next_u32(); let tx_hash = Hash::Sha256(Default::default()); @@ -406,18 +437,21 @@ mod tests { let client_keypair = KeyPairUser::new(); let client_ecash_pubkey = &client_keypair.public_key(); - let deposit_keypair = ed25519::KeyPair::new(&mut rng); + let deposit_key = ed25519::PrivateKey::new(&mut rng); self.inner - .insert_deposit_data( - deposit_id, + .insert_new_deposits(&PerformedDeposits { + deposits_data: vec![BufferedDeposit { + deposit_id, + ed25519_private_key: deposit_key, + }], tx_hash, requested_on, - uuid, deposit_amount, - client_ecash_pubkey, - &deposit_keypair, - ) + }) + .await?; + self.inner + .insert_deposit_usage(deposit_id, requested_on, *client_ecash_pubkey, uuid) .await?; Ok(deposit_id) @@ -425,7 +459,7 @@ mod tests { } impl Deref for StorageTestWrapper { - type Target = VpnApiStorage; + type Target = CredentialProxyStorage; fn deref(&self) -> &Self::Target { &self.inner } @@ -447,19 +481,19 @@ mod tests { async fn test_add() -> anyhow::Result<()> { let storage = get_storage().await?; - let dummy_uuid = helpers::random_uuid(); + let dummy_uuid = random_uuid(); println!("🚀 insert_pending_blinded_share..."); - storage.insert_dummy_deposit(dummy_uuid).await?; + storage.insert_dummy_used_deposit(dummy_uuid).await?; let res = storage .insert_new_pending_async_shares_request(dummy_uuid, "1234", "1234") .await; if let Err(e) = &res { - println!("❌ {}", e); + println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); - println!("res = {:?}", res); + let res = res?; + println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Pending); println!("🚀 update_pending_blinded_share_error..."); @@ -467,11 +501,11 @@ mod tests { .update_pending_async_blinded_shares_error(0, "1234", "1234", "this is an error") .await; if let Err(e) = &res { - println!("❌ {}", e); + println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); - println!("res = {:?}", res); + let res = res?; + println!("res = {res:?}"); assert!(res.error_message.is_some()); assert_eq!(res.status, BlindedSharesStatus::Error); @@ -480,11 +514,11 @@ mod tests { .update_pending_async_blinded_shares_issued(42, "1234", "1234") .await; if let Err(e) = &res { - println!("❌ {}", e); + println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); - println!("res = {:?}", res); + let res = res?; + println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Issued); assert!(res.error_message.is_none()); diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs b/common/credential-proxy/src/storage/models.rs similarity index 57% rename from nym-credential-proxy/nym-credential-proxy/src/storage/models.rs rename to common/credential-proxy/src/storage/models.rs index 5bc443bf45..2c369d199d 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs +++ b/common/credential-proxy/src/storage/models.rs @@ -1,11 +1,49 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; -use std::convert::Into; +use sqlx::sqlite::SqliteRow; +use sqlx::{FromRow, Row, Type}; use strum_macros::{Display, EnumString}; use time::{Date, OffsetDateTime}; +use zeroize::Zeroizing; + +pub(crate) struct StorableEcashDeposit { + pub(crate) deposit_id: u32, + pub(crate) deposit_tx_hash: String, + pub(crate) requested_on: OffsetDateTime, + pub(crate) deposit_amount: String, + pub(crate) ed25519_deposit_private_key: Zeroizing<[u8; ed25519::SECRET_KEY_LENGTH]>, +} + +impl<'r> FromRow<'r, SqliteRow> for StorableEcashDeposit { + fn from_row(row: &'r SqliteRow) -> Result { + let deposit_id = row.try_get("deposit_id")?; + let deposit_tx_hash = row.try_get("deposit_tx_hash")?; + let requested_on = row.try_get("requested_on")?; + let deposit_amount = row.try_get("deposit_amount")?; + let ed25519_deposit_private_key: Vec = row.try_get("ed25519_deposit_private_key")?; + if ed25519_deposit_private_key.len() != ed25519::SECRET_KEY_LENGTH { + return Err(sqlx::Error::decode( + "stored ed25519_deposit_private_key has invalid length", + )); + } + + // SAFETY: we just checked the length is correct + #[allow(clippy::unwrap_used)] + let ed25519_deposit_private_key: [u8; ed25519::SECRET_KEY_LENGTH] = + ed25519_deposit_private_key.try_into().unwrap(); + + Ok(StorableEcashDeposit { + deposit_id, + deposit_tx_hash, + requested_on, + deposit_amount, + ed25519_deposit_private_key: Zeroizing::new(ed25519_deposit_private_key), + }) + } +} #[derive(Serialize, Deserialize, Debug, Clone, EnumString, Type, PartialEq, Display)] #[sqlx(rename_all = "snake_case")] @@ -29,15 +67,8 @@ pub struct BlindedShares { pub updated: OffsetDateTime, } -pub struct FullBlindedShares { - pub status: BlindedShares, - pub shares: (), -} - #[derive(FromRow)] pub struct RawExpirationDateSignatures { - #[allow(dead_code)] - pub epoch_id: u32, pub serialised_signatures: Vec, pub serialization_revision: u8, } diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/common/credential-proxy/src/storage/pruner.rs similarity index 74% rename from nym-credential-proxy/nym-credential-proxy/src/tasks.rs rename to common/credential-proxy/src/storage/pruner.rs index f886b7fd44..6ed8d3b6c0 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs +++ b/common/credential-proxy/src/storage/pruner.rs @@ -1,17 +1,17 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::storage::VpnApiStorage; +use crate::storage::CredentialProxyStorage; use tokio_util::sync::CancellationToken; use tracing::{error, info}; pub struct StoragePruner { cancellation_token: CancellationToken, - storage: VpnApiStorage, + storage: CredentialProxyStorage, } impl StoragePruner { - pub fn new(cancellation_token: CancellationToken, storage: VpnApiStorage) -> Self { + pub fn new(cancellation_token: CancellationToken, storage: CredentialProxyStorage) -> Self { Self { cancellation_token, storage, @@ -22,6 +22,7 @@ impl StoragePruner { info!("starting the storage pruner task"); loop { tokio::select! { + biased; _ = self.cancellation_token.cancelled() => { break } diff --git a/common/credential-proxy/src/ticketbook_manager/mod.rs b/common/credential-proxy/src/ticketbook_manager/mod.rs new file mode 100644 index 0000000000..7159cd279b --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/mod.rs @@ -0,0 +1,163 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::DepositsBuffer; +use crate::error::CredentialProxyError; +use crate::quorum_checker::QuorumStateChecker; +use crate::shared_state::ecash_state::EcashState; +use crate::shared_state::nyxd_client::ChainClient; +use crate::shared_state::required_deposit_cache::RequiredDepositCache; +use crate::shared_state::CredentialProxyState; +use crate::storage::pruner::StoragePruner; +use crate::storage::CredentialProxyStorage; +use crate::webhook::ZkNymWebhook; +use nym_credentials::ecash::utils::ecash_today; +use nym_validator_client::nym_api::EpochId; +use std::future::Future; +use std::time::Duration; +use time::Date; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tokio_util::task::TaskTracker; + +mod shares_handlers; +pub mod ticketbook_handlers; +pub mod wallet_shares; + +#[derive(Clone, Default)] +pub struct ShutdownTracker { + pub shutdown_token: CancellationToken, + pub tracker: TaskTracker, +} + +#[derive(Clone)] +pub struct TicketbookManager { + pub(crate) state: CredentialProxyState, + pub(crate) webhook: ZkNymWebhook, + pub(crate) shutdown_tracker: ShutdownTracker, +} + +impl TicketbookManager { + pub async fn new( + build_sha: &'static str, + quorum_check_interval: Duration, + deposits_buffer_size: usize, + max_concurrent_deposits: usize, + storage: CredentialProxyStorage, + mnemonic: bip39::Mnemonic, + webhook: ZkNymWebhook, + ) -> Result { + let chain_client = ChainClient::new(mnemonic)?; + let shutdown_tracker = ShutdownTracker::default(); + + let quorum_state_checker = QuorumStateChecker::new( + chain_client.clone(), + quorum_check_interval, + shutdown_tracker.shutdown_token.clone(), + ) + .await?; + + let required_deposit_cache = RequiredDepositCache::default(); + + let deposits_buffer = DepositsBuffer::new( + storage.clone(), + chain_client.clone(), + required_deposit_cache.clone(), + build_sha, + deposits_buffer_size, + max_concurrent_deposits, + shutdown_tracker.shutdown_token.clone(), + ) + .await?; + + let storage_pruner = + StoragePruner::new(shutdown_tracker.shutdown_token.clone(), storage.clone()); + + let this = TicketbookManager { + state: CredentialProxyState::new( + storage.clone(), + chain_client, + deposits_buffer, + EcashState::new( + required_deposit_cache, + quorum_state_checker.quorum_state_ref(), + ), + ), + webhook, + shutdown_tracker, + }; + + // since this is startup, + // might as well do all the needed network queries to establish needed global signatures + // if we don't already have them + this.build_initial_cache().await?; + + // spawn the background tasks + this.try_spawn_in_background(quorum_state_checker.run_forever()); + this.try_spawn_in_background(storage_pruner.run_forever()); + + Ok(this) + } + + async fn build_initial_cache(&self) -> Result<(), CredentialProxyError> { + let today = ecash_today().date(); + + let epoch_id = self.state.current_epoch_id().await?; + let _ = self.state.deposit_amount().await?; + let _ = self.state.master_verification_key(Some(epoch_id)).await?; + let _ = self.state.ecash_threshold(epoch_id).await?; + let _ = self.state.ecash_clients(epoch_id).await?; + let _ = self + .state + .master_coin_index_signatures(Some(epoch_id)) + .await?; + let _ = self + .state + .master_expiration_date_signatures(epoch_id, today) + .await?; + + Ok(()) + } + + pub async fn cancel_and_wait(&self) { + self.shutdown_tracker.shutdown_token.cancel(); + self.state.deposits_buffer().wait_for_shutdown().await; + self.shutdown_tracker.tracker.wait().await + } + + pub fn shutdown_token(&self) -> CancellationToken { + self.shutdown_tracker.shutdown_token.clone() + } + + /// Ensure the required global data for the specified epoch and expiration date exists in our cache (and storage) + async fn ensure_global_data_cached( + &self, + epoch: EpochId, + expiration_date: Date, + ) -> Result<(), CredentialProxyError> { + let _ = self.state.master_verification_key(Some(epoch)).await?; + let _ = self.state.master_coin_index_signatures(Some(epoch)).await?; + let _ = self + .state + .master_expiration_date_signatures(epoch, expiration_date) + .await?; + Ok(()) + } + + pub fn try_spawn_in_background(&self, task: F) -> Option> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + // don't spawn new task if we've received cancellation token + if self.shutdown_tracker.shutdown_token.is_cancelled() { + None + } else { + self.shutdown_tracker.tracker.reopen(); + // TODO: later use a task queue since most requests will be blocked waiting on chain permit anyway + let join_handle = self.shutdown_tracker.tracker.spawn(task); + self.shutdown_tracker.tracker.close(); + Some(join_handle) + } + } +} diff --git a/common/credential-proxy/src/ticketbook_manager/shares_handlers.rs b/common/credential-proxy/src/ticketbook_manager/shares_handlers.rs new file mode 100644 index 0000000000..4890c7608b --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/shares_handlers.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::storage::models::MinimalWalletShare; +use crate::ticketbook_manager::TicketbookManager; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + GlobalDataParams, TicketbookWalletSharesResponse, +}; +use nym_validator_client::nym_api::EpochId; +use tracing::{debug, span, Instrument, Level}; +use uuid::Uuid; + +impl TicketbookManager { + async fn shares_to_response( + &self, + shares: Vec, + params: GlobalDataParams, + ) -> Result { + // in all calls we ensured the shares are non-empty + #[allow(clippy::unwrap_used)] + let first = shares.first().unwrap(); + let expiration_date = first.expiration_date; + let epoch_id = first.epoch_id as EpochId; + + let threshold = self.state.ecash_threshold(epoch_id).await?; + if shares.len() < threshold as usize { + return Err(CredentialProxyError::InsufficientNumberOfCredentials { + available: shares.len(), + threshold, + }); + } + + // grab any requested additional data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = self + .state + .global_data(params, epoch_id, expiration_date) + .await?; + + // finally produce a response + Ok(TicketbookWalletSharesResponse { + epoch_id, + shares: shares.into_iter().map(Into::into).collect(), + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }) + } + + /// Query by id for blinded shares of a bandwidth voucher + pub async fn query_for_shares_by_id( + &self, + uuid: Uuid, + params: GlobalDataParams, + share_id: i64, + ) -> Result { + let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); + async move { + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = self + .state + .storage() + .load_wallet_shares_by_shares_id(share_id) + .await?; + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + if let Some(error_message) = self + .state + .storage() + .load_shares_error_by_shares_id(share_id) + .await? + { + return Err(CredentialProxyError::ShareByIdLoadError { + message: error_message, + id: share_id, + }); + } + + return Err(CredentialProxyError::SharesByIdNotFound { id: share_id }); + } + + self.shares_to_response(shares, params).await + } + .instrument(span) + .await + } + + /// Query by id for blinded wallet shares of a ticketbook + pub async fn query_for_shares_by_device_id_and_credential_id( + &self, + uuid: Uuid, + params: GlobalDataParams, + device_id: String, + credential_id: String, + ) -> Result { + let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); + async move { + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = self + .state + .storage() + .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) + .await?; + + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + if let Some(error_message) = self + .state + .storage() + .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) + .await? + { + return Err(CredentialProxyError::ShareByDeviceLoadError { + message: error_message, + device_id, + credential_id, + }); + } + + return Err(CredentialProxyError::SharesByDeviceNotFound { + device_id, + credential_id, + }); + } + + self.shares_to_response(shares, params).await + } + .instrument(span) + .await + } +} diff --git a/common/credential-proxy/src/ticketbook_manager/ticketbook_handlers.rs b/common/credential-proxy/src/ticketbook_manager/ticketbook_handlers.rs new file mode 100644 index 0000000000..b812878e22 --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/ticketbook_handlers.rs @@ -0,0 +1,164 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::nym_api_helpers::ensure_sane_expiration_date; +use crate::ticketbook_manager::TicketbookManager; +use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + CurrentEpochResponse, DepositResponse, GlobalDataParams, MasterVerificationKeyResponse, + PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest, + TicketbookObtainParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse, + TicketbookWalletSharesResponse, +}; +use time::OffsetDateTime; +use tracing::{error, info, span, warn, Instrument, Level}; +use uuid::Uuid; + +impl TicketbookManager { + pub async fn obtain_ticketbook_shares( + &self, + uuid: Uuid, + request: TicketbookRequest, + params: GlobalDataParams, + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); + let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); + + async move { + info!(""); + + self.state.ensure_credentials_issuable().await?; + let epoch_id = self.state.current_epoch_id().await?; + ensure_sane_expiration_date(request.expiration_date)?; + + // if additional data was requested, grab them first in case there are any cache/network issues + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = self + .state + .global_data(params, epoch_id, request.expiration_date) + .await?; + + let shares = self + .try_obtain_wallet_shares(uuid, requested_on, request) + .await + .inspect_err(|err| warn!("shares request failure: {err}"))?; + + info!("request was successful!"); + Ok(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }) + } + .instrument(span) + .await + } + + pub async fn obtain_ticketbook_shares_async( + &self, + uuid: Uuid, + request: TicketbookAsyncRequest, + params: TicketbookObtainParams, + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); + let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); + async move { + info!(""); + + // 1. perform basic validation + self.state.ensure_credentials_issuable().await?; + + ensure_sane_expiration_date(request.inner.expiration_date)?; + + // 2. store the request to retrieve the id + let pending = self + .state + .storage() + .insert_new_pending_async_shares_request( + uuid, + &request.device_id, + &request.credential_id, + ) + .await + .inspect_err(|err| error!("failed to insert new pending async shares: {err}"))?; + + let id = pending.id; + + // 3. try to spawn a new task attempting to resolve the request + let this = self.clone(); + if self + .try_spawn_in_background(async move { + this.try_obtain_blinded_ticketbook_async( + uuid, + requested_on, + request, + params, + pending, + ) + .await + }) + .is_none() + { + warn!("could not start async ticketbook issuance due to shutdown in progress"); + return Err(CredentialProxyError::ShutdownInProgress); + } + + // 4. in the meantime, return the id to the user + Ok(TicketbookWalletSharesAsyncResponse { id, uuid }) + } + .instrument(span) + .await + } + + pub async fn current_deposit(&self) -> Result { + let current_deposit = self.state.deposit_amount().await?; + Ok(DepositResponse { + current_deposit_amount: current_deposit.amount, + current_deposit_denom: current_deposit.denom, + }) + } + + pub async fn partial_verification_keys( + &self, + ) -> Result { + self.state.ensure_credentials_issuable().await?; + + let epoch_id = self.state.current_epoch_id().await?; + let signers = self.state.ecash_clients(epoch_id).await?; + Ok(PartialVerificationKeysResponse { + epoch_id, + keys: signers + .iter() + .map(|signer| PartialVerificationKey { + node_index: signer.node_id, + bs58_encoded_key: signer.verification_key.to_bs58(), + }) + .collect(), + }) + } + + pub async fn master_verification_key( + &self, + ) -> Result { + self.state.ensure_credentials_issuable().await?; + + let epoch_id = self.state.current_epoch_id().await?; + let key = self.state.master_verification_key(Some(epoch_id)).await?; + Ok(MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + }) + } + + pub async fn current_epoch(&self) -> Result { + self.state.ensure_credentials_issuable().await?; + let epoch_id = self.state.current_epoch_id().await?; + Ok(CurrentEpochResponse { epoch_id }) + } +} diff --git a/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs b/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs new file mode 100644 index 0000000000..72542090aa --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs @@ -0,0 +1,343 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::storage::models::BlindedShares; +use crate::ticketbook_manager::TicketbookManager; +use futures::{stream, StreamExt}; +use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + TicketbookAsyncRequest, TicketbookObtainParams, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, + WebhookTicketbookWalletSharesRequest, +}; +use nym_validator_client::ecash::BlindSignRequestBody; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex; +use tokio::time::timeout; +use tracing::{debug, error, info, instrument}; +use uuid::Uuid; + +impl TicketbookManager { + #[instrument( + skip(self, request_data, request, requested_on), + fields( + expiration_date = %request_data.expiration_date, + ticketbook_type = %request_data.ticketbook_type + ) + )] + pub async fn try_obtain_wallet_shares( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, + ) -> Result, CredentialProxyError> { + // don't proceed if we don't have quorum available as the request will definitely fail + if !self.state.ecash_state().quorum_state.available() { + return Err(CredentialProxyError::UnavailableSigningQuorum); + } + + let epoch = self.state.current_epoch_id().await?; + let threshold = self.state.ecash_threshold(epoch).await?; + let expiration_date = request_data.expiration_date; + + // before we commit to making the deposit, ensure we have required signatures cached and stored + self.ensure_global_data_cached(epoch, expiration_date) + .await?; + let ecash_api_clients = self.state.ecash_clients(epoch).await?.clone(); + + let deposit_data = self + .state + .get_deposit(request, requested_on, request_data.ecash_pubkey) + .await?; + let deposit_id = deposit_data.deposit_id; + let signature = deposit_data.sign_ticketbook_plaintext(&request_data.withdrawal_request); + + let credential_request = BlindSignRequestBody::new( + request_data.withdrawal_request.into(), + deposit_id, + signature, + request_data.ecash_pubkey, + request_data.expiration_date, + request_data.ticketbook_type, + ); + + let wallet_shares = Arc::new(Mutex::new(HashMap::new())); + + info!("attempting to contract all nym-apis for the partial wallets..."); + stream::iter(ecash_api_clients) + .for_each_concurrent(None, |client| async { + // move the client into the block + let client = client; + + debug!("contacting {client} for blinded partial wallet"); + let res = timeout( + Duration::from_secs(5), + client.api_client.blind_sign(&credential_request), + ) + .await + .map_err(|_| CredentialProxyError::EcashApiRequestTimeout { + client_repr: client.to_string(), + }) + .and_then(|res| res.map_err(Into::into)); + + // 1. try to store it + if let Err(err) = self + .state + .storage() + .insert_partial_wallet_share( + deposit_id, + epoch, + expiration_date, + client.node_id, + &res, + ) + .await + { + error!("failed to persist issued partial share: {err}") + } + + // 2. add it to the map + match res { + Ok(share) => { + wallet_shares + .lock() + .await + .insert(client.node_id, share.blinded_signature); + } + Err(err) => { + error!("failed to obtain partial blinded wallet share from {client}: {err}") + } + } + }) + .await; + + // SAFETY: the futures have completed, so we MUST have the only arc reference + #[allow(clippy::unwrap_used)] + let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner(); + let shares = wallet_shares.len(); + + if shares < threshold as usize { + let err = CredentialProxyError::InsufficientNumberOfCredentials { + available: shares, + threshold, + }; + self.state + .insert_deposit_usage_error(deposit_id, err.to_string()) + .await; + return Err(err); + } + + Ok(wallet_shares + .into_iter() + .map(|(node_index, share)| WalletShare { + node_index, + bs58_encoded_share: share.to_bs58(), + }) + .collect()) + } + + pub async fn try_obtain_wallet_shares_async( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, + device_id: &str, + credential_id: &str, + ) -> Result, CredentialProxyError> { + let shares = match self + .try_obtain_wallet_shares(request, requested_on, request_data) + .await + { + Ok(shares) => shares, + Err(err) => { + let obtained = match err { + CredentialProxyError::InsufficientNumberOfCredentials { available, .. } => { + available + } + _ => 0, + }; + + // currently there's no retry mechanisms, but, who knows, that might change + if let Err(err) = self + .state + .storage() + .update_pending_async_blinded_shares_error( + obtained, + device_id, + credential_id, + &err.to_string(), + ) + .await + { + error!("failed to update database with the error information: {err}") + } + return Err(err); + } + }; + + Ok(shares) + } + + async fn try_obtain_blinded_ticketbook_async_inner( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainParams, + pending: &BlindedShares, + ) -> Result<(), CredentialProxyError> { + let epoch_id = self.state.current_epoch_id().await?; + + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + // 1. try to obtain global data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = self + .state + .global_data(params.global, epoch_id, request_data.inner.expiration_date) + .await?; + + // 2. try to obtain shares (failures are written to the DB) + let shares = self + .try_obtain_wallet_shares_async( + request, + requested_on, + request_data.inner, + device_id, + credential_id, + ) + .await?; + + // 3. update the storage, if possible + // (as long as we can trigger webhook, we should still be good) + if let Err(err) = self + .state + .storage() + .update_pending_async_blinded_shares_issued(shares.len(), device_id, credential_id) + .await + { + error!(uuid = %request, "failed to update db with issued information: {err}") + } + + // 4. build the webhook request body + let data = Some(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: pending.status.to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data, + error_message: None, + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + // 5. call the webhook + self.webhook.try_trigger(request, &webhook_request).await; + + Ok(()) + } + + async fn try_trigger_webhook_request_for_error( + &self, + request: Uuid, + request_data: TicketbookAsyncRequest, + pending: &BlindedShares, + error_message: String, + ) -> Result<(), CredentialProxyError> { + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: "error".to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data: None, + error_message: Some(error_message), + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + self.webhook.try_trigger(request, &webhook_request).await; + + Ok(()) + } + + #[instrument( + skip_all, + fields( + credential_id = %request_data.credential_id, + device_id = %request_data.device_id) + ) + ] + #[allow(clippy::too_many_arguments)] + pub(crate) async fn try_obtain_blinded_ticketbook_async( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainParams, + pending: BlindedShares, + ) { + let skip_webhook = params.skip_webhook; + if let Err(err) = self + .try_obtain_blinded_ticketbook_async_inner( + request, + requested_on, + request_data.clone(), + params, + &pending, + ) + .await + { + if skip_webhook { + info!(uuid = %request,"the webhook is not going to be called for this request"); + return; + } + + // post to the webhook to notify of errors on this side + if let Err(webhook_err) = self + .try_trigger_webhook_request_for_error( + request, + request_data, + &pending, + format!("Failed to get ticketbook: {err}"), + ) + .await + { + error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}") + } + error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") + } else { + info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs b/common/credential-proxy/src/webhook.rs similarity index 58% rename from nym-credential-proxy/nym-credential-proxy/src/webhook.rs rename to common/credential-proxy/src/webhook.rs index d32f27c929..256a6e76fb 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs +++ b/common/credential-proxy/src/webhook.rs @@ -1,57 +1,34 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::error::VpnApiError; -use clap::Args; use reqwest::header::AUTHORIZATION; use serde::Serialize; use tracing::{debug, error, instrument, span, Instrument, Level}; use url::Url; use uuid::Uuid; -#[derive(Args, Debug, Clone)] -pub struct ZkNymWebHookConfig { - #[clap(long, env = "WEBHOOK_ZK_NYMS_URL")] - pub webhook_url: Url, +#[derive(Debug, Clone)] +pub struct ZkNymWebhook { + pub webhook_client_url: Url, - #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_ID")] - pub webhook_client_id: String, - - #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_SECRET")] pub webhook_client_secret: String, } -impl ZkNymWebHookConfig { - pub fn ensure_valid_client_url(&self) -> Result<(), VpnApiError> { - self.client_url() - .map_err(|_| VpnApiError::InvalidWebhookUrl) - .map(|_| ()) - } - - fn client_url(&self) -> Result { - self.webhook_url.join(&self.webhook_client_id) - } - - fn unchecked_client_url(&self) -> Url { - // we ensured we have valid url on startup - #[allow(clippy::unwrap_used)] - self.client_url().unwrap() - } - +impl ZkNymWebhook { fn bearer_token(&self) -> String { format!("Bearer {}", self.webhook_client_secret) } #[instrument(skip_all)] pub async fn try_trigger(&self, original_uuid: Uuid, payload: &T) { - let url = self.unchecked_client_url(); + let url = self.webhook_client_url.clone(); let span = span!(Level::DEBUG, "webhook", uuid = %original_uuid, url = %url); async move { debug!("🕸️ about to trigger the webhook"); match reqwest::Client::new() - .post(url.clone()) + .post(url) .header(AUTHORIZATION, self.bearer_token()) .json(payload) .send() diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index b822d49db6..9220c21b1c 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -3,6 +3,7 @@ name = "nym-credential-storage" version = "0.1.0" edition = "2021" license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,6 +21,8 @@ nym-credentials = { path = "../credentials" } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard] +path = "../../sqlx-pool-guard" [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true @@ -31,8 +34,14 @@ features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +anyhow = { workspace = true } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [features] -persistent-storage = ["bincode", "serde"] \ No newline at end of file +persistent-storage = ["bincode", "serde"] diff --git a/common/credential-storage/build.rs b/common/credential-storage/build.rs index 4716c65d0d..cc713b8c6f 100644 --- a/common/credential-storage/build.rs +++ b/common/credential-storage/build.rs @@ -3,22 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/coconut-credential-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -27,4 +34,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql b/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql new file mode 100644 index 0000000000..3c83cc0aaa --- /dev/null +++ b/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql @@ -0,0 +1,123 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- 1. add temporary `epoch_id` column +ALTER TABLE pending_issuance + ADD COLUMN epoch_id INTEGER; + +-- 2. populate the value +UPDATE pending_issuance +SET epoch_id = (SELECT epoch_id + FROM expiration_date_signatures + WHERE expiration_date_signatures.expiration_date = pending_issuance.expiration_date); + +-- 3. create new expiration_date_signatures table (with changed constraints) +CREATE TABLE expiration_date_signatures_new +( + expiration_date DATE NOT NULL, + + epoch_id INTEGER NOT NULL, + + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +); + +-- 4. migrate the data +INSERT INTO expiration_date_signatures_new (expiration_date, epoch_id, serialization_revision, serialised_signatures) +SELECT expiration_date, epoch_id, serialization_revision, serialised_signatures +FROM expiration_date_signatures; + +-- 5. drop and recreate the table references (due to new FK) + +-- 5.1. +-- (data for ticketbooks that have an associated deposit, but failed to get issued) +CREATE TABLE pending_issuance_new +( + deposit_id INTEGER NOT NULL PRIMARY KEY, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + pending_ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL, + epoch_id INTEGER NOT NULL, + + -- for each ticketbook we MUST have corresponding expiration date signatures + FOREIGN KEY (epoch_id, expiration_date) REFERENCES expiration_date_signatures_new (epoch_id, expiration_date) +); + +INSERT INTO pending_issuance_new (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, + epoch_id) +SELECT deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, epoch_id +FROM pending_issuance; + + +-- 5.2. +CREATE TABLE ecash_ticketbook_new +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + -- the type of the associated ticketbook + ticketbook_type TEXT NOT NULL, + + -- the actual crypto data of the ticketbook (wallet, keys, etc.) + ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL, + + -- for each ticketbook we MUST have corresponding coin index signatures + epoch_id INTEGER NOT NULL, + + -- the initial number of tickets the wallet has been created for + total_tickets INTEGER NOT NULL, + + -- how many tickets have been used so far (the `l` value of the wallet) + used_tickets INTEGER NOT NULL, + + + -- FOREIGN KEYS: + + -- for each ticketbook we MUST have corresponding coin index signatures + FOREIGN KEY (epoch_id) REFERENCES coin_indices_signatures (epoch_id), + + -- for each ticketbook we MUST have corresponding expiration date signatures + FOREIGN KEY (expiration_date, epoch_id) REFERENCES expiration_date_signatures_new (expiration_date, epoch_id) +); + +INSERT INTO ecash_ticketbook_new (id, serialization_revision, ticketbook_type, ticketbook_data, expiration_date, + epoch_id, total_tickets, used_tickets) +SELECT id, + serialization_revision, + ticketbook_type, + ticketbook_data, + expiration_date, + epoch_id, + total_tickets, + used_tickets +FROM ecash_ticketbook; + +-- 6. finally swap out the old tables +-- drop old tables +DROP TABLE pending_issuance; +DROP TABLE ecash_ticketbook; +DROP TABLE expiration_date_signatures; + +-- rename new tables +ALTER TABLE pending_issuance_new + RENAME TO pending_issuance; +ALTER TABLE ecash_ticketbook_new + RENAME TO ecash_ticketbook; +ALTER TABLE expiration_date_signatures_new + RENAME TO expiration_date_signatures; diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index f565376748..b142b70a20 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -28,7 +28,7 @@ struct EcashCredentialManagerInner { pending: HashMap, master_vk: HashMap, coin_indices_sigs: HashMap>, - expiration_date_sigs: HashMap>, + expiration_date_sigs: HashMap<(u64, Date), Vec>, _next_id: i64, } @@ -242,10 +242,14 @@ impl MemoryEcachTicketbookManager { pub(crate) async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Option> { let guard = self.inner.read().await; - guard.expiration_date_sigs.get(&expiration_date).cloned() + guard + .expiration_date_sigs + .get(&(epoch_id, expiration_date)) + .cloned() } pub(crate) async fn insert_expiration_date_signatures( @@ -254,8 +258,9 @@ impl MemoryEcachTicketbookManager { ) { let mut guard = self.inner.write().await; - guard - .expiration_date_sigs - .insert(sigs.expiration_date, sigs.signatures.clone()); + guard.expiration_date_sigs.insert( + (sigs.epoch_id, sigs.expiration_date), + sigs.signatures.clone(), + ); } } diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index f1fd0b802d..d196cad049 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -7,10 +7,11 @@ use crate::models::{ }; use nym_ecash_time::Date; use sqlx::{Executor, Sqlite, Transaction}; +use sqlx_pool_guard::SqlitePoolGuard; #[derive(Clone)] pub struct SqliteEcashTicketbookManager { - connection_pool: sqlx::SqlitePool, + connection_pool: SqlitePoolGuard, } impl SqliteEcashTicketbookManager { @@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager { /// # Arguments /// /// * `connection_pool`: database connection pool to use. - pub fn new(connection_pool: sqlx::SqlitePool) -> Self { + pub fn new(connection_pool: SqlitePoolGuard) -> Self { SqliteEcashTicketbookManager { connection_pool } } @@ -33,12 +34,12 @@ impl SqliteEcashTicketbookManager { "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", deadline ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } - pub(crate) async fn begin_storage_tx(&self) -> Result, sqlx::Error> { + pub(crate) async fn begin_storage_tx(&self) -> Result, sqlx::Error> { self.connection_pool.begin().await } @@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager { data, expiration_date, ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) @@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager { epoch_id, total_tickets, used_tickets, - ).execute(&self.connection_pool).await?; + ).execute(&*self.connection_pool).await?; Ok(()) } @@ -98,15 +99,14 @@ impl SqliteEcashTicketbookManager { pub(crate) async fn contains_ticketbook_data(&self, data: &[u8]) -> Result { let exists = sqlx::query( r#" - SELECT EXISTS ( - SELECT 1 - FROM ecash_ticketbook - WHERE ticketbook_data = ? - ) + SELECT 1 + FROM ecash_ticketbook + WHERE ticketbook_data = ? + "#, ) .bind(data) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await? .is_some(); @@ -122,7 +122,7 @@ impl SqliteEcashTicketbookManager { FROM ecash_ticketbook "#, ) - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -144,7 +144,7 @@ impl SqliteEcashTicketbookManager { ticketbook_id, expected_current_total_spent ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await? .rows_affected(); Ok(affected > 0) @@ -154,7 +154,7 @@ impl SqliteEcashTicketbookManager { &self, ) -> Result, sqlx::Error> { sqlx::query_as("SELECT * FROM pending_issuance") - .fetch_all(&self.connection_pool) + .fetch_all(&*self.connection_pool) .await } @@ -166,7 +166,7 @@ impl SqliteEcashTicketbookManager { "DELETE FROM pending_issuance WHERE deposit_id = ?", pending_id ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -183,7 +183,7 @@ impl SqliteEcashTicketbookManager { "#, epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -209,7 +209,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, epoch_id ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -226,7 +226,7 @@ impl SqliteEcashTicketbookManager { "#, epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -252,7 +252,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, epoch_id, ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } @@ -260,17 +260,19 @@ impl SqliteEcashTicketbookManager { pub(crate) async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" - SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + SELECT serialised_signatures, serialization_revision as "serialization_revision: u8" FROM expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) - .fetch_optional(&self.connection_pool) + .fetch_optional(&*self.connection_pool) .await } @@ -299,7 +301,7 @@ impl SqliteEcashTicketbookManager { serialisation_revision, expiration_date ) - .execute(&self.connection_pool) + .execute(&*self.connection_pool) .await?; Ok(()) } diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index fdcd89ccbd..2356f2da9d 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -166,10 +166,11 @@ impl Storage for EphemeralStorage { async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Result>, Self::StorageError> { Ok(self .storage_manager - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await) } diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 2edf9df3cb..219874d829 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -60,7 +60,6 @@ pub struct StoredPendingTicketbook { #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] pub struct RawExpirationDateSignatures { - pub epoch_id: u32, pub serialised_signatures: Vec, pub serialization_revision: u8, } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index 565ab94513..23a9f00cb9 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -37,6 +37,7 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; +use sqlx_pool_guard::SqlitePoolGuard; use std::path::Path; use zeroize::Zeroizing; @@ -54,8 +55,8 @@ impl PersistentStorage { /// * `database_path`: path to the database. pub async fn init>(database_path: P) -> Result { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); let opts = sqlx::sqlite::SqliteConnectOptions::new() @@ -74,13 +75,16 @@ impl PersistentStorage { } }; - if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + let connection_pool = SqlitePoolGuard::new(connection_pool); + + if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await { error!("Failed to perform migration on the SQLx database: {err}"); + connection_pool.close().await; return Err(err.into()); } Ok(PersistentStorage { - storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()), + storage_manager: SqliteEcashTicketbookManager::new(connection_pool), }) } } @@ -321,10 +325,11 @@ impl Storage for PersistentStorage { async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Result>, Self::StorageError> { let Some(raw) = self .storage_manager - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index b4288f1b88..cc28282889 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -92,6 +92,7 @@ pub trait Storage: Clone + Send + Sync { async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Result>, Self::StorageError>; async fn insert_expiration_date_signatures( diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index be7133b4c2..1aeab19ed8 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -95,7 +95,7 @@ where } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { // Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries let secs_until_final = final_timestamp.saturating_sub(current_timestamp_secs) + 1; - info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final); + info!("Approximately {secs_until_final} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment."); tokio::time::sleep(Duration::from_secs(secs_until_final)).await; } else if matches!(epoch.state, EpochState::WaitingInitialisation) { info!("dkg hasn't been initialised yet and it is not known when it will be. Going to check again later"); diff --git a/common/credential-verification/Cargo.toml b/common/credential-verification/Cargo.toml index a86b7da382..e85589eaae 100644 --- a/common/credential-verification/Cargo.toml +++ b/common/credential-verification/Cargo.toml @@ -11,9 +11,11 @@ rust-version.workspace = true readme.workspace = true [dependencies] +async-trait = { workspace = true } bs58 = { workspace = true } cosmwasm-std = { workspace = true } cw-utils = { workspace = true } +dyn-clone = { workspace = true } futures = { workspace = true } rand = { workspace = true } si-scale = { workspace = true } diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index 19df1dba6f..17f89a59fb 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -7,25 +7,36 @@ use crate::ClientBandwidth; use nym_credentials::ecash::utils::ecash_today; use nym_credentials_interface::Bandwidth; use nym_gateway_requests::ServerResponse; -use nym_gateway_storage::GatewayStorage; +use nym_gateway_storage::traits::BandwidthGatewayStorage; use si_scale::helpers::bibytes2; use time::OffsetDateTime; use tracing::*; const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB -#[derive(Clone)] pub struct BandwidthStorageManager { - pub(crate) storage: GatewayStorage, + pub(crate) storage: Box, pub(crate) client_bandwidth: ClientBandwidth, pub(crate) client_id: i64, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, pub(crate) only_coconut_credentials: bool, } +impl Clone for BandwidthStorageManager { + fn clone(&self) -> Self { + Self { + storage: dyn_clone::clone_box(&*self.storage), + client_bandwidth: self.client_bandwidth.clone(), + client_id: self.client_id, + bandwidth_cfg: self.bandwidth_cfg, + only_coconut_credentials: self.only_coconut_credentials, + } + } +} + impl BandwidthStorageManager { pub fn new( - storage: GatewayStorage, + storage: Box, client_bandwidth: ClientBandwidth, client_id: i64, bandwidth_cfg: BandwidthFlushingBehaviourConfig, @@ -40,6 +51,10 @@ impl BandwidthStorageManager { } } + pub fn client_bandwidth(&self) -> ClientBandwidth { + self.client_bandwidth.clone() + } + pub async fn available_bandwidth(&self) -> i64 { self.client_bandwidth.available().await } @@ -84,7 +99,8 @@ impl BandwidthStorageManager { debug!(available = available_bi2, required = required_bi2); self.consume_bandwidth(required_bandwidth).await?; - Ok(available_bandwidth) + let remaining_bandwidth = self.client_bandwidth.available().await; + Ok(remaining_bandwidth) } async fn expire_bandwidth(&mut self) -> Result<()> { diff --git a/common/credential-verification/src/client_bandwidth.rs b/common/credential-verification/src/client_bandwidth.rs index d98f89b511..0a0acba8c1 100644 --- a/common/credential-verification/src/client_bandwidth.rs +++ b/common/credential-verification/src/client_bandwidth.rs @@ -73,7 +73,7 @@ impl ClientBandwidth { false } - pub(crate) async fn available(&self) -> i64 { + pub async fn available(&self) -> i64 { self.inner.read().await.bandwidth.bytes } diff --git a/common/credential-verification/src/ecash/mod.rs b/common/credential-verification/src/ecash/mod.rs index 71d0d993ff..5586ef5ab1 100644 --- a/common/credential-verification/src/ecash/mod.rs +++ b/common/credential-verification/src/ecash/mod.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::Error; +use async_trait::async_trait; use credential_sender::CredentialHandler; use credential_sender::CredentialHandlerConfig; use error::EcashTicketError; use futures::channel::mpsc::{self, UnboundedSender}; use nym_credentials::CredentialSpendingData; use nym_credentials_interface::{ClientTicket, CompactEcashError, NymPayInfo, VerificationKeyAuth}; +use nym_gateway_storage::traits::BandwidthGatewayStorage; use nym_gateway_storage::GatewayStorage; use nym_validator_client::nym_api::EpochId; use nym_validator_client::DirectSigningHttpRpcNyxdClient; @@ -20,6 +22,7 @@ pub mod credential_sender; pub mod error; mod helpers; mod state; +pub mod traits; pub const TIME_RANGE_SEC: i64 = 30; @@ -31,44 +34,21 @@ pub struct EcashManager { cred_sender: UnboundedSender, } -impl EcashManager { - pub async fn new( - credential_handler_cfg: CredentialHandlerConfig, - nyxd_client: DirectSigningHttpRpcNyxdClient, - pk_bytes: [u8; 32], - shutdown: nym_task::TaskClient, - storage: GatewayStorage, - ) -> Result { - let shared_state = SharedState::new(nyxd_client, storage).await?; - - let (cred_sender, cred_receiver) = mpsc::unbounded(); - - let cs = - CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone()) - .await?; - cs.start(shutdown); - - Ok(EcashManager { - shared_state, - pk_bytes, - pay_infos: Default::default(), - cred_sender, - }) - } - - pub async fn verification_key( +#[async_trait] +impl traits::EcashManager for EcashManager { + async fn verification_key( &self, epoch_id: EpochId, - ) -> Result, EcashTicketError> { + ) -> Result, EcashTicketError> { self.shared_state.verification_key(epoch_id).await } - pub fn storage(&self) -> &GatewayStorage { - &self.shared_state.storage + fn storage(&self) -> Box { + dyn_clone::clone_box(&*self.shared_state.storage) } //Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded - pub async fn check_payment( + async fn check_payment( &self, credential: &CredentialSpendingData, aggregated_verification_key: &VerificationKeyAuth, @@ -88,6 +68,40 @@ impl EcashManager { .await } + fn async_verify(&self, ticket: ClientTicket) { + // TODO: I guess do something for shutdowns + let _ = self + .cred_sender + .unbounded_send(ticket) + .inspect_err(|_| error!("failed to send the client ticket for verification task")); + } +} + +impl EcashManager { + pub async fn new( + credential_handler_cfg: CredentialHandlerConfig, + nyxd_client: DirectSigningHttpRpcNyxdClient, + pk_bytes: [u8; 32], + shutdown: nym_task::TaskClient, + storage: GatewayStorage, + ) -> Result { + let shared_state = SharedState::new(nyxd_client, Box::new(storage)).await?; + + let (cred_sender, cred_receiver) = mpsc::unbounded(); + + let cs = + CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone()) + .await?; + cs.start(shutdown); + + Ok(EcashManager { + shared_state, + pk_bytes, + pay_infos: Default::default(), + cred_sender, + }) + } + pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result { //Public key check if pay_info.pk() != self.pk_bytes { @@ -152,12 +166,86 @@ impl EcashManager { inner.insert(index, pay_info); Ok(()) } +} - pub fn async_verify(&self, ticket: ClientTicket) { - // TODO: I guess do something for shutdowns - let _ = self - .cred_sender - .unbounded_send(ticket) - .inspect_err(|_| error!("failed to send the client ticket for verification task")); +pub struct MockEcashManager { + verfication_key: tokio::sync::RwLock, + storage: Box, +} + +impl MockEcashManager { + pub fn new(storage: Box) -> Self { + Self { + verfication_key: tokio::sync::RwLock::new( + VerificationKeyAuth::from_bytes(&[ + 129, 187, 76, 12, 1, 51, 46, 26, 132, 205, 148, 109, 140, 131, 50, 119, 45, + 128, 51, 218, 106, 70, 181, 74, 244, 38, 162, 62, 42, 12, 5, 100, 7, 136, 32, + 155, 18, 219, 195, 182, 3, 56, 168, 16, 93, 154, 249, 230, 16, 202, 90, 134, + 246, 25, 98, 6, 175, 215, 188, 239, 71, 84, 66, 1, 43, 66, 197, 180, 216, 80, + 55, 185, 140, 216, 14, 48, 244, 214, 20, 68, 106, 41, 48, 252, 188, 181, 231, + 170, 23, 211, 215, 12, 91, 147, 47, 7, 4, 0, 0, 0, 0, 0, 0, 0, 174, 31, 237, + 215, 159, 183, 71, 125, 90, 147, 84, 78, 49, 216, 66, 232, 92, 206, 41, 230, + 239, 209, 211, 166, 131, 190, 148, 36, 225, 194, 146, 6, 120, 34, 194, 5, 154, + 155, 234, 41, 191, 119, 227, 51, 91, 128, 151, 240, 129, 208, 253, 171, 234, + 170, 71, 139, 251, 78, 49, 35, 218, 16, 77, 150, 177, 204, 83, 210, 67, 147, + 66, 162, 58, 25, 96, 168, 61, 180, 92, 21, 18, 78, 194, 98, 176, 123, 122, 176, + 81, 150, 187, 20, 64, 69, 0, 134, 142, 3, 84, 108, 3, 55, 107, 111, 73, 31, 46, + 51, 225, 248, 202, 173, 194, 24, 104, 96, 31, 61, 24, 140, 220, 31, 176, 200, + 30, 217, 66, 58, 11, 181, 158, 196, 179, 199, 177, 7, 210, 4, 119, 142, 149, + 59, 3, 186, 145, 27, 230, 125, 230, 246, 197, 196, 119, 70, 239, 115, 99, 215, + 63, 205, 63, 74, 108, 201, 42, 226, 150, 137, 3, 157, 45, 25, 163, 54, 107, + 153, 61, 141, 64, 207, 139, 41, 203, 39, 36, 97, 181, 72, 206, 235, 221, 178, + 171, 60, 4, 6, 170, 181, 213, 10, 216, 53, 28, 32, 33, 41, 224, 60, 247, 206, + 137, 108, 251, 229, 234, 112, 65, 145, 124, 212, 125, 116, 154, 114, 2, 125, + 202, 24, 25, 196, 219, 104, 200, 131, 133, 180, 39, 21, 144, 204, 8, 151, 218, + 99, 64, 209, 47, 5, 42, 13, 214, 139, 54, 112, 224, 53, 238, 250, 56, 42, 105, + 15, 21, 238, 99, 225, 79, 121, 104, 155, 230, 243, 133, 47, 39, 147, 98, 45, + 113, 137, 200, 102, 151, 122, 174, 9, 250, 17, 138, 191, 129, 202, 244, 107, + 75, 48, 141, 136, 89, 168, 124, 88, 174, 251, 17, 35, 146, 88, 76, 134, 102, + 105, 204, 16, 176, 214, 63, 13, 170, 225, 250, 112, 7, 237, 161, 160, 15, 71, + 10, 130, 137, 69, 186, 64, 223, 188, 5, 5, 228, 57, 214, 134, 247, 20, 171, + 140, 43, 230, 57, 29, 127, 136, 169, 80, 14, 137, 130, 200, 205, 222, 81, 143, + 40, 77, 68, 197, 91, 142, 91, 84, 164, 15, 133, 242, 149, 255, 173, 201, 108, + 208, 23, 188, 230, 158, 146, 54, 198, 52, 148, 123, 202, 52, 222, 50, 4, 62, + 211, 208, 176, 61, 104, 151, 227, 192, 224, 200, 132, 53, 187, 240, 254, 150, + 60, 30, 140, 11, 63, 71, 12, 30, 233, 255, 144, 250, 16, 81, 38, 33, 9, 185, + 195, 214, 0, 119, 117, 94, 100, 103, 144, 10, 189, 65, 113, 114, 192, 11, 177, + 214, 223, 218, 36, 139, 183, 2, 206, 247, 245, 88, 62, 231, 183, 50, 46, 95, + 202, 152, 82, 244, 80, 173, 192, 147, 51, 248, 46, 181, 194, 205, 233, 67, 144, + 155, 250, 142, 124, 71, 9, 136, 142, 88, 29, 99, 222, 43, 181, 172, 120, 187, + 179, 172, 240, 231, 57, 236, 195, 158, 182, 203, 19, 49, 220, 180, 212, 101, + 105, 239, 58, 215, 0, 50, 100, 172, 29, 236, 170, 108, 129, 150, 5, 64, 238, + 59, 50, 4, 21, 131, 197, 142, 191, 76, 101, 140, 133, 112, 38, 235, 113, 203, + 22, 161, 204, 84, 73, 125, 219, 70, 62, 67, 119, 52, 130, 208, 180, 231, 78, + 141, 181, 13, 207, 196, 126, 159, 70, 34, 195, 70, + ]) + .unwrap(), + ), + storage: dyn_clone::clone_box(&*storage), + } } } + +#[async_trait] +impl traits::EcashManager for MockEcashManager { + async fn verification_key( + &self, + _epoch_id: EpochId, + ) -> Result, EcashTicketError> { + Ok(self.verfication_key.read().await) + } + + fn storage(&self) -> Box { + dyn_clone::clone_box(&*self.storage) + } + + async fn check_payment( + &self, + _credential: &CredentialSpendingData, + _aggregated_verification_key: &VerificationKeyAuth, + ) -> Result<(), EcashTicketError> { + Ok(()) + } + + fn async_verify(&self, _ticket: ClientTicket) {} +} diff --git a/common/credential-verification/src/ecash/state.rs b/common/credential-verification/src/ecash/state.rs index 2c937fd692..85ada02f5c 100644 --- a/common/credential-verification/src/ecash/state.rs +++ b/common/credential-verification/src/ecash/state.rs @@ -6,7 +6,7 @@ use crate::Error; use cosmwasm_std::{from_json, CosmosMsg, WasmMsg}; use nym_credentials_interface::VerificationKeyAuth; use nym_ecash_contract_common::msg::ExecuteMsg; -use nym_gateway_storage::GatewayStorage; +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::contract_traits::{ @@ -22,18 +22,28 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tracing::{error, trace, warn}; // state shared by different subtasks dealing with credentials -#[derive(Clone)] pub(crate) struct SharedState { pub(crate) nyxd_client: Arc>, pub(crate) address: AccountId, pub(crate) epoch_data: Arc>>, - pub(crate) storage: GatewayStorage, + pub(crate) storage: Box, +} + +impl Clone for SharedState { + fn clone(&self) -> Self { + Self { + nyxd_client: self.nyxd_client.clone(), + address: self.address.clone(), + epoch_data: self.epoch_data.clone(), + storage: dyn_clone::clone_box(&*self.storage), + } + } } impl SharedState { pub(crate) async fn new( nyxd_client: DirectSigningHttpRpcNyxdClient, - storage: GatewayStorage, + storage: Box, ) -> Result { let address = nyxd_client.address(); @@ -115,7 +125,7 @@ impl SharedState { async fn set_epoch_data( &self, epoch_id: EpochId, - ) -> Result>, EcashTicketError> { + ) -> Result>, EcashTicketError> { let Some(threshold) = self.threshold(epoch_id).await? else { return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id }); }; @@ -176,7 +186,7 @@ impl SharedState { pub(crate) async fn api_clients( &self, epoch_id: EpochId, - ) -> Result>, EcashTicketError> { + ) -> Result>, EcashTicketError> { let guard = self.epoch_data.read().await; // the key was already in the map @@ -202,7 +212,7 @@ impl SharedState { pub(crate) async fn verification_key( &self, epoch_id: EpochId, - ) -> Result, EcashTicketError> { + ) -> Result, EcashTicketError> { let guard = self.epoch_data.read().await; // the key was already in the map @@ -225,11 +235,11 @@ impl SharedState { })) } - pub(crate) async fn start_tx(&self) -> RwLockWriteGuard { + pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> { self.nyxd_client.write().await } - pub(crate) async fn start_query(&self) -> RwLockReadGuard { + pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> { self.nyxd_client.read().await } diff --git a/common/credential-verification/src/ecash/traits.rs b/common/credential-verification/src/ecash/traits.rs new file mode 100644 index 0000000000..ae25016f19 --- /dev/null +++ b/common/credential-verification/src/ecash/traits.rs @@ -0,0 +1,23 @@ +use async_trait::async_trait; +use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::{ClientTicket, VerificationKeyAuth}; +use nym_gateway_storage::traits::BandwidthGatewayStorage; +use nym_validator_client::nym_api::EpochId; +use tokio::sync::RwLockReadGuard; + +use crate::ecash::error::EcashTicketError; + +#[async_trait] +pub trait EcashManager { + async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, EcashTicketError>; + fn storage(&self) -> Box; + async fn check_payment( + &self, + credential: &CredentialSpendingData, + aggregated_verification_key: &VerificationKeyAuth, + ) -> Result<(), EcashTicketError>; + fn async_verify(&self, ticket: ClientTicket); +} diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 50d6db1e05..61f9a1b30c 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -1,8 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::ecash::traits::EcashManager; +use async_trait::async_trait; use bandwidth_storage_manager::BandwidthStorageManager; -use ecash::EcashManager; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; use nym_gateway_requests::models::CredentialSpendingRequest; @@ -20,14 +21,14 @@ pub mod error; pub struct CredentialVerifier { credential: CredentialSpendingRequest, - ecash_verifier: Arc, + ecash_verifier: Arc, bandwidth_storage_manager: BandwidthStorageManager, } impl CredentialVerifier { pub fn new( credential: CredentialSpendingRequest, - ecash_verifier: Arc, + ecash_verifier: Arc, bandwidth_storage_manager: BandwidthStorageManager, ) -> Self { CredentialVerifier { @@ -139,3 +140,18 @@ impl CredentialVerifier { .await) } } + +#[async_trait] +pub trait TicketVerifier { + /// Verify that the ticket is valid and cryptographically correct. + /// If the verification succeeds, also increase the bandwidth with the ticket's + /// amount and return the latest available bandwidth + async fn verify(&mut self) -> Result; +} + +#[async_trait] +impl TicketVerifier for CredentialVerifier { + async fn verify(&mut self) -> Result { + self.verify().await + } +} diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index f7f957d68b..d6d4ce842b 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -15,6 +15,7 @@ bls12_381 = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } time = { workspace = true, features = ["serde"] } utoipa = { workspace = true } rand = { workspace = true } diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index cf1b138677..9653a3a72d 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -229,9 +229,9 @@ impl From for NymPayInfo { Serialize, Deserialize, Hash, - strum::Display, - strum::EnumString, - strum::EnumIter, + strum_macros::Display, + strum_macros::EnumString, + strum_macros::EnumIter, )] #[serde(rename_all = "kebab-case")] #[strum(serialize_all = "kebab-case")] diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs index 3d1cacc72d..f6de39c259 100644 --- a/common/credentials/src/ecash/bandwidth/issuance.rs +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -108,7 +108,7 @@ impl IssuanceTicketBook { signing_request.withdrawal_request.clone(), self.deposit_id, request_signature, - signing_request.ecash_pub_key.clone(), + signing_request.ecash_pub_key, signing_request.expiration_date, signing_request.ticketbook_type, ) diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs index 96d24ad018..305aafd532 100644 --- a/common/credentials/src/ecash/utils.rs +++ b/common/credentials/src/ecash/utils.rs @@ -51,7 +51,7 @@ pub async fn obtain_expiration_date_signatures( for ecash_api_client in ecash_api_clients.iter() { match ecash_api_client .api_client - .partial_expiration_date_signatures(None) + .partial_expiration_date_signatures(None, None) .await { Ok(signature) => { diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 6e1f8cfb12..96549a448d 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -11,6 +11,7 @@ repository = { workspace = true } aes-gcm-siv = { workspace = true, optional = true } aes = { workspace = true, optional = true } aead = { workspace = true, optional = true } +base64.workspace = true bs58 = { workspace = true } blake3 = { workspace = true, features = ["traits-preview"], optional = true } ctr = { workspace = true, optional = true } @@ -18,6 +19,7 @@ digest = { workspace = true, optional = true } generic-array = { workspace = true, optional = true } hkdf = { workspace = true, optional = true } hmac = { workspace = true, optional = true } +jwt-simple = { workspace = true, optional = true } cipher = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true } ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true } @@ -39,6 +41,7 @@ rand_chacha = { workspace = true } [features] default = [] aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] +naive_jwt = ["asymmetric", "jwt-simple"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"] diff --git a/common/crypto/src/asymmetric/ed25519/mod.rs b/common/crypto/src/asymmetric/ed25519/mod.rs index 387034a8d1..8d23ee7d8c 100644 --- a/common/crypto/src/asymmetric/ed25519/mod.rs +++ b/common/crypto/src/asymmetric/ed25519/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 pub use ed25519_dalek::SignatureError; -use ed25519_dalek::{SecretKey, Signer, SigningKey}; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; + +use ed25519_dalek::Signer; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; @@ -13,6 +14,9 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; #[cfg(feature = "serde")] pub mod serde_helpers; +#[cfg(feature = "serde")] +pub use serde_helpers::*; + #[cfg(feature = "sphinx")] use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; @@ -81,8 +85,8 @@ impl KeyPair { } } - pub fn from_secret(secret: SecretKey, index: u32) -> Self { - let ed25519_signing_key = SigningKey::from(secret); + pub fn from_secret(secret: ed25519_dalek::SecretKey, index: u32) -> Self { + let ed25519_signing_key = ed25519_dalek::SigningKey::from(secret); KeyPair { private_key: PrivateKey(ed25519_signing_key.to_bytes()), @@ -276,7 +280,7 @@ impl Display for PrivateKey { impl<'a> From<&'a PrivateKey> for PublicKey { fn from(pk: &'a PrivateKey) -> Self { - PublicKey(SigningKey::from_bytes(&pk.0).verifying_key()) + PublicKey(ed25519_dalek::SigningKey::from_bytes(&pk.0).verifying_key()) } } @@ -320,7 +324,7 @@ impl PrivateKey { } pub fn sign>(&self, message: M) -> Signature { - let signing_key: SigningKey = self.0.into(); + let signing_key: ed25519_dalek::SigningKey = self.0.into(); let sig = signing_key.sign(message.as_ref()); Signature(sig) } @@ -425,9 +429,57 @@ impl<'d> Deserialize<'d> for Signature { } } +#[cfg(feature = "naive_jwt")] +impl PublicKey { + pub fn to_jwt_compatible_key(&self) -> jwt_simple::algorithms::Ed25519PublicKey { + (*self).into() + } +} + +#[cfg(feature = "naive_jwt")] +impl From for jwt_simple::algorithms::Ed25519PublicKey { + fn from(value: PublicKey) -> Self { + // SAFETY: we have a valid ed25519 pubkey, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519PublicKey::from_bytes(&value.to_bytes()).unwrap() + } +} + +#[cfg(feature = "naive_jwt")] +impl PrivateKey { + pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair { + let pub_key = self.public_key(); + let mut bytes = zeroize::Zeroizing::new([0u8; 64]); + + bytes[..SECRET_KEY_LENGTH] + .copy_from_slice(zeroize::Zeroizing::new(self.to_bytes()).as_ref()); + bytes[SECRET_KEY_LENGTH..].copy_from_slice(&pub_key.to_bytes()); + + // SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap() + } +} + +#[cfg(feature = "naive_jwt")] +impl KeyPair { + pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair { + let mut bytes = zeroize::Zeroizing::new([0u8; 64]); + + bytes[..SECRET_KEY_LENGTH] + .copy_from_slice(zeroize::Zeroizing::new(self.private_key.to_bytes()).as_ref()); + bytes[SECRET_KEY_LENGTH..].copy_from_slice(&self.public_key.to_bytes()); + + // SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap() + } +} + #[cfg(test)] mod tests { use super::*; + use rand::thread_rng; fn assert_zeroize_on_drop() {} @@ -438,4 +490,29 @@ mod tests { assert_zeroize::(); assert_zeroize_on_drop::(); } + + #[test] + #[cfg(all(feature = "naive_jwt", feature = "rand"))] + fn check_jwt_key_compat_conversion() { + let mut rng = thread_rng(); + let keys = KeyPair::new(&mut rng); + let jwt_keys = keys.to_jwt_compatible_keys(); + + // internally they're represented by hidden `Edwards25519KeyPair` (plus key_id) + // which has way nicer API for assertions + let jwt_keys_inner = + jwt_simple::algorithms::Edwards25519KeyPair::from_bytes(&jwt_keys.to_bytes()).unwrap(); + + let compact_ed25519 = jwt_keys_inner.as_ref(); + assert!(compact_ed25519 + .sk + .validate_public_key(&compact_ed25519.pk) + .is_ok()); + + let dummy_message = "hello world"; + let sig1 = keys.private_key.sign(dummy_message).to_bytes(); + let sig2 = compact_ed25519.sk.sign(dummy_message, None).to_vec(); + + assert_eq!(sig1.to_vec(), sig2); + } } diff --git a/common/crypto/src/asymmetric/x25519/mod.rs b/common/crypto/src/asymmetric/x25519/mod.rs index 4b580f289b..3f41319a97 100644 --- a/common/crypto/src/asymmetric/x25519/mod.rs +++ b/common/crypto/src/asymmetric/x25519/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use base64::Engine; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; @@ -158,6 +159,15 @@ impl PublicKey { .map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?; Self::from_bytes(&bytes) } + + pub fn from_base64(s: &str) -> Option { + let bytes = base64::engine::general_purpose::STANDARD.decode(s).ok()?; + Self::from_bytes(&bytes).ok() + } + + pub fn to_base64(&self) -> String { + base64::engine::general_purpose::STANDARD.encode(self.as_bytes()) + } } impl FromStr for PublicKey { @@ -218,6 +228,12 @@ impl From for x25519_dalek::PublicKey { } } +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + #[derive(Zeroize, ZeroizeOnDrop)] pub struct PrivateKey(x25519_dalek::StaticSecret); @@ -248,6 +264,10 @@ impl PrivateKey { PrivateKey(x25519_secret) } + pub fn inner(&self) -> &x25519_dalek::StaticSecret { + &self.0 + } + pub fn public_key(&self) -> PublicKey { self.into() } @@ -256,6 +276,10 @@ impl PrivateKey { self.0.to_bytes() } + pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_SIZE] { + self.0.as_bytes() + } + pub fn from_bytes(b: &[u8]) -> Result { if b.len() != PRIVATE_KEY_SIZE { return Err(KeyRecoveryError::InvalidSizePrivateKey { @@ -335,6 +359,12 @@ impl AsRef for PrivateKey { } } +impl AsRef<[u8]> for PrivateKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/dkg/benches/benchmarks.rs b/common/dkg/benches/benchmarks.rs index 2951a57a7c..d12305bba7 100644 --- a/common/dkg/benches/benchmarks.rs +++ b/common/dkg/benches/benchmarks.rs @@ -14,6 +14,7 @@ use nym_dkg::bte::{ }; use nym_dkg::interpolation::polynomial::Polynomial; use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold}; +use rand::CryptoRng; use rand_core::{RngCore, SeedableRng}; use std::collections::BTreeMap; @@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) { } fn prepare_keys( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, nodes: usize, ) -> (BTreeMap, Vec) { let params = setup(); @@ -50,7 +51,7 @@ fn prepare_keys( } fn prepare_resharing( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, params: &Params, nodes: usize, threshold: Threshold, @@ -68,7 +69,7 @@ fn prepare_resharing( for (i, ref mut dk) in dks.iter_mut().enumerate() { let shares = first_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -154,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -237,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -320,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite |b| { b.iter(|| { assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); + black_box( + decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(), + ); }) }, ); @@ -547,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) { let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng); c.bench_function("single share decryption", |b| { - b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None))) + b.iter(|| black_box(decrypt_share(¶ms, &dk, 0, &ciphertexts, None))) }); } diff --git a/common/dkg/src/bte/encryption.rs b/common/dkg/src/bte/encryption.rs index 8cd6a3e774..81b304bc7b 100644 --- a/common/dkg/src/bte/encryption.rs +++ b/common/dkg/src/bte/encryption.rs @@ -9,6 +9,7 @@ use crate::{Chunk, ChunkedShare, Share}; use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar}; use ff::Field; use group::{Curve, Group, GroupEncoding}; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::HashMap; use std::ops::Neg; @@ -191,7 +192,7 @@ impl HazmatRandomness { pub fn encrypt_shares( shares: &[(&Share, &PublicKey)], params: &Params, - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, ) -> (Ciphertexts, HazmatRandomness) { let g1 = G1Projective::generator(); @@ -262,6 +263,7 @@ pub fn encrypt_shares( } pub fn decrypt_share( + params: &Params, dk: &DecryptionKey, // in the case of multiple receivers, specifies which index of ciphertext chunks should be used i: usize, @@ -270,6 +272,10 @@ pub fn decrypt_share( ) -> Result { let mut plaintext = ChunkedShare::default(); + if !ciphertext.verify_integrity(params) { + return Err(DkgError::FailedCiphertextIntegrityCheck); + } + if i >= ciphertext.ciphertext_chunks.len() { return Err(DkgError::UnavailableCiphertext(i)); } @@ -461,10 +467,22 @@ mod tests { let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); - let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); - let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); + let recovered1 = decrypt_share( + ¶ms, + &decryption_key1, + 0, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); + let recovered2 = decrypt_share( + ¶ms, + &decryption_key2, + 1, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -490,10 +508,22 @@ mod tests { let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); - let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); - let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); + let recovered1 = decrypt_share( + ¶ms, + &decryption_key1, + 0, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); + let recovered2 = decrypt_share( + ¶ms, + &decryption_key2, + 1, + &ciphertext, + Some(lookup_table), + ) + .unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -574,7 +604,10 @@ mod tests { #[test] fn ciphertexts_roundtrip() { - fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts { + fn random_ciphertexts( + mut rng: impl RngCore + CryptoRng, + num_receivers: usize, + ) -> Ciphertexts { Ciphertexts { rr: (0..NUM_CHUNKS) .map(|_| G1Projective::random(&mut rng)) diff --git a/common/dkg/src/bte/keys.rs b/common/dkg/src/bte/keys.rs index 132f161f5e..20d896a547 100644 --- a/common/dkg/src/bte/keys.rs +++ b/common/dkg/src/bte/keys.rs @@ -9,11 +9,15 @@ use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; +use rand::CryptoRng; use rand_core::RngCore; use zeroize::Zeroize; // produces public key and a decryption key for the root of the tree -pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) { +pub fn keygen( + params: &Params, + mut rng: impl RngCore + CryptoRng, +) -> (DecryptionKey, PublicKeyWithProof) { let g1 = G1Projective::generator(); let g2 = G2Projective::generator(); @@ -244,7 +248,7 @@ pub struct KeyPair { } impl KeyPair { - pub fn new(params: &Params, rng: impl RngCore) -> Self { + pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self { let (dk, pk) = keygen(params, rng); Self { private_key: dk, diff --git a/common/dkg/src/bte/proof_chunking.rs b/common/dkg/src/bte/proof_chunking.rs index 1365aecbd7..2da6e83b33 100644 --- a/common/dkg/src/bte/proof_chunking.rs +++ b/common/dkg/src/bte/proof_chunking.rs @@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder}; use bls12_381::{G1Projective, Scalar}; use ff::Field; use group::{Group, GroupEncoding}; -use rand::Rng; +use rand::{CryptoRng, Rng}; use rand_core::{RngCore, SeedableRng}; const CHUNKING_ORACLE_DOMAIN: &[u8] = @@ -28,6 +28,7 @@ const SECURITY_PARAMETER: usize = 256; /// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS); +const EE: usize = 1 << NUM_CHALLENGE_BITS; // type alias for ease of use type FirstChallenge = Vec>>; @@ -94,7 +95,7 @@ impl ProofOfChunking { // Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to // what we wanted. pub fn construct( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, instance: Instance, witness_r: &[Scalar; NUM_CHUNKS], witnesses_s: &[Share], @@ -110,21 +111,20 @@ impl ProofOfChunking { // define bounds for the blinding factors let n = instance.public_keys.len(); let m = NUM_CHUNKS; - let ee = 1 << NUM_CHALLENGE_BITS; - // CHUNK_MAX corresponds to paper's B - let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; - let zz = (2 * (PARALLEL_RUNS as u64)) - .checked_mul(ss) - .expect("overflow in Z = 2 * l * S"); + // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) + // Z = 2 * l * S + let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?; let ss_scalar = Scalar::from(ss); // rather than generating blinding factors in [-S, Z-1] directly, // do it via [0, Z - 1 + S + 1] and deal with the shift later. - let combined_upper_range = (zz - 1) - .checked_add(ss + 1) - .expect("overflow in Z - 1 + S + 1"); + // combined_upper_range = Z - 1 + S + 1 + + let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | Z - 1 + S + 1", + })?; let mut betas = Vec::with_capacity(PARALLEL_RUNS); let mut bs = Vec::with_capacity(PARALLEL_RUNS); @@ -178,12 +178,23 @@ impl ProofOfChunking { // I think this part is more readable with a range loop #[allow(clippy::needless_range_loop)] for l in 0..PARALLEL_RUNS { - let mut sum = 0; + let mut sum: u64 = 0; for (i, witness_i) in witnesses_s.iter().enumerate() { for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() { debug_assert!(std::mem::size_of::() <= std::mem::size_of::()); - sum += first_challenge[i][j][l] * (*witness_ij as u64) + // sum += first_challenge[i][j][l] * (*witness_ij as u64) + sum = sum + .checked_add( + first_challenge[i][j][l] + .checked_mul(*witness_ij as u64) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij", + })?, + ) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)", + })?; } } @@ -191,7 +202,18 @@ impl ProofOfChunking { continue 'retry_loop; } // shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range - let response = sum + shifted_blinding_factors[l] - ss; + // let response = sum + shifted_blinding_factors[l] - ss; + let response = sum + .checked_add(shifted_blinding_factors[l]) + .ok_or(DkgError::ArithmeticOverflow { + info: + "ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)", + })? + .checked_sub(ss) + .ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss", + })?; + if response < zz { responses_chunks.push(response) } else { @@ -276,11 +298,13 @@ impl ProofOfChunking { ensure_len!(&self.responses_r, n); ensure_len!(&self.responses_chunks, PARALLEL_RUNS); - let ee = 1 << NUM_CHALLENGE_BITS; + // ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) + // Z = 2 * l * S - // CHUNK_MAX corresponds to paper's B - let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; - let zz = 2 * (PARALLEL_RUNS as u64) * ss; + let zz: u64 = match compute_ss_zz(n, m) { + Ok((_, zz_res)) => zz_res, + _ => return false, + }; for response_chunk in &self.responses_chunks { if response_chunk >= &zz { @@ -411,7 +435,7 @@ impl ProofOfChunking { random_oracle_builder.update(lambda_e.to_be_bytes()); let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize()); - let range_max_excl = 1 << NUM_CHALLENGE_BITS; + let range_max_excl = EE as u64; (0..n) .map(|_| { @@ -637,6 +661,50 @@ impl ProofOfChunking { } } +fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> { + // let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64; + // CHUNK_MAX corresponds to paper's B + + let ee = EE; + + let ss = n + .checked_mul(m) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | n * m", + })? + .checked_mul( + CHUNK_SIZE + .checked_sub(1) + .ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)", + })? + .checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow { + info: "ProofOfChunking::compute_ss_zz | (ee - 1)", + })?) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)", + })?, + ) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs", + })? as u64; + + // let zz = 2 * PARALLEL_RUNS as u64 * ss; + // Z = 2 * l * S + + let zz = 2u64 + .checked_mul(PARALLEL_RUNS as u64) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | 2 * l", + })? + .checked_mul(ss) + .ok_or(DkgError::ArithmeticOverflow { + info: "ProofOfChunking::compute_ss_zz | (2 * l) * S", + })?; + + Ok((ss, zz)) +} + #[cfg(test)] mod tests { use super::*; @@ -652,7 +720,9 @@ mod tests { ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>, } - fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec) { + fn setup( + mut rng: impl RngCore + CryptoRng, + ) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec) { let g1 = G1Projective::generator(); let mut pks = Vec::with_capacity(NODES); diff --git a/common/dkg/src/bte/proof_discrete_log.rs b/common/dkg/src/bte/proof_discrete_log.rs index 95eef6eb42..3956e3837d 100644 --- a/common/dkg/src/bte/proof_discrete_log.rs +++ b/common/dkg/src/bte/proof_discrete_log.rs @@ -5,6 +5,7 @@ use crate::utils::hash_to_scalar; use bls12_381::{G1Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use zeroize::Zeroize; @@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog { } impl ProofOfDiscreteLog { - pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self { + pub fn construct( + mut rng: impl RngCore + CryptoRng, + public: &G1Projective, + witness: &Scalar, + ) -> Self { let mut rand_x = Scalar::random(&mut rng); let rand_commitment = G1Projective::generator() * rand_x; let challenge = Self::compute_challenge(public, &rand_commitment); diff --git a/common/dkg/src/bte/proof_sharing.rs b/common/dkg/src/bte/proof_sharing.rs index 4b843bb8f4..fe088c772f 100644 --- a/common/dkg/src/bte/proof_sharing.rs +++ b/common/dkg/src/bte/proof_sharing.rs @@ -9,6 +9,7 @@ use crate::{NodeIndex, Share}; use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::BTreeMap; @@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing { impl ProofOfSecretSharing { pub fn construct( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, instance: Instance, witness_r: &Scalar, witnesses_s: &[Share], @@ -309,13 +310,14 @@ mod tests { use super::*; use crate::interpolation::polynomial::Polynomial; use group::Group; + use rand::CryptoRng; use rand_core::SeedableRng; const NODES: u64 = 50; const THRESHOLD: u64 = 40; fn setup( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng, ) -> ( BTreeMap, PublicCoefficients, diff --git a/common/dkg/src/dealing.rs b/common/dkg/src/dealing.rs index a051f9f88c..e9a96886c2 100644 --- a/common/dkg/src/dealing.rs +++ b/common/dkg/src/dealing.rs @@ -13,6 +13,7 @@ use crate::utils::deserialize_g2; use crate::{NodeIndex, Share, Threshold}; use bls12_381::{G2Projective, Scalar}; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; @@ -94,7 +95,7 @@ impl Dealing { // I'm not a big fan of this function signature, but I'm not clear on how to improve it while // allowing the dealer to skip decryption of its own share if it was also one of the receivers pub fn create( - mut rng: impl RngCore, + mut rng: impl RngCore + CryptoRng + CryptoRng, params: &Params, dealer_index: NodeIndex, threshold: Threshold, @@ -484,7 +485,7 @@ mod tests { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); derived_secrets.push( combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(), @@ -593,7 +594,7 @@ mod tests { for (i, (dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &dealer_indices).unwrap(); diff --git a/common/dkg/src/error.rs b/common/dkg/src/error.rs index 9329f32794..5bea0fe3a3 100644 --- a/common/dkg/src/error.rs +++ b/common/dkg/src/error.rs @@ -99,6 +99,12 @@ pub enum DkgError { "The reshared dealing has different public constant coefficient than its prior variant" )] InvalidResharing, + + #[error("Arithmetic Overflow: {info}")] + ArithmeticOverflow { info: &'static str }, + + #[error("Arithmetic Underflow: {info}")] + ArithmeticUnderflow { info: &'static str }, } impl DkgError { diff --git a/common/dkg/src/interpolation/polynomial.rs b/common/dkg/src/interpolation/polynomial.rs index 671b8e6832..f3874806e1 100644 --- a/common/dkg/src/interpolation/polynomial.rs +++ b/common/dkg/src/interpolation/polynomial.rs @@ -6,6 +6,7 @@ use crate::utils::deserialize_g2; use bls12_381::{G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use rand::CryptoRng; use rand_core::RngCore; use std::ops::{Add, Index, IndexMut}; use zeroize::Zeroize; @@ -120,7 +121,7 @@ impl Polynomial { // for polynomial of degree n, we generate n+1 values // (for example for degree 1, like y = x + 2, we need [2,1]) /// Creates new pseudorandom polynomial of specified degree. - pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self { + pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self { Polynomial { coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(), } diff --git a/common/dkg/tests/integration.rs b/common/dkg/tests/integration.rs index 81c861425d..e34195488e 100644 --- a/common/dkg/tests/integration.rs +++ b/common/dkg/tests/integration.rs @@ -53,11 +53,12 @@ fn single_sender() { // make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check) for (i, (ref dk, _)) in full_keys.iter().enumerate() { - let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap(); + let _recovered = decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap(); } // and for good measure, check that the dealer's share matches decryption result - let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); + let recovered_dealer = + decrypt_share(¶ms, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); assert_eq!(recovered_dealer, dealer_share.unwrap()); } @@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); // we know dealer_share matches, but it would be inconvenient to try to put them in here, @@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = first_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = resharing_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -305,7 +306,7 @@ fn full_threshold_secret_resharing_left_party() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = first_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -369,7 +370,7 @@ fn full_threshold_secret_resharing_left_party() { for (i, (ref dk, _)) in full_keys.iter().enumerate() { let shares = resharing_dealings .values() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = combine_shares(shares, &node_indices).unwrap(); diff --git a/common/ecash-signer-check-types/Cargo.toml b/common/ecash-signer-check-types/Cargo.toml new file mode 100644 index 0000000000..0f07437c71 --- /dev/null +++ b/common/ecash-signer-check-types/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nym-ecash-signer-check-types" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +semver = { workspace = true } +serde = { workspace = true, features = ["derive"] } +url = { workspace = true } +thiserror = { workspace = true } +time = { workspace = true } +tracing = { workspace = true } +utoipa = { workspace = true } + +nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } + + +[lints] +workspace = true diff --git a/common/ecash-signer-check-types/src/dealer_information.rs b/common/ecash-signer-check-types/src/dealer_information.rs new file mode 100644 index 0000000000..b589fecb10 --- /dev/null +++ b/common/ecash-signer-check-types/src/dealer_information.rs @@ -0,0 +1,97 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_coconut_dkg_common::dealer::DealerDetails; +use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use url::Url; +use utoipa::ToSchema; + +#[derive(Debug, Error)] +pub enum MalformedDealer { + #[error("dealer at {dealer_url} has provided invalid ed25519 pubkey: {source}")] + InvalidDealerPubkey { + dealer_url: String, + source: Ed25519RecoveryError, + }, + + #[error("dealer at {dealer_url} has provided invalid announce url: {source}")] + InvalidDealerAddress { + dealer_url: String, + source: url::ParseError, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct RawDealerInformation { + pub announce_address: String, + pub owner_address: String, + pub node_index: u64, + pub public_key: String, + pub verification_key_share: Option, + pub share_verified: bool, +} + +impl RawDealerInformation { + pub fn new( + dealer_details: &DealerDetails, + contract_share: Option<&ContractVKShare>, + ) -> RawDealerInformation { + RawDealerInformation { + announce_address: dealer_details.announce_address.clone(), + owner_address: dealer_details.address.to_string(), + node_index: dealer_details.assigned_index, + public_key: dealer_details.ed25519_identity.clone(), + verification_key_share: contract_share.map(|s| s.share.clone()), + share_verified: contract_share.map(|s| s.verified).unwrap_or(false), + } + } + + pub fn parse(&self) -> Result { + Ok(DealerInformation { + announce_address: self.announce_address.parse().map_err(|source| { + MalformedDealer::InvalidDealerAddress { + dealer_url: self.announce_address.clone(), + source, + } + })?, + owner_address: self.owner_address.clone(), + node_index: self.node_index, + public_key: self.public_key.parse().map_err(|source| { + MalformedDealer::InvalidDealerPubkey { + dealer_url: self.announce_address.clone(), + source, + } + })?, + verification_key_share: self.verification_key_share.clone(), + share_verified: self.share_verified, + }) + } +} + +#[derive(Debug)] +pub struct DealerInformation { + pub announce_address: Url, + pub owner_address: String, + pub node_index: u64, + pub public_key: ed25519::PublicKey, + // no need to parse it into the full type as it doesn't get us anything + pub verification_key_share: Option, + pub share_verified: bool, +} + +impl From for RawDealerInformation { + fn from(d: DealerInformation) -> Self { + RawDealerInformation { + announce_address: d.announce_address.to_string(), + owner_address: d.owner_address, + node_index: d.node_index, + public_key: d.public_key.to_base58_string(), + verification_key_share: d.verification_key_share, + share_verified: d.share_verified, + } + } +} diff --git a/common/ecash-signer-check-types/src/helper_traits.rs b/common/ecash-signer-check-types/src/helper_traits.rs new file mode 100644 index 0000000000..c6d6becd33 --- /dev/null +++ b/common/ecash-signer-check-types/src/helper_traits.rs @@ -0,0 +1,127 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_coconut_dkg_common::types::EpochId; +use nym_coconut_dkg_common::verification_key::VerificationKeyShare; +use nym_crypto::asymmetric::ed25519; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::{debug, warn}; + +pub trait Verifiable { + fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool; +} + +pub trait TimestampedResponse { + fn timestamp(&self) -> OffsetDateTime; +} + +pub trait LegacyChainResponse { + fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool; +} + +pub trait ChainResponse: Verifiable + TimestampedResponse { + fn chain_synced(&self) -> bool; + + fn chain_available( + &self, + pub_key: &ed25519::PublicKey, + now: OffsetDateTime, + stale_response_threshold: Duration, + ) -> bool { + if !self.verify_signature(pub_key) { + warn!("failed signature verification on chain status response"); + return false; + } + + // we rely on information provided from the api itself AS LONG AS it's not too outdated + if self.timestamp() + stale_response_threshold < now { + return false; + } + self.chain_synced() + } +} + +pub trait LegacySignerResponse { + fn signer_identity(&self) -> &str; + + fn signer_verification_key(&self) -> &Option; + + fn unprovable_signing_available( + &self, + pub_key: &ed25519::PublicKey, + expected_verification_key: Option, + share_verified: bool, + ) -> bool { + if self.signer_identity() != pub_key.to_base58_string() { + warn!("mismatched identity key on the legacy response"); + return false; + } + + // the contract share hasn't been verified yet, so we're probably in the middle of DKG + // thus if there's a bit of desync in the state, it's fine + if !share_verified { + return true; + } + + if self.signer_verification_key() != &expected_verification_key { + warn!("mismatched [ecash] verification key on the legacy response"); + return false; + } + + true + } +} + +pub trait SignerResponse: Verifiable + TimestampedResponse { + fn has_signing_keys(&self) -> bool; + + fn signer_disabled(&self) -> bool; + + fn is_ecash_signer(&self) -> bool; + + fn dkg_ecash_epoch_id(&self) -> EpochId; + + fn provable_signing_available( + &self, + pub_key: &ed25519::PublicKey, + dkg_epoch_id: EpochId, + now: OffsetDateTime, + stale_response_threshold: Duration, + ) -> bool { + if !self.verify_signature(pub_key) { + warn!("failed signature verification on chain status response"); + return false; + } + + // we rely on information provided from the api itself AS LONG AS it's not too outdated + if self.timestamp() + stale_response_threshold < now { + return false; + } + + if !self.has_signing_keys() { + debug!("missing signing keys"); + return false; + } + + if self.signer_disabled() { + debug!("signer functionalities explicitly disabled"); + return false; + } + + if !self.is_ecash_signer() { + debug!("signer doesn't recognise it's a signer for this epoch"); + return false; + } + + if dkg_epoch_id != self.dkg_ecash_epoch_id() { + debug!( + "mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}", + self.dkg_ecash_epoch_id() + ); + return false; + } + + true + } +} diff --git a/common/ecash-signer-check-types/src/lib.rs b/common/ecash-signer-check-types/src/lib.rs new file mode 100644 index 0000000000..60e5ccd667 --- /dev/null +++ b/common/ecash-signer-check-types/src/lib.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod dealer_information; +pub mod helper_traits; +pub mod status; diff --git a/common/ecash-signer-check-types/src/status.rs b/common/ecash-signer-check-types/src/status.rs new file mode 100644 index 0000000000..ea07862633 --- /dev/null +++ b/common/ecash-signer-check-types/src/status.rs @@ -0,0 +1,311 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealer_information::RawDealerInformation; +use crate::helper_traits::{ + ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse, +}; +use nym_coconut_dkg_common::types::EpochId; +use nym_coconut_dkg_common::verification_key::VerificationKeyShare; +use nym_crypto::asymmetric::ed25519; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::OffsetDateTime; +use utoipa::ToSchema; + +pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60); +pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60); + +// the reason for generics is not to remove duplication of code, +// but because without them, we'd be having problems with circular dependencies, +// i.e. nym-api-requests depending on ecash-signer-check-types and +// ecash-signer-check-types needing nym-api-requests +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub enum Status { + /// The API, even though it reports correct version, did not response to the status query + Unreachable, + + /// The API is running an outdated version that does not expose the required endpoint + Outdated, + + /// Response to the legacy (unsigned) status query + ReachableLegacy { response: Box }, + + /// Response to the current (signed) status query + Reachable { response: Box }, +} + +impl Status +where + L: LegacyChainResponse, + T: ChainResponse, +{ + pub fn chain_available(&self, pub_key: ed25519::PublicKey) -> bool { + let now = OffsetDateTime::now_utc(); + + match self { + Status::Unreachable | Status::Outdated => false, + Status::ReachableLegacy { response } => { + response.chain_synced(now, CHAIN_STALL_THRESHOLD) + } + Status::Reachable { response } => { + response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD) + } + } + } + + pub fn chain_provably_stalled(&self, pub_key: ed25519::PublicKey) -> bool { + let now = OffsetDateTime::now_utc(); + + match self { + Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false, + Status::Reachable { response } => { + !response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD) + } + } + } + + pub fn chain_unprovably_stalled(&self) -> bool { + let now = OffsetDateTime::now_utc(); + + match self { + Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false, + Status::ReachableLegacy { response } => { + !response.chain_synced(now, CHAIN_STALL_THRESHOLD) + } + } + } +} + +impl Status +where + L: LegacySignerResponse, + T: SignerResponse, +{ + pub fn signing_available( + &self, + pub_key: ed25519::PublicKey, + dkg_epoch_id: u64, + expected_verification_key: Option, + share_verified: bool, + ) -> bool { + let now = OffsetDateTime::now_utc(); + + match self { + Status::Unreachable | Status::Outdated => false, + Status::ReachableLegacy { response } => response.unprovable_signing_available( + &pub_key, + expected_verification_key, + share_verified, + ), + Status::Reachable { response } => response.provable_signing_available( + &pub_key, + dkg_epoch_id, + now, + STALE_RESPONSE_THRESHOLD, + ), + } + } + + pub fn signing_provably_unavailable( + &self, + pub_key: ed25519::PublicKey, + dkg_epoch_id: EpochId, + ) -> bool { + let now = OffsetDateTime::now_utc(); + + match self { + Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false, + Status::Reachable { response } => !response.provable_signing_available( + &pub_key, + dkg_epoch_id, + now, + STALE_RESPONSE_THRESHOLD, + ), + } + } + + pub fn signing_unprovably_unavailable( + &self, + pub_key: ed25519::PublicKey, + expected_verification_key: Option, + share_verified: bool, + ) -> bool { + match self { + Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false, + Status::ReachableLegacy { response } => !response.unprovable_signing_available( + &pub_key, + expected_verification_key, + share_verified, + ), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct SignerResult { + pub dkg_epoch_id: u64, + pub information: RawDealerInformation, + pub status: SignerStatus, +} + +impl SignerResult { + pub fn signer_unreachable(&self) -> bool { + matches!(self.status, SignerStatus::Unreachable) + } + + pub fn malformed_details(&self) -> bool { + self.information.parse().is_err() + } + + pub fn try_get_test_result(&self) -> Option<&SignerTestResult> { + if let SignerStatus::Tested { result } = &self.status { + Some(result) + } else { + None + } + } +} + +impl SignerResult +where + LC: LegacyChainResponse, + TC: ChainResponse, +{ + pub fn unknown_chain_status(&self) -> bool { + let Ok(_) = self.information.parse() else { + return true; + }; + if let SignerStatus::Tested { .. } = &self.status { + return false; + } + true + } + + pub fn chain_available(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + result + .local_chain_status + .chain_available(parsed_info.public_key) + } + + pub fn chain_provably_stalled(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + + result + .local_chain_status + .chain_provably_stalled(parsed_info.public_key) + } + + pub fn chain_unprovably_stalled(&self) -> bool { + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + + result.local_chain_status.chain_unprovably_stalled() + } +} + +impl SignerResult +where + LS: LegacySignerResponse, + TS: SignerResponse, +{ + pub fn unknown_signing_status(&self) -> bool { + let Ok(_) = self.information.parse() else { + return true; + }; + if let SignerStatus::Tested { .. } = &self.status { + return false; + } + true + } + + pub fn signing_available(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + result.signing_status.signing_available( + parsed_info.public_key, + self.dkg_epoch_id, + parsed_info.verification_key_share, + parsed_info.share_verified, + ) + } + + pub fn signing_provably_unavailable(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + + result + .signing_status + .signing_provably_unavailable(parsed_info.public_key, self.dkg_epoch_id) + } + + pub fn signing_unprovably_unavailable(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + + result.signing_status.signing_unprovably_unavailable( + parsed_info.public_key, + parsed_info.verification_key_share, + parsed_info.share_verified, + ) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub enum SignerStatus { + Unreachable, + ProvidedInvalidDetails, + Tested { + result: SignerTestResult, + }, +} + +impl SignerStatus { + pub fn with_details( + self, + information: impl Into, + dkg_epoch_id: u64, + ) -> SignerResult { + SignerResult { + dkg_epoch_id, + status: self, + information: information.into(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +pub struct SignerTestResult { + pub reported_version: String, + pub signing_status: Status, + pub local_chain_status: Status, +} diff --git a/common/ecash-signer-check/Cargo.toml b/common/ecash-signer-check/Cargo.toml new file mode 100644 index 0000000000..be22872ad8 --- /dev/null +++ b/common/ecash-signer-check/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nym-ecash-signer-check" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +futures = { workspace = true } +thiserror = { workspace = true } +semver = { workspace = true } +tokio = { workspace = true, features = ["time"] } +tracing = { workspace = true } +url = { workspace = true } + + +nym-validator-client = { path = "../client-libs/validator-client" } +nym-network-defaults = { path = "../network-defaults" } +nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" } + +[lints] +workspace = true diff --git a/common/ecash-signer-check/src/client_check.rs b/common/ecash-signer-check/src/client_check.rs new file mode 100644 index 0000000000..b5c367794d --- /dev/null +++ b/common/ecash-signer-check/src/client_check.rs @@ -0,0 +1,225 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{LocalChainStatus, SigningStatus, TypedSignerResult}; +use nym_ecash_signer_check_types::dealer_information::RawDealerInformation; +use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::BinaryBuildInformationOwned; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ + ContractVKShare, DealerDetails, +}; +use nym_validator_client::NymApiClient; +use std::time::Duration; +use tracing::{error, warn}; +use url::Url; + +pub(crate) mod chain_status { + + // Dorina + pub(crate) const MINIMUM_VERSION_LEGACY: semver::Version = semver::Version::new(1, 1, 51); + + // Gruyere + pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64); +} + +pub(crate) mod signing_status { + // Magura (possibly earlier) + pub(crate) const MINIMUM_LEGACY_VERSION: semver::Version = semver::Version::new(1, 1, 46); + + // Gruyere + pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64); +} + +struct ClientUnderTest { + api_client: NymApiClient, + build_info: Option, +} + +impl ClientUnderTest { + pub(crate) fn new(api_url: &Url) -> Self { + ClientUnderTest { + api_client: NymApiClient::new(api_url.clone()), + build_info: None, + } + } + + pub(crate) async fn try_retrieve_build_information(&mut self) -> bool { + match tokio::time::timeout( + Duration::from_secs(5), + self.api_client.nym_api.build_information(), + ) + .await + { + Ok(Ok(build_information)) => { + self.build_info = Some(build_information); + true + } + Ok(Err(err)) => { + warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.api_url()); + false + } + Err(_timeout) => { + warn!( + "{}: timed out while attempting to retrieve build information", + self.api_client.api_url() + ); + false + } + } + } + + pub(crate) fn version(&self) -> Option { + self.build_info.as_ref().and_then(|build_info| { + build_info + .build_version + .parse() + .inspect_err(|err| { + error!( + "ecash signer '{}' reports invalid version {}: {err}", + self.api_client.api_url(), + build_info.build_version + ) + }) + .ok() + }) + } + + pub(crate) fn supports_legacy_signing_status_query(&self) -> bool { + let Some(version) = self.version() else { + return false; + }; + version >= signing_status::MINIMUM_LEGACY_VERSION + } + + pub(crate) fn supports_signing_status_query(&self) -> bool { + let Some(version) = self.version() else { + return false; + }; + version >= signing_status::MINIMUM_VERSION + } + + pub(crate) fn supports_chain_status_query(&self) -> bool { + let Some(version) = self.version() else { + return false; + }; + version >= chain_status::MINIMUM_VERSION + } + + pub(crate) fn supports_legacy_chain_status_query(&self) -> bool { + let Some(version) = self.version() else { + return false; + }; + version >= chain_status::MINIMUM_VERSION_LEGACY + } + + pub(crate) async fn check_local_chain(&self) -> LocalChainStatus { + // check if it at least supports legacy query + if !self.supports_legacy_chain_status_query() { + return LocalChainStatus::Outdated; + } + + // check if it supports the current query + if self.supports_chain_status_query() { + return match self.api_client.nym_api.get_chain_blocks_status().await { + Ok(status) => LocalChainStatus::Reachable { + response: Box::new(status), + }, + Err(err) => { + warn!( + "{}: failed to retrieve local chain status: {err}", + self.api_client.api_url() + ); + LocalChainStatus::Unreachable + } + }; + } + + // fallback to the legacy query + match self.api_client.nym_api.get_chain_status().await { + Ok(status) => LocalChainStatus::ReachableLegacy { + response: Box::new(status), + }, + Err(err) => { + warn!( + "{}: failed to retrieve [legacy] local chain status: {err}", + self.api_client.api_url() + ); + LocalChainStatus::Unreachable + } + } + } + + pub(crate) async fn check_signing_status(&self) -> SigningStatus { + // check if it at least supports legacy query + if !self.supports_legacy_signing_status_query() { + return SigningStatus::Outdated; + } + + // check if it supports the current query + if self.supports_signing_status_query() { + return match self.api_client.nym_api.get_signer_status().await { + Ok(response) => SigningStatus::Reachable { + response: Box::new(response), + }, + Err(err) => { + warn!( + "{}: failed to retrieve signer chain status: {err}", + self.api_client.api_url() + ); + SigningStatus::Unreachable + } + }; + } + + // fallback to the legacy query + match self.api_client.nym_api.get_signer_information().await { + Ok(status) => SigningStatus::ReachableLegacy { + response: Box::new(status), + }, + Err(err) => { + warn!( + "{}: failed to retrieve [legacy] signer chain status: {err}", + self.api_client.api_url() + ); + // NOTE: this might equally mean the signing is disabled + SigningStatus::Unreachable + } + } + } +} + +pub(crate) async fn check_client( + dealer_details: DealerDetails, + dkg_epoch: u64, + contract_share: Option<&ContractVKShare>, +) -> TypedSignerResult { + let dealer_information = RawDealerInformation::new(&dealer_details, contract_share); + + // 7. attempt to construct client instances out of them + let Ok(parsed_information) = dealer_information.parse() else { + return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch); + }; + + let mut client = ClientUnderTest::new(&parsed_information.announce_address); + + // 8. check basic connection status - can you retrieve build information? + if !client.try_retrieve_build_information().await { + return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch); + } + + // 9. check perceived chain status + let local_chain_status = client.check_local_chain().await; + + // 10. check signer status + let signing_status = client.check_signing_status().await; + + SignerStatus::Tested { + result: SignerTestResult { + reported_version: client.version().map(|v| v.to_string()).unwrap_or_default(), + signing_status, + local_chain_status, + }, + } + .with_details(dealer_information, dkg_epoch) +} diff --git a/common/ecash-signer-check/src/dealer_information.rs b/common/ecash-signer-check/src/dealer_information.rs new file mode 100644 index 0000000000..73f13cc489 --- /dev/null +++ b/common/ecash-signer-check/src/dealer_information.rs @@ -0,0 +1,80 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::SignerCheckError; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ + ContractVKShare, DealerDetails, VerificationKeyShare, +}; +use url::Url; + +#[derive(Debug)] +pub struct RawDealerInformation { + pub announce_address: String, + pub owner_address: String, + pub node_index: u64, + pub public_key: String, + pub verification_key_share: Option, + pub share_verified: bool, +} + +impl RawDealerInformation { + pub fn new( + dealer_details: &DealerDetails, + contract_share: Option<&ContractVKShare>, + ) -> RawDealerInformation { + RawDealerInformation { + announce_address: dealer_details.announce_address.clone(), + owner_address: dealer_details.address.to_string(), + node_index: dealer_details.assigned_index, + public_key: dealer_details.ed25519_identity.clone(), + verification_key_share: contract_share.map(|s| s.share.clone()), + share_verified: contract_share.map(|s| s.verified).unwrap_or(false), + } + } + + pub fn parse(&self) -> Result { + Ok(DealerInformation { + announce_address: self.announce_address.parse().map_err(|source| { + SignerCheckError::InvalidDealerAddress { + dealer_url: self.announce_address.clone(), + source, + } + })?, + owner_address: self.owner_address.clone(), + node_index: self.node_index, + public_key: self.announce_address.parse().map_err(|source| { + SignerCheckError::InvalidDealerPubkey { + dealer_url: self.announce_address.clone(), + source, + } + })?, + verification_key_share: self.verification_key_share.clone(), + share_verified: self.share_verified, + }) + } +} + +#[derive(Debug)] +pub struct DealerInformation { + pub announce_address: Url, + pub owner_address: String, + pub node_index: u64, + pub public_key: ed25519::PublicKey, + // no need to parse it into the full type as it doesn't get us anything + pub verification_key_share: Option, + pub share_verified: bool, +} + +impl From for RawDealerInformation { + fn from(d: DealerInformation) -> Self { + RawDealerInformation { + announce_address: d.announce_address.to_string(), + owner_address: d.owner_address, + node_index: d.node_index, + public_key: d.public_key.to_base58_string(), + verification_key_share: d.verification_key_share, + share_verified: d.share_verified, + } + } +} diff --git a/common/ecash-signer-check/src/error.rs b/common/ecash-signer-check/src/error.rs new file mode 100644 index 0000000000..bb7c3c719b --- /dev/null +++ b/common/ecash-signer-check/src/error.rs @@ -0,0 +1,24 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_validator_client::nyxd::error::NyxdError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SignerCheckError { + #[error("failed to connect to nyxd chain due to invalid connection details: {source}")] + InvalidNyxdConnectionDetails { source: NyxdError }, + + #[error("failed to query the DKG contract: {source}")] + DKGContractQueryFailure { source: NyxdError }, +} + +impl SignerCheckError { + pub fn invalid_nyxd_connection_details(source: NyxdError) -> Self { + Self::InvalidNyxdConnectionDetails { source } + } + + pub fn dkg_contract_query_failure(source: NyxdError) -> Self { + Self::DKGContractQueryFailure { source } + } +} diff --git a/common/ecash-signer-check/src/lib.rs b/common/ecash-signer-check/src/lib.rs new file mode 100644 index 0000000000..c7ddb07113 --- /dev/null +++ b/common/ecash-signer-check/src/lib.rs @@ -0,0 +1,127 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client_check::check_client; +use futures::stream::{FuturesUnordered, StreamExt}; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; +use nym_validator_client::QueryHttpRpcNyxdClient; +use std::collections::HashMap; +use url::Url; + +pub use error::SignerCheckError; +use nym_ecash_signer_check_types::status::{SignerResult, Status}; +use nym_validator_client::ecash::models::EcashSignerStatusResponse; +use nym_validator_client::models::{ + ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse, +}; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ + ContractVKShare, DealerDetails, Epoch, +}; + +mod client_check; +pub mod error; + +pub type TypedSignerResult = SignerResult< + SignerInformationResponse, + EcashSignerStatusResponse, + ChainStatusResponse, + ChainBlocksStatusResponse, +>; +pub type LocalChainStatus = Status; +pub type SigningStatus = Status; + +pub struct SignersTestResult { + pub threshold: Option, + pub results: Vec, +} + +pub async fn check_signers( + rpc_endpoint: Url, + // details such as denoms, prefixes, etc. + network_details: NymNetworkDetails, +) -> Result { + // 1. create nyx client instance + let client = QueryHttpRpcNyxdClient::connect_with_network_details( + rpc_endpoint.as_str(), + network_details, + ) + .map_err(SignerCheckError::invalid_nyxd_connection_details)?; + + check_signers_with_client(&client).await +} + +pub struct DkgDetails { + pub dkg_epoch: Epoch, + pub threshold: Option, + pub network_dealers: Vec, + pub submitted_shared: HashMap, +} + +pub async fn check_signers_with_client(client: &C) -> Result +where + C: DkgQueryClient + Sync, +{ + let dkg_details = dkg_details_with_client(client).await?; + check_known_dealers(dkg_details).await +} + +pub async fn dkg_details_with_client(client: &C) -> Result +where + C: DkgQueryClient + Sync, +{ + // 2. retrieve current dkg epoch + let dkg_epoch = client + .get_current_epoch() + .await + .map_err(SignerCheckError::dkg_contract_query_failure)?; + + // 3. retrieve the dkg threshold as reference point + let threshold = client + .get_epoch_threshold(dkg_epoch.epoch_id) + .await + .map_err(SignerCheckError::dkg_contract_query_failure)?; + + // 4. retrieve information on current DKG dealers (i.e. eligible signers) + let dealers = client + .get_all_current_dealers() + .await + .map_err(SignerCheckError::dkg_contract_query_failure)?; + + // 5. retrieve their published keys (if available) + let shares: HashMap<_, _> = client + .get_all_verification_key_shares(dkg_epoch.epoch_id) + .await + .map_err(SignerCheckError::dkg_contract_query_failure)? + .into_iter() + .map(|share| (share.node_index, share)) + .collect(); + + Ok(DkgDetails { + dkg_epoch, + threshold, + network_dealers: dealers, + submitted_shared: shares, + }) +} + +pub async fn check_known_dealers( + dkg_details: DkgDetails, +) -> Result { + // 6. for each dealer attempt to perform the checks + let results = dkg_details + .network_dealers + .into_iter() + .map(|d| { + let share = dkg_details.submitted_shared.get(&d.assigned_index); + check_client(d, dkg_details.dkg_epoch.epoch_id, share) + }) + .collect::>() + .collect::>() + .await; + + Ok(SignersTestResult { + threshold: dkg_details.threshold, + results, + }) +} diff --git a/common/ecash-signer-check/src/status.rs b/common/ecash-signer-check/src/status.rs new file mode 100644 index 0000000000..a75a86448c --- /dev/null +++ b/common/ecash-signer-check/src/status.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::chain_status::LocalChainStatus; +use crate::dealer_information::RawDealerInformation; +use crate::signing_status::SigningStatus; +use std::time::Duration; + +pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60); + +#[derive(Debug)] +pub struct SignerResult { + pub dkg_epoch_id: u64, + pub information: RawDealerInformation, + pub status: SignerStatus, +} + +impl SignerResult { + pub fn chain_available(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + result.local_chain_status.available(parsed_info.public_key) + } + + pub fn signer_available(&self) -> bool { + let Ok(parsed_info) = self.information.parse() else { + return false; + }; + let SignerStatus::Tested { result } = &self.status else { + return false; + }; + + result.signing_status.available( + parsed_info.public_key, + self.dkg_epoch_id, + parsed_info.verification_key_share, + parsed_info.share_verified, + ) + } +} + +#[derive(Debug)] +pub enum SignerStatus { + Unreachable, + ProvidedInvalidDetails, + Tested { result: SignerTestResult }, +} + +impl SignerStatus { + pub fn with_details( + self, + information: impl Into, + dkg_epoch_id: u64, + ) -> SignerResult { + SignerResult { + dkg_epoch_id, + status: self, + information: information.into(), + } + } +} + +#[derive(Debug)] +pub struct SignerTestResult { + pub reported_version: String, + pub signing_status: SigningStatus, + pub local_chain_status: LocalChainStatus, +} diff --git a/common/execute/Cargo.toml b/common/execute/Cargo.toml deleted file mode 100644 index 613d5ebb5f..0000000000 --- a/common/execute/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "nym-execute" -version = "0.1.0" -edition = "2021" -license.workspace = true - -[lib] -proc-macro = true - -[dependencies] -syn = { workspace = true, features = ["full"] } -quote = { workspace = true } diff --git a/common/execute/src/lib.rs b/common/execute/src/lib.rs deleted file mode 100644 index c05a2e8748..0000000000 --- a/common/execute/src/lib.rs +++ /dev/null @@ -1,110 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - parse_macro_input, Block, ExprMethodCall, FnArg, Ident, ItemFn, LitStr, ReturnType, Token, - VisPublic, Visibility, -}; - -#[proc_macro_attribute] -pub fn execute(attr: TokenStream, item: TokenStream) -> TokenStream { - let f = parse_macro_input!(item as ItemFn); - let target = parse_macro_input!(attr as LitStr).value(); - - let cl = if target == "mixnet" { - quote! {self.mixnet_contract_address()} - } else if target == "vesting" { - quote! {self.vesting_contract_address()} - } else { - panic!("Only `mixnet` and `vesting` targets are supported!") - }; - let cl = proc_macro::TokenStream::from(cl); - let cl = parse_macro_input!(cl as ExprMethodCall); - - let orig_f = f.clone(); - let mut execute_f = f.clone(); - let mut simulate_f = f.clone(); - let name = f.sig.ident; - let name_str = name.to_string(); - let call_args = f.sig.inputs.into_iter().filter_map(|arg| match arg { - FnArg::Receiver(_) => None, - FnArg::Typed(arg) => Some(arg.pat), - }); - let execute_args = call_args.clone(); - let simulate_args = call_args; - - execute_f.sig.asyncness = Some(Token![async](execute_f.sig.ident.span())); - simulate_f.sig.asyncness = Some(Token![async](simulate_f.sig.ident.span())); - - execute_f.vis = Visibility::Public(VisPublic { - pub_token: Token![pub](execute_f.sig.ident.span()), - }); - simulate_f.vis = Visibility::Public(VisPublic { - pub_token: Token![pub](simulate_f.sig.ident.span()), - }); - - execute_f.sig.ident = Ident::new( - &format!("execute{}", execute_f.sig.ident), - execute_f.sig.ident.span(), - ); - - simulate_f.sig.ident = Ident::new( - &format!("simulate{}", simulate_f.sig.ident), - simulate_f.sig.ident.span(), - ); - - let execute_output = quote! { - -> Result - }; - let o_ts = proc_macro::TokenStream::from(execute_output); - execute_f.sig.output = parse_macro_input!(o_ts as ReturnType); - - let simulate_output = quote! { - -> Result - }; - let o_ts = proc_macro::TokenStream::from(simulate_output); - simulate_f.sig.output = parse_macro_input!(o_ts as ReturnType); - - let simulate_block = quote! { - { - let (msg, _fee) = self.#name(#(#simulate_args),*); - let msg = self.wrap_contract_execute_message( - #cl, - &msg, - vec![], - )?; - - self.simulate(vec![msg]).await - } - }; - - let ts = proc_macro::TokenStream::from(simulate_block); - simulate_f.block = Box::new(parse_macro_input!(ts as Block)); - - let execute_block = quote! { - { - let (req, fee) = self.#name(#(#execute_args),*); - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - self.client - .execute( - self.address(), - #cl, - &req, - fee, - #name_str, - vec![], - ) - .await - } - }; - - let ts = proc_macro::TokenStream::from(execute_block); - execute_f.block = Box::new(parse_macro_input!(ts as Block)); - - let out = quote! { - #orig_f - #execute_f - #simulate_f - }; - - out.into() -} diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 3448184eae..83042ead14 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -28,6 +28,7 @@ nym-crypto = { path = "../crypto", features = ["aead", "hashing"] } nym-pemstore = { path = "../pemstore" } nym-sphinx = { path = "../nymsphinx" } nym-serde-helpers = { path = "../serde-helpers", features = ["base64"] } +nym-statistics-common = { path = "../statistics" } nym-task = { path = "../task" } nym-credentials = { path = "../credentials" } @@ -46,4 +47,7 @@ workspace = true default-features = false [dev-dependencies] +anyhow = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests +nym-test-utils = { path = "../test-utils" } +tokio = { workspace = true, features = ["full"] } diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index 0d300bc4a4..bdb9a30a0f 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,7 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION; +pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -28,10 +28,12 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION; // 2 - changes to client credentials structure // 3 - change to AES-GCM-SIV and non-zero IVs // 4 - introduction of v2 authentication protocol to prevent reply attacks +// 5 - add key rotation information to the serialised mix packet pub const INITIAL_PROTOCOL_VERSION: u8 = 1; pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; +pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? @@ -40,6 +42,7 @@ pub type LegacyGatewayMacSize = bool; fn supports_authenticate_v2(&self) -> bool; + fn supports_key_rotation_packet(&self) -> bool; } impl GatewayProtocolVersionExt for Option { @@ -52,4 +55,9 @@ impl GatewayProtocolVersionExt for Option { let Some(protocol) = *self else { return false }; protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION } + + fn supports_key_rotation_packet(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION + } } diff --git a/common/gateway-requests/src/models.rs b/common/gateway-requests/src/models.rs index 32b2a9f812..f7e79d7685 100644 --- a/common/gateway-requests/src/models.rs +++ b/common/gateway-requests/src/models.rs @@ -89,7 +89,7 @@ mod tests { .unwrap(); let blind_sig = issue( keypair.secret_key(), - sig_req.ecash_pub_key.clone(), + sig_req.ecash_pub_key, &sig_req.withdrawal_request, expiration_date.ecash_unix_timestamp(), issuance.ticketbook_type().encode(), diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 53373f5c30..f6ef8a4de4 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -109,3 +109,85 @@ GATEWAY -> CLIENT DONE(status) */ + +#[cfg(test)] +mod tests { + use super::*; + use crate::ClientControlRequest; + use futures::StreamExt; + use nym_test_utils::helpers::u64_seeded_rng; + use nym_test_utils::mocks::stream_sink::mock_streams; + use nym_test_utils::traits::{Leak, Timeboxed, TimeboxedSpawnable}; + use tokio::join; + use tungstenite::Message; + + #[tokio::test] + async fn basic_handshake() -> anyhow::Result<()> { + use anyhow::Context as _; + + // solve the lifetime issue by just leaking the contents of the boxes + // which is perfectly fine in test + let client_rng = u64_seeded_rng(42).leak(); + let gateway_rng = u64_seeded_rng(69).leak(); + + let client_keys = ed25519::KeyPair::new(client_rng).leak(); + let gateway_keys = ed25519::KeyPair::new(gateway_rng).leak(); + + let (client_ws, gateway_ws) = mock_streams::(); + + // we need streams that return Result + let client_ws = client_ws.map(Ok); + let gateway_ws = gateway_ws.map(Ok); + + let client_ws = client_ws.leak(); + let gateway_ws = gateway_ws.leak(); + + let handshake_client = client_handshake( + client_rng, + client_ws, + client_keys, + *gateway_keys.public_key(), + false, + true, + TaskClient::dummy(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version: _, + data, + } = (gateway_ws.next()) + .timeboxed() + .await + .context("timeout")? + .context("no message!")?? + .into_text()? + .parse::()? + else { + panic!("bad message") + }; + + let init_msg = data; + + let handshake_gateway = gateway_handshake( + gateway_rng, + gateway_ws, + gateway_keys, + init_msg, + TaskClient::dummy(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_key = client???; + let gateway_key = gateway???; + + // ensure the created keys are the same + assert_eq!(client_key, gateway_key); + + Ok(()) + } +} diff --git a/common/gateway-requests/src/types/binary_request.rs b/common/gateway-requests/src/types/binary_request.rs index 17eab6c9cb..b52ee819cd 100644 --- a/common/gateway-requests/src/types/binary_request.rs +++ b/common/gateway-requests/src/types/binary_request.rs @@ -11,6 +11,9 @@ use tungstenite::Message; #[non_exhaustive] pub enum BinaryRequest { ForwardSphinx { packet: MixPacket }, + + // identical to `ForwardSphinx`, but also contains information about sphinx key rotation used + ForwardSphinxV2 { packet: MixPacket }, } #[repr(u8)] @@ -18,6 +21,9 @@ pub enum BinaryRequest { #[non_exhaustive] pub enum BinaryRequestKind { ForwardSphinx = 1, + + // identical to `ForwardSphinx`, but also contains information about sphinx key rotation used + ForwardSphinxV2 = 2, } // Right now the only valid `BinaryRequest` is a request to forward a sphinx packet. @@ -29,6 +35,7 @@ impl BinaryRequest { pub fn kind(&self) -> BinaryRequestKind { match self { BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx, + BinaryRequest::ForwardSphinxV2 { .. } => BinaryRequestKind::ForwardSphinxV2, } } @@ -38,9 +45,13 @@ impl BinaryRequest { ) -> Result { match kind { BinaryRequestKind::ForwardSphinx => { - let packet = MixPacket::try_from_bytes(plaintext)?; + let packet = MixPacket::try_from_v1_bytes(plaintext)?; Ok(BinaryRequest::ForwardSphinx { packet }) } + BinaryRequestKind::ForwardSphinxV2 => { + let packet = MixPacket::try_from_v2_bytes(plaintext)?; + Ok(BinaryRequest::ForwardSphinxV2 { packet }) + } } } @@ -58,7 +69,8 @@ impl BinaryRequest { let kind = self.kind(); let plaintext = match self { - BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?, + BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?, + BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?, }; BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) @@ -70,7 +82,9 @@ impl BinaryRequest { ) -> Result { // all variants are currently encrypted let blob = match self { - BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?, + BinaryRequest::ForwardSphinx { .. } | BinaryRequest::ForwardSphinxV2 { .. } => { + self.into_encrypted_tagged_bytes(shared_key)? + } }; Ok(Message::Binary(blob)) diff --git a/common/gateway-requests/src/types/text_request/mod.rs b/common/gateway-requests/src/types/text_request/mod.rs index 7182dd834e..342bc9adfd 100644 --- a/common/gateway-requests/src/types/text_request/mod.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -11,6 +11,7 @@ use crate::{ use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::ed25519; use nym_sphinx::DestinationAddressBytes; +use nym_statistics_common::types::SessionType; use serde::{Deserialize, Serialize}; use std::str::FromStr; use tungstenite::Message; @@ -29,6 +30,9 @@ pub enum ClientRequest { client: bool, stats: bool, }, + RememberMe { + session_type: SessionType, + }, } impl ClientRequest { diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index 5c6ce668b5..c3418649cf 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -12,6 +12,7 @@ use tungstenite::Message; pub enum SensitiveServerResponse { KeyUpgradeAck {}, ForgetMeAck {}, + RememberMeAck {}, } impl SensitiveServerResponse { @@ -111,6 +112,10 @@ impl ServerResponse { _ => false, } } + + pub fn is_send(&self) -> bool { + matches!(self, ServerResponse::Send { .. }) + } } impl From for Message { diff --git a/common/gateway-stats-storage/Cargo.toml b/common/gateway-stats-storage/Cargo.toml index 0a36798e52..e958e25526 100644 --- a/common/gateway-stats-storage/Cargo.toml +++ b/common/gateway-stats-storage/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true [dependencies] sqlx = { workspace = true, features = [ @@ -22,12 +23,12 @@ thiserror = { workspace = true } tracing = { workspace = true } nym-sphinx = { path = "../nymsphinx" } -nym-credentials-interface = { path = "../credentials-interface" } nym-node-metrics = { path = "../../nym-node/nym-node-metrics" } nym-statistics-common = { path = "../statistics" } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", diff --git a/common/gateway-stats-storage/build.rs b/common/gateway-stats-storage/build.rs index 166349c67a..6c665678e6 100644 --- a/common/gateway-stats-storage/build.rs +++ b/common/gateway-stats-storage/build.rs @@ -1,22 +1,29 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/gateway-stats-example.sqlite", out_dir); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; + let database_path = format!("{out_dir}/gateway-stats-example.sqlite"); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -25,4 +32,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/gateway-stats-storage/migrations/20250505120000_add_remember_column.sql b/common/gateway-stats-storage/migrations/20250505120000_add_remember_column.sql new file mode 100644 index 0000000000..02cd2a7020 --- /dev/null +++ b/common/gateway-stats-storage/migrations/20250505120000_add_remember_column.sql @@ -0,0 +1,7 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE sessions_active + ADD COLUMN remember INTEGER NOT NULL default 0; \ No newline at end of file diff --git a/common/gateway-stats-storage/src/lib.rs b/common/gateway-stats-storage/src/lib.rs index 5bfc658a93..d29de3a1b0 100644 --- a/common/gateway-stats-storage/src/lib.rs +++ b/common/gateway-stats-storage/src/lib.rs @@ -3,8 +3,9 @@ use error::StatsStorageError; use models::StoredFinishedSession; -use nym_node_metrics::entry::{ActiveSession, FinishedSession, SessionType}; +use nym_node_metrics::entry::{ActiveSession, FinishedSession}; use nym_sphinx::DestinationAddressBytes; +use nym_statistics_common::types::SessionType; use sessions::SessionManager; use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, @@ -32,8 +33,8 @@ impl PersistentStatsStorage { /// * `database_path`: path to the database. pub async fn init + Send>(database_path: P) -> Result { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); // TODO: we can inject here more stuff based on our gateway global config @@ -147,6 +148,16 @@ impl PersistentStatsStorage { .await?) } + pub async fn remember_active_session( + &self, + client_address: DestinationAddressBytes, + ) -> Result<(), StatsStorageError> { + Ok(self + .session_manager + .remember_active_session(client_address.as_base58_string()) + .await?) + } + pub async fn update_active_session_type( &self, client_address: DestinationAddressBytes, @@ -182,7 +193,7 @@ impl PersistentStatsStorage { pub async fn get_started_sessions_count( &self, start_date: Date, - ) -> Result { + ) -> Result { Ok(self .session_manager .get_started_sessions_count(start_date) diff --git a/common/gateway-stats-storage/src/models.rs b/common/gateway-stats-storage/src/models.rs index 5553875fd4..a8f5cec193 100644 --- a/common/gateway-stats-storage/src/models.rs +++ b/common/gateway-stats-storage/src/models.rs @@ -1,12 +1,11 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_node_metrics::entry::{ActiveSession, FinishedSession, SessionType}; +use nym_node_metrics::entry::{ActiveSession, FinishedSession}; +use nym_statistics_common::types::SessionType; use sqlx::prelude::FromRow; use time::OffsetDateTime; -pub use nym_credentials_interface::TicketType; - #[derive(FromRow)] pub struct StoredFinishedSession { duration_ms: i64, @@ -22,25 +21,11 @@ impl From for FinishedSession { } } -pub trait ToSessionType { - fn to_session_type(&self) -> SessionType; -} - -impl ToSessionType for TicketType { - fn to_session_type(&self) -> SessionType { - match self { - TicketType::V1MixnetEntry => SessionType::Mixnet, - TicketType::V1MixnetExit => SessionType::Mixnet, - TicketType::V1WireguardEntry => SessionType::Vpn, - TicketType::V1WireguardExit => SessionType::Vpn, - } - } -} - #[derive(FromRow)] pub(crate) struct StoredActiveSession { start_time: OffsetDateTime, typ: String, + remember: u8, } impl From for ActiveSession { @@ -48,6 +33,7 @@ impl From for ActiveSession { ActiveSession { start: value.start_time, typ: SessionType::from_string(&value.typ), + remember: value.remember != 0, } } } diff --git a/common/gateway-stats-storage/src/sessions.rs b/common/gateway-stats-storage/src/sessions.rs index a919696967..bef28730cc 100644 --- a/common/gateway-stats-storage/src/sessions.rs +++ b/common/gateway-stats-storage/src/sessions.rs @@ -107,7 +107,7 @@ impl SessionManager { typ: String, ) -> Result<()> { sqlx::query!( - "INSERT INTO sessions_active (client_address, start_time, typ) VALUES (?, ?, ?)", + "INSERT INTO sessions_active (client_address, start_time, typ, remember) VALUES (?, ?, ?, 0)", client_address_b58, start_time, typ @@ -117,6 +117,16 @@ impl SessionManager { Ok(()) } + pub(crate) async fn remember_active_session(&self, client_address_b58: String) -> Result<()> { + sqlx::query!( + "UPDATE sessions_active SET remember = 1 WHERE client_address = ?", + client_address_b58, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + pub(crate) async fn update_active_session_type( &self, client_address_b58: String, @@ -136,19 +146,21 @@ impl SessionManager { &self, client_address_b58: String, ) -> Result> { - sqlx::query_as("SELECT start_time, typ FROM sessions_active WHERE client_address = ?") - .bind(client_address_b58) - .fetch_optional(&self.connection_pool) - .await + sqlx::query_as( + "SELECT start_time, typ, remember FROM sessions_active WHERE client_address = ?", + ) + .bind(client_address_b58) + .fetch_optional(&self.connection_pool) + .await } pub(crate) async fn get_all_active_sessions(&self) -> Result> { - sqlx::query_as("SELECT start_time, typ FROM sessions_active") + sqlx::query_as("SELECT start_time, typ, remember FROM sessions_active") .fetch_all(&self.connection_pool) .await } - pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result { + pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result { Ok(sqlx::query!( "SELECT COUNT(*) as count FROM sessions_active WHERE date(start_time) = ?", start_date diff --git a/common/gateway-storage/.sqlx/query-03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24.json b/common/gateway-storage/.sqlx/query-03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24.json deleted file mode 100644 index 32d90ac983..0000000000 --- a/common/gateway-storage/.sqlx/query-03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\" \n FROM message_store \n WHERE client_address_bs58 = ? AND id > ?\n ORDER BY id ASC\n LIMIT ?;\n ", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "client_address_bs58!", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "content!", - "ordinal": 2, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24" -} diff --git a/common/gateway-storage/.sqlx/query-071fbde5277c0806ed47fa15a9c6288609379049828a94008f854b3daeed21d1.json b/common/gateway-storage/.sqlx/query-071fbde5277c0806ed47fa15a9c6288609379049828a94008f854b3daeed21d1.json new file mode 100644 index 0000000000..96301d56f8 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-071fbde5277c0806ed47fa15a9c6288609379049828a94008f854b3daeed21d1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM message_store WHERE timestamp < ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "071fbde5277c0806ed47fa15a9c6288609379049828a94008f854b3daeed21d1" +} diff --git a/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json b/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json index 875f8594b4..6afb936993 100644 --- a/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json +++ b/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json @@ -16,7 +16,7 @@ { "name": "protocol_version", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "endpoint", @@ -31,17 +31,17 @@ { "name": "tx_bytes", "ordinal": 5, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "rx_bytes", "ordinal": 6, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "persistent_keepalive_interval", "ordinal": 7, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "allowed_ips", @@ -51,7 +51,7 @@ { "name": "client_id", "ordinal": 9, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json b/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json index e6da2236a2..a643bb4b65 100644 --- a/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json +++ b/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json @@ -6,7 +6,7 @@ { "name": "signer_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/.sqlx/query-451158af8f5602e30445986a8de22d2e065c4e9dce4e7463a875ca8c21ac97c1.json b/common/gateway-storage/.sqlx/query-451158af8f5602e30445986a8de22d2e065c4e9dce4e7463a875ca8c21ac97c1.json new file mode 100644 index 0000000000..55b5c1ff74 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-451158af8f5602e30445986a8de22d2e065c4e9dce4e7463a875ca8c21ac97c1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "451158af8f5602e30445986a8de22d2e065c4e9dce4e7463a875ca8c21ac97c1" +} diff --git a/common/gateway-storage/.sqlx/query-564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c.json b/common/gateway-storage/.sqlx/query-564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c.json deleted file mode 100644 index 308bbb2325..0000000000 --- a/common/gateway-storage/.sqlx/query-564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT * FROM shared_keys WHERE client_address_bs58 = ?", - "describe": { - "columns": [ - { - "name": "client_id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "client_address_bs58", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "derived_aes128_ctr_blake3_hmac_keys_bs58", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "derived_aes256_gcm_siv_key", - "ordinal": 3, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - true, - true - ] - }, - "hash": "564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c" -} diff --git a/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json b/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json index 73f500093c..490a56aeb7 100644 --- a/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json +++ b/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json @@ -6,7 +6,7 @@ { "name": "client_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json b/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json index 45fa5f4a5d..5f7cc9917a 100644 --- a/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json +++ b/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "client_type: ClientType", diff --git a/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json b/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json index ec245e2d27..94f882f60e 100644 --- a/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json +++ b/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json @@ -6,7 +6,7 @@ { "name": "ticket_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "data!", diff --git a/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json b/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json index d8931d6e6e..83f1ca9c30 100644 --- a/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json +++ b/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json @@ -6,14 +6,14 @@ { "name": "exists", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { "Right": 1 }, "nullable": [ - null + false ] }, "hash": "7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c" diff --git a/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json b/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json index e54a2f1b02..97ee76ccee 100644 --- a/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json +++ b/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json @@ -6,7 +6,7 @@ { "name": "signer_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json b/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json index 36e35bba32..754f7f206e 100644 --- a/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json +++ b/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json @@ -6,7 +6,7 @@ { "name": "available", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json b/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json index 8dbd639d40..f4d4a0289f 100644 --- a/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json +++ b/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json @@ -6,7 +6,7 @@ { "name": "ticket_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serial_number", diff --git a/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json b/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json index 4573da3fd8..80674be84e 100644 --- a/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json +++ b/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json @@ -6,7 +6,7 @@ { "name": "ticket_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serial_number", diff --git a/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json b/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json index bbd32fe422..86bccb8094 100644 --- a/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json +++ b/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json @@ -16,7 +16,7 @@ { "name": "protocol_version", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "endpoint", @@ -31,17 +31,17 @@ { "name": "tx_bytes", "ordinal": 5, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "rx_bytes", "ordinal": 6, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "persistent_keepalive_interval", "ordinal": 7, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "allowed_ips", @@ -51,7 +51,7 @@ { "name": "client_id", "ordinal": 9, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/.sqlx/query-e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d.json b/common/gateway-storage/.sqlx/query-e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d.json deleted file mode 100644 index c24cc93187..0000000000 --- a/common/gateway-storage/.sqlx/query-e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\"\n FROM message_store\n WHERE client_address_bs58 = ?\n ORDER BY id ASC\n LIMIT ?;\n ", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "client_address_bs58!", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "content!", - "ordinal": 2, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d" -} diff --git a/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json b/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json index b8efd9cfff..1cfcf9b205 100644 --- a/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json +++ b/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json @@ -6,7 +6,7 @@ { "name": "proposal_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/common/gateway-storage/Cargo.toml b/common/gateway-storage/Cargo.toml index 810697f258..c7878511be 100644 --- a/common/gateway-storage/Cargo.toml +++ b/common/gateway-storage/Cargo.toml @@ -7,10 +7,13 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true [dependencies] +async-trait = { workspace = true } bincode = { workspace = true } defguard_wireguard_rs = { workspace = true } +dyn-clone = { workspace = true } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", @@ -21,6 +24,7 @@ sqlx = { workspace = true, features = [ ] } time = { workspace = true } thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync"], optional = true } tracing = { workspace = true } nym-credentials-interface = { path = "../credentials-interface" } @@ -28,6 +32,7 @@ nym-gateway-requests = { path = "../gateway-requests" } nym-sphinx = { path = "../nymsphinx" } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", @@ -35,3 +40,7 @@ sqlx = { workspace = true, features = [ "macros", "migrate", ] } + +[features] +default = [] +mock = ["tokio"] \ No newline at end of file diff --git a/common/gateway-storage/build.rs b/common/gateway-storage/build.rs index 27d55fccd2..bfdaf4fca9 100644 --- a/common/gateway-storage/build.rs +++ b/common/gateway-storage/build.rs @@ -1,22 +1,29 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/gateway-example.sqlite", out_dir); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; + let database_path = format!("{out_dir}/gateway-example.sqlite"); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -25,4 +32,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/gateway-storage/migrations/20250605120000_trim_wireguard_peer_data.sql b/common/gateway-storage/migrations/20250605120000_trim_wireguard_peer_data.sql new file mode 100644 index 0000000000..cd627850bf --- /dev/null +++ b/common/gateway-storage/migrations/20250605120000_trim_wireguard_peer_data.sql @@ -0,0 +1,19 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +DELETE FROM wireguard_peer WHERE client_id IS NULL; + +CREATE TABLE wireguard_peer_new +( + public_key TEXT NOT NULL PRIMARY KEY UNIQUE, + allowed_ips BLOB NOT NULL, + client_id INTEGER REFERENCES clients(id) NOT NULL +); + +INSERT INTO wireguard_peer_new (public_key, allowed_ips, client_id) +SELECT public_key, allowed_ips, client_id FROM wireguard_peer; + +DROP TABLE wireguard_peer; +ALTER TABLE wireguard_peer_new RENAME TO wireguard_peer; \ No newline at end of file diff --git a/common/gateway-storage/src/clients.rs b/common/gateway-storage/src/clients.rs index 96b5f857eb..83365f8cfd 100644 --- a/common/gateway-storage/src/clients.rs +++ b/common/gateway-storage/src/clients.rs @@ -3,6 +3,8 @@ use std::str::FromStr; +use nym_credentials_interface::TicketType; + use crate::models::Client; #[derive(Debug, PartialEq, sqlx::Type)] @@ -15,6 +17,17 @@ pub enum ClientType { ExitWireguard, } +impl From for ClientType { + fn from(value: TicketType) -> Self { + match value { + TicketType::V1MixnetEntry => ClientType::EntryMixnet, + TicketType::V1MixnetExit => ClientType::ExitMixnet, + TicketType::V1WireguardEntry => ClientType::EntryWireguard, + TicketType::V1WireguardExit => ClientType::ExitWireguard, + } + } +} + impl FromStr for ClientType { type Err = &'static str; @@ -37,7 +50,7 @@ impl std::fmt::Display for ClientType { ClientType::EntryWireguard => "entry_wireguard", ClientType::ExitWireguard => "exit_wireguard", }; - write!(f, "{}", s) + write!(f, "{s}") } } diff --git a/common/gateway-storage/src/error.rs b/common/gateway-storage/src/error.rs index 272d86b557..2600451751 100644 --- a/common/gateway-storage/src/error.rs +++ b/common/gateway-storage/src/error.rs @@ -20,6 +20,18 @@ pub enum GatewayStorageError { #[error("the stored data associated with ticket {ticket_id} is malformed!")] MalformedStoredTicketData { ticket_id: i64 }, - #[error("Failed to convert from type of database: {0}")] - TypeConversion(String), + #[error("Failed to convert from type of database: {field_key}")] + TypeConversion { field_key: &'static str }, + + #[error("Serialization failure for {field_key}")] + Serialize { + field_key: &'static str, + source: bincode::Error, + }, + + #[error("Deserialization failure for {field_key}")] + Deserialize { + field_key: &'static str, + source: bincode::Error, + }, } diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 2d05d43fe7..e10fdc3c1e 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use async_trait::async_trait; use bandwidth::BandwidthManager; use clients::{ClientManager, ClientType}; use models::{ @@ -15,10 +16,10 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; -use std::path::Path; +use std::{path::Path, time::Duration}; use tickets::TicketStorageManager; use time::OffsetDateTime; -use tracing::{debug, error}; +use tracing::{debug, error, log::LevelFilter}; pub mod bandwidth; mod clients; @@ -27,11 +28,21 @@ mod inboxes; pub mod models; mod shared_keys; mod tickets; +pub mod traits; mod wireguard_peers; pub use error::GatewayStorageError; pub use inboxes::InboxManager; +use crate::traits::{BandwidthGatewayStorage, InboxGatewayStorage, SharedKeyGatewayStorage}; + +fn make_bincode_serializer() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} + // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub struct GatewayStorage { @@ -71,6 +82,21 @@ impl GatewayStorage { &self.wireguard_peer_manager } + pub async fn handle_forget_me( + &self, + client_address: DestinationAddressBytes, + ) -> Result<(), GatewayStorageError> { + let client_id = self.get_mixnet_client_id(client_address).await?; + self.inbox_manager() + .remove_messages_for_client(&client_address.as_base58_string()) + .await?; + self.bandwidth_manager().remove_client(client_id).await?; + self.shared_key_manager() + .remove_shared_keys(&client_address.as_base58_string()) + .await?; + Ok(()) + } + /// Initialises `PersistentStorage` using the provided path. /// /// # Arguments @@ -82,8 +108,8 @@ impl GatewayStorage { message_retrieval_limit: i64, ) -> Result { debug!( - "Attempting to connect to database {:?}", - database_path.as_ref().as_os_str() + "Attempting to connect to database {}", + database_path.as_ref().display() ); // TODO: we can inject here more stuff based on our gateway global config @@ -92,6 +118,7 @@ impl GatewayStorage { .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) + .log_slow_statements(LevelFilter::Warn, Duration::from_millis(250)) .filename(database_path) .create_if_missing(true) .disable_statement_logging(); @@ -123,8 +150,9 @@ impl GatewayStorage { } } -impl GatewayStorage { - pub async fn get_mixnet_client_id( +#[async_trait] +impl SharedKeyGatewayStorage for GatewayStorage { + async fn get_mixnet_client_id( &self, client_address: DestinationAddressBytes, ) -> Result { @@ -134,22 +162,7 @@ impl GatewayStorage { .await?) } - pub async fn handle_forget_me( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), GatewayStorageError> { - let client_id = self.get_mixnet_client_id(client_address).await?; - self.inbox_manager() - .remove_messages_for_client(&client_address.as_base58_string()) - .await?; - self.bandwidth_manager().remove_client(client_id).await?; - self.shared_key_manager() - .remove_shared_keys(&client_address.as_base58_string()) - .await?; - Ok(()) - } - - pub async fn insert_shared_keys( + async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, shared_keys: &SharedGatewayKey, @@ -178,7 +191,7 @@ impl GatewayStorage { Ok(client_id) } - pub async fn get_shared_keys( + async fn get_shared_keys( &self, client_address: DestinationAddressBytes, ) -> Result, GatewayStorageError> { @@ -190,7 +203,7 @@ impl GatewayStorage { } #[allow(dead_code)] - pub async fn remove_shared_keys( + async fn remove_shared_keys( &self, client_address: DestinationAddressBytes, ) -> Result<(), GatewayStorageError> { @@ -200,7 +213,7 @@ impl GatewayStorage { Ok(()) } - pub async fn update_last_used_authentication_timestamp( + async fn update_last_used_authentication_timestamp( &self, client_id: i64, last_used_authentication_timestamp: OffsetDateTime, @@ -214,12 +227,15 @@ impl GatewayStorage { Ok(()) } - pub async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError> { + async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError> { let client = self.client_manager.get_client(client_id).await?; Ok(client) } +} - pub async fn store_message( +#[async_trait] +impl InboxGatewayStorage for GatewayStorage { + async fn store_message( &self, client_address: DestinationAddressBytes, message: Vec, @@ -230,7 +246,7 @@ impl GatewayStorage { Ok(()) } - pub async fn retrieve_messages( + async fn retrieve_messages( &self, client_address: DestinationAddressBytes, start_after: Option, @@ -242,19 +258,22 @@ impl GatewayStorage { Ok(messages) } - pub async fn remove_messages(&self, ids: Vec) -> Result<(), GatewayStorageError> { + async fn remove_messages(&self, ids: Vec) -> Result<(), GatewayStorageError> { for id in ids { self.inbox_manager.remove_message(id).await?; } Ok(()) } +} - pub async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> { +#[async_trait] +impl BandwidthGatewayStorage for GatewayStorage { + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> { self.bandwidth_manager.insert_new_client(client_id).await?; Ok(()) } - pub async fn set_expiration( + async fn set_expiration( &self, client_id: i64, expiration: OffsetDateTime, @@ -265,12 +284,12 @@ impl GatewayStorage { Ok(()) } - pub async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> { + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> { self.bandwidth_manager.reset_bandwidth(client_id).await?; Ok(()) } - pub async fn get_available_bandwidth( + async fn get_available_bandwidth( &self, client_id: i64, ) -> Result, GatewayStorageError> { @@ -280,7 +299,7 @@ impl GatewayStorage { .await?) } - pub async fn increase_bandwidth( + async fn increase_bandwidth( &self, client_id: i64, amount: i64, @@ -291,7 +310,7 @@ impl GatewayStorage { .await?) } - pub async fn revoke_ticket_bandwidth( + async fn revoke_ticket_bandwidth( &self, ticket_id: i64, amount: i64, @@ -302,7 +321,7 @@ impl GatewayStorage { .await?) } - pub async fn decrease_bandwidth( + async fn decrease_bandwidth( &self, client_id: i64, amount: i64, @@ -313,7 +332,7 @@ impl GatewayStorage { .await?) } - pub async fn insert_epoch_signers( + async fn insert_epoch_signers( &self, epoch_id: i64, signer_ids: Vec, @@ -324,7 +343,7 @@ impl GatewayStorage { Ok(()) } - pub async fn insert_received_ticket( + async fn insert_received_ticket( &self, client_id: i64, received_at: OffsetDateTime, @@ -344,11 +363,11 @@ impl GatewayStorage { Ok(ticket_id) } - pub async fn contains_ticket(&self, serial_number: &[u8]) -> Result { + async fn contains_ticket(&self, serial_number: &[u8]) -> Result { Ok(self.ticket_manager.has_ticket_data(serial_number).await?) } - pub async fn insert_ticket_verification( + async fn insert_ticket_verification( &self, ticket_id: i64, signer_id: i64, @@ -361,7 +380,7 @@ impl GatewayStorage { Ok(()) } - pub async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { // set the ticket as rejected self.ticket_manager.set_rejected_ticket(ticket_id).await?; @@ -372,7 +391,7 @@ impl GatewayStorage { Ok(()) } - pub async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { // 1. insert into verified table self.ticket_manager .insert_verified_ticket(ticket_id) @@ -386,7 +405,7 @@ impl GatewayStorage { Ok(()) } - pub async fn remove_verified_ticket_binary_data( + async fn remove_verified_ticket_binary_data( &self, ticket_id: i64, ) -> Result<(), GatewayStorageError> { @@ -396,7 +415,7 @@ impl GatewayStorage { Ok(()) } - pub async fn get_all_verified_tickets_with_sn( + async fn get_all_verified_tickets_with_sn( &self, ) -> Result, GatewayStorageError> { Ok(self @@ -405,7 +424,7 @@ impl GatewayStorage { .await?) } - pub async fn get_all_proposed_tickets_with_sn( + async fn get_all_proposed_tickets_with_sn( &self, proposal_id: u32, ) -> Result, GatewayStorageError> { @@ -415,7 +434,7 @@ impl GatewayStorage { .await?) } - pub async fn insert_redemption_proposal( + async fn insert_redemption_proposal( &self, tickets: &[VerifiedTicket], proposal_id: u32, @@ -438,7 +457,7 @@ impl GatewayStorage { Ok(()) } - pub async fn clear_post_proposal_data( + async fn clear_post_proposal_data( &self, proposal_id: u32, resolved_at: OffsetDateTime, @@ -462,13 +481,11 @@ impl GatewayStorage { Ok(()) } - pub async fn latest_proposal(&self) -> Result, GatewayStorageError> { + async fn latest_proposal(&self) -> Result, GatewayStorageError> { Ok(self.ticket_manager.get_latest_redemption_proposal().await?) } - pub async fn get_all_unverified_tickets( - &self, - ) -> Result, GatewayStorageError> { + async fn get_all_unverified_tickets(&self) -> Result, GatewayStorageError> { self.ticket_manager .get_unverified_tickets() .await? @@ -477,21 +494,21 @@ impl GatewayStorage { .collect() } - pub async fn get_all_unresolved_proposals(&self) -> Result, GatewayStorageError> { + async fn get_all_unresolved_proposals(&self) -> Result, GatewayStorageError> { Ok(self .ticket_manager .get_all_unresolved_redemption_proposal_ids() .await?) } - pub async fn get_votes(&self, ticket_id: i64) -> Result, GatewayStorageError> { + async fn get_votes(&self, ticket_id: i64) -> Result, GatewayStorageError> { Ok(self .ticket_manager .get_verification_votes(ticket_id) .await?) } - pub async fn get_signers(&self, epoch_id: i64) -> Result, GatewayStorageError> { + async fn get_signers(&self, epoch_id: i64) -> Result, GatewayStorageError> { Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?) } @@ -500,34 +517,20 @@ impl GatewayStorage { /// # Arguments /// /// * `peer`: wireguard peer data to be stored - /// * `with_client_id`: if the peer should have a corresponding client_id - /// (created with entry wireguard ticket) or live without one (or with an - /// exiting one), for temporary backwards compatibility. - pub async fn insert_wireguard_peer( + async fn insert_wireguard_peer( &self, peer: &defguard_wireguard_rs::host::Peer, - with_client_id: bool, - ) -> Result, GatewayStorageError> { + client_type: ClientType, + ) -> Result { let client_id = match self .wireguard_peer_manager .retrieve_peer(&peer.public_key.to_string()) .await? { Some(peer) => peer.client_id, - _ => { - if with_client_id { - Some( - self.client_manager - .insert_client(ClientType::EntryWireguard) - .await?, - ) - } else { - None - } - } + None => self.client_manager.insert_client(client_type).await?, }; - let mut peer = WireguardPeer::from(peer.clone()); - peer.client_id = client_id; + let peer = WireguardPeer::from_defguard_peer(peer.clone(), client_id)?; self.wireguard_peer_manager.insert_peer(&peer).await?; Ok(client_id) } @@ -537,7 +540,7 @@ impl GatewayStorage { /// # Arguments /// /// * `peer_public_key`: wireguard public key of the peer to be retrieved. - pub async fn get_wireguard_peer( + async fn get_wireguard_peer( &self, peer_public_key: &str, ) -> Result, GatewayStorageError> { @@ -549,7 +552,7 @@ impl GatewayStorage { } /// Retrieves all wireguard peers. - pub async fn get_all_wireguard_peers(&self) -> Result, GatewayStorageError> { + async fn get_all_wireguard_peers(&self) -> Result, GatewayStorageError> { let ret = self.wireguard_peer_manager.retrieve_all_peers().await?; Ok(ret) } @@ -559,7 +562,7 @@ impl GatewayStorage { /// # Arguments /// /// * `peer_public_key`: wireguard public key of the peer to be removed. - pub async fn remove_wireguard_peer( + async fn remove_wireguard_peer( &self, peer_public_key: &str, ) -> Result<(), GatewayStorageError> { diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index 665b8fee47..32b12b80a5 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -1,7 +1,7 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::GatewayStorageError; +use crate::{error::GatewayStorageError, make_bincode_serializer}; use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData}; use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; use sqlx::FromRow; @@ -110,46 +110,24 @@ impl TryFrom for ClientTicket { #[derive(Debug, Clone, FromRow)] pub struct WireguardPeer { pub public_key: String, - pub preshared_key: Option, - pub protocol_version: Option, - pub endpoint: Option, - pub last_handshake: Option, - pub tx_bytes: i64, - pub rx_bytes: i64, - pub persistent_keepalive_interval: Option, pub allowed_ips: Vec, - pub client_id: Option, + pub client_id: i64, } -impl From for WireguardPeer { - fn from(value: defguard_wireguard_rs::host::Peer) -> Self { - WireguardPeer { +impl WireguardPeer { + pub fn from_defguard_peer( + value: defguard_wireguard_rs::host::Peer, + client_id: i64, + ) -> Result { + Ok(WireguardPeer { public_key: value.public_key.to_string(), - preshared_key: value.preshared_key.as_ref().map(|k| k.to_string()), - protocol_version: value.protocol_version.map(|v| v as i64), - endpoint: value.endpoint.map(|e| e.to_string()), - last_handshake: value.last_handshake.and_then(|t| { - if let Ok(d) = t.duration_since(std::time::UNIX_EPOCH) { - if let Ok(millis) = d.as_millis().try_into() { - sqlx::types::chrono::DateTime::from_timestamp_millis(millis) - .map(|d| d.naive_utc()) - } else { - None - } - } else { - None - } - }), - tx_bytes: value.tx_bytes as i64, - rx_bytes: value.rx_bytes as i64, - persistent_keepalive_interval: value.persistent_keepalive_interval.map(|v| v as i64), - allowed_ips: bincode::Options::serialize( - bincode::DefaultOptions::new(), - &value.allowed_ips, - ) - .unwrap_or_default(), - client_id: None, - } + allowed_ips: bincode::Options::serialize(make_bincode_serializer(), &value.allowed_ips) + .map_err(|source| crate::error::GatewayStorageError::Serialize { + field_key: "allowed_ips", + source, + })?, + client_id, + }) } } @@ -158,57 +136,20 @@ impl TryFrom for defguard_wireguard_rs::host::Peer { fn try_from(value: WireguardPeer) -> Result { Ok(Self { - public_key: value - .public_key - .as_str() - .try_into() - .map_err(|e| Self::Error::TypeConversion(format!("public key {e}")))?, - preshared_key: value - .preshared_key - .as_deref() - .map(TryFrom::try_from) - .transpose() - .map_err(|e| Self::Error::TypeConversion(format!("preshared key {e}")))?, - protocol_version: value - .protocol_version - .map(TryFrom::try_from) - .transpose() - .map_err(|e| Self::Error::TypeConversion(format!("protocol version {e}")))?, - endpoint: value - .endpoint - .as_deref() - .map(|e| e.parse()) - .transpose() - .map_err(|e| Self::Error::TypeConversion(format!("endpoint {e}")))?, - last_handshake: value.last_handshake.and_then(|t| { - let unix_time = std::time::UNIX_EPOCH; - if let Ok(millis) = t.and_utc().timestamp_millis().try_into() { - let duration = std::time::Duration::from_millis(millis); - unix_time.checked_add(duration) - } else { - None + public_key: value.public_key.as_str().try_into().map_err(|_| { + Self::Error::TypeConversion { + field_key: "public_key", } - }), - tx_bytes: value - .tx_bytes - .try_into() - .map_err(|e| Self::Error::TypeConversion(format!("tx bytes {e}")))?, - rx_bytes: value - .rx_bytes - .try_into() - .map_err(|e| Self::Error::TypeConversion(format!("rx bytes {e}")))?, - persistent_keepalive_interval: value - .persistent_keepalive_interval - .map(TryFrom::try_from) - .transpose() - .map_err(|e| { - Self::Error::TypeConversion(format!("persistent keepalive interval {e}")) - })?, + })?, allowed_ips: bincode::Options::deserialize( bincode::DefaultOptions::new(), &value.allowed_ips, ) - .map_err(|e| Self::Error::TypeConversion(format!("allowed ips {e}")))?, + .map_err(|source| Self::Error::Deserialize { + field_key: "allowed_ips", + source, + })?, + ..Default::default() }) } } diff --git a/common/gateway-storage/src/tickets.rs b/common/gateway-storage/src/tickets.rs index f8b9539b38..ce68d44514 100644 --- a/common/gateway-storage/src/tickets.rs +++ b/common/gateway-storage/src/tickets.rs @@ -92,7 +92,7 @@ impl TicketStorageManager { ) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == Some(1)) + .map(|result| result.exists == 1) } pub(crate) async fn remove_binary_ticket_data( diff --git a/common/gateway-storage/src/traits.rs b/common/gateway-storage/src/traits.rs new file mode 100644 index 0000000000..aa7e01e434 --- /dev/null +++ b/common/gateway-storage/src/traits.rs @@ -0,0 +1,511 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use async_trait::async_trait; +use nym_credentials_interface::ClientTicket; +use nym_gateway_requests::SharedGatewayKey; +use nym_sphinx::DestinationAddressBytes; +use time::OffsetDateTime; + +use crate::{ + clients::ClientType, + models::{ + Client, PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage, + VerifiedTicket, WireguardPeer, + }, + GatewayStorageError, +}; + +#[async_trait] +pub trait SharedKeyGatewayStorage { + async fn get_mixnet_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result; + async fn insert_shared_keys( + &self, + client_address: DestinationAddressBytes, + shared_keys: &SharedGatewayKey, + ) -> Result; + async fn get_shared_keys( + &self, + client_address: DestinationAddressBytes, + ) -> Result, GatewayStorageError>; + #[allow(dead_code)] + async fn remove_shared_keys( + &self, + client_address: DestinationAddressBytes, + ) -> Result<(), GatewayStorageError>; + async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used_authentication_timestamp: OffsetDateTime, + ) -> Result<(), GatewayStorageError>; + async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError>; +} + +#[async_trait] +pub trait InboxGatewayStorage { + async fn store_message( + &self, + client_address: DestinationAddressBytes, + message: Vec, + ) -> Result<(), GatewayStorageError>; + async fn retrieve_messages( + &self, + client_address: DestinationAddressBytes, + start_after: Option, + ) -> Result<(Vec, Option), GatewayStorageError>; + async fn remove_messages(&self, ids: Vec) -> Result<(), GatewayStorageError>; +} + +#[async_trait] +pub trait BandwidthGatewayStorage: dyn_clone::DynClone { + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError>; + async fn set_expiration( + &self, + client_id: i64, + expiration: OffsetDateTime, + ) -> Result<(), GatewayStorageError>; + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError>; + async fn get_available_bandwidth( + &self, + client_id: i64, + ) -> Result, GatewayStorageError>; + async fn increase_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result; + async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, + amount: i64, + ) -> Result<(), GatewayStorageError>; + async fn decrease_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result; + + async fn insert_epoch_signers( + &self, + epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), GatewayStorageError>; + async fn insert_received_ticket( + &self, + client_id: i64, + received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result; + async fn contains_ticket(&self, serial_number: &[u8]) -> Result; + async fn insert_ticket_verification( + &self, + ticket_id: i64, + signer_id: i64, + verified_at: OffsetDateTime, + accepted: bool, + ) -> Result<(), GatewayStorageError>; + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError>; + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError>; + async fn remove_verified_ticket_binary_data( + &self, + ticket_id: i64, + ) -> Result<(), GatewayStorageError>; + async fn get_all_verified_tickets_with_sn( + &self, + ) -> Result, GatewayStorageError>; + async fn get_all_proposed_tickets_with_sn( + &self, + proposal_id: u32, + ) -> Result, GatewayStorageError>; + async fn insert_redemption_proposal( + &self, + tickets: &[VerifiedTicket], + proposal_id: u32, + created_at: OffsetDateTime, + ) -> Result<(), GatewayStorageError>; + async fn clear_post_proposal_data( + &self, + proposal_id: u32, + resolved_at: OffsetDateTime, + rejected: bool, + ) -> Result<(), GatewayStorageError>; + async fn latest_proposal(&self) -> Result, GatewayStorageError>; + async fn get_all_unverified_tickets(&self) -> Result, GatewayStorageError>; + async fn get_all_unresolved_proposals(&self) -> Result, GatewayStorageError>; + async fn get_votes(&self, ticket_id: i64) -> Result, GatewayStorageError>; + async fn get_signers(&self, epoch_id: i64) -> Result, GatewayStorageError>; + + /// Insert a wireguard peer in the storage. + /// + /// # Arguments + /// + /// * `peer`: wireguard peer data to be stored + async fn insert_wireguard_peer( + &self, + peer: &defguard_wireguard_rs::host::Peer, + client_type: ClientType, + ) -> Result; + + /// Tries to retrieve a particular peer with the given public key. + /// + /// # Arguments + /// + /// * `peer_public_key`: wireguard public key of the peer to be retrieved. + async fn get_wireguard_peer( + &self, + peer_public_key: &str, + ) -> Result, GatewayStorageError>; + + /// Retrieves all wireguard peers. + async fn get_all_wireguard_peers(&self) -> Result, GatewayStorageError>; + + /// Remove a wireguard peer from the storage. + /// + /// # Arguments + /// + /// * `peer_public_key`: wireguard public key of the peer to be removed. + async fn remove_wireguard_peer(&self, peer_public_key: &str) + -> Result<(), GatewayStorageError>; +} + +#[cfg(feature = "mock")] +pub mod mock { + use std::{collections::HashMap, sync::Arc}; + + use tokio::sync::RwLock; + + use super::*; + + struct EcashSigner { + _epoch_id: i64, + _signer_id: i64, + } + + struct ReceivedTicket { + client_id: i64, + _received_at: OffsetDateTime, + rejected: Option, + } + + struct TicketData { + serial_number: Vec, + data: Option>, + } + + struct TicketVerification { + _ticket_id: i64, + _signer_id: i64, + _verified_at: OffsetDateTime, + _accepted: bool, + } + + #[derive(Default)] + pub struct MockGatewayStorage { + available_bandwidth: HashMap, + ecash_signers: Vec, + received_ticket: HashMap, + ticket_data: HashMap, + ticket_verification: HashMap, + verified_tickets: Vec, + wireguard_peers: HashMap, + clients: HashMap, + } + + #[async_trait] + impl BandwidthGatewayStorage for Arc> { + async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> { + self.write().await.available_bandwidth.insert( + client_id, + PersistedBandwidth { + client_id, + available: 0, + expiration: Some(OffsetDateTime::UNIX_EPOCH), + }, + ); + Ok(()) + } + + async fn set_expiration( + &self, + client_id: i64, + expiration: OffsetDateTime, + ) -> Result<(), GatewayStorageError> { + if let Some(bw) = self.write().await.available_bandwidth.get_mut(&client_id) { + bw.expiration = Some(expiration); + } + Ok(()) + } + + async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> { + if let Some(bw) = self.write().await.available_bandwidth.get_mut(&client_id) { + bw.available = 0; + bw.expiration = Some(OffsetDateTime::UNIX_EPOCH); + } + Ok(()) + } + + async fn get_available_bandwidth( + &self, + client_id: i64, + ) -> Result, GatewayStorageError> { + Ok(self + .read() + .await + .available_bandwidth + .get(&client_id) + .cloned()) + } + + async fn increase_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result { + self.write() + .await + .available_bandwidth + .get_mut(&client_id) + .map(|bw| { + bw.available += amount; + bw.available + }) + .ok_or(GatewayStorageError::InternalDatabaseError( + sqlx::Error::RowNotFound, + )) + } + + async fn revoke_ticket_bandwidth( + &self, + ticket_id: i64, + amount: i64, + ) -> Result<(), GatewayStorageError> { + let mut guard = self.write().await; + if let Some(client_id) = guard + .received_ticket + .get(&ticket_id) + .map(|ticket| ticket.client_id) + { + if let Some(bw) = guard.available_bandwidth.get_mut(&client_id) { + bw.available -= amount; + } + } + Ok(()) + } + + async fn decrease_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result { + self.write() + .await + .available_bandwidth + .get_mut(&client_id) + .map(|bw| { + bw.available -= amount; + bw.available + }) + .ok_or(GatewayStorageError::InternalDatabaseError( + sqlx::Error::RowNotFound, + )) + } + + async fn insert_epoch_signers( + &self, + _epoch_id: i64, + signer_ids: Vec, + ) -> Result<(), GatewayStorageError> { + self.write() + .await + .ecash_signers + .extend(signer_ids.iter().map(|signer_id| EcashSigner { + _epoch_id, + _signer_id: *signer_id, + })); + Ok(()) + } + + async fn insert_received_ticket( + &self, + client_id: i64, + _received_at: OffsetDateTime, + serial_number: Vec, + data: Vec, + ) -> Result { + let mut guard = self.write().await; + let ticket_id = guard.received_ticket.len() as i64; + guard.received_ticket.insert( + ticket_id, + ReceivedTicket { + client_id, + _received_at, + rejected: None, + }, + ); + guard.ticket_data.insert( + ticket_id, + TicketData { + serial_number, + data: Some(data), + }, + ); + Ok(ticket_id) + } + + async fn contains_ticket(&self, serial_number: &[u8]) -> Result { + Ok(self + .read() + .await + .ticket_data + .values() + .any(|ticket_data| ticket_data.serial_number == serial_number)) + } + + async fn insert_ticket_verification( + &self, + _ticket_id: i64, + _signer_id: i64, + _verified_at: OffsetDateTime, + _accepted: bool, + ) -> Result<(), GatewayStorageError> { + self.write().await.ticket_verification.insert( + _ticket_id, + TicketVerification { + _ticket_id, + _signer_id, + _verified_at, + _accepted, + }, + ); + Ok(()) + } + + async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { + let mut guard = self.write().await; + if let Some(ticket) = guard.received_ticket.get_mut(&ticket_id) { + ticket.rejected = Some(true); + } + guard.ticket_data.remove(&ticket_id); + Ok(()) + } + + async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { + let mut guard = self.write().await; + guard.verified_tickets.push(ticket_id); + guard.ticket_verification.remove(&ticket_id); + Ok(()) + } + + async fn remove_verified_ticket_binary_data( + &self, + ticket_id: i64, + ) -> Result<(), GatewayStorageError> { + if let Some(ticket) = self.write().await.ticket_data.get_mut(&ticket_id) { + ticket.data = None; + } + Ok(()) + } + + async fn get_all_verified_tickets_with_sn( + &self, + ) -> Result, GatewayStorageError> { + todo!() + } + + async fn get_all_proposed_tickets_with_sn( + &self, + _proposal_id: u32, + ) -> Result, GatewayStorageError> { + todo!() + } + + async fn insert_redemption_proposal( + &self, + _tickets: &[VerifiedTicket], + _proposal_id: u32, + _created_at: OffsetDateTime, + ) -> Result<(), GatewayStorageError> { + todo!() + } + + async fn clear_post_proposal_data( + &self, + _proposal_id: u32, + _resolved_at: OffsetDateTime, + _rejected: bool, + ) -> Result<(), GatewayStorageError> { + todo!() + } + + async fn latest_proposal(&self) -> Result, GatewayStorageError> { + todo!() + } + + async fn get_all_unverified_tickets( + &self, + ) -> Result, GatewayStorageError> { + todo!() + } + + async fn get_all_unresolved_proposals(&self) -> Result, GatewayStorageError> { + todo!() + } + + async fn get_votes(&self, _ticket_id: i64) -> Result, GatewayStorageError> { + todo!() + } + + async fn get_signers(&self, _epoch_id: i64) -> Result, GatewayStorageError> { + todo!() + } + + async fn insert_wireguard_peer( + &self, + peer: &defguard_wireguard_rs::host::Peer, + client_type: ClientType, + ) -> Result { + let mut guard = self.write().await; + let client_id = + if let Some(peer) = guard.wireguard_peers.get(&peer.public_key.to_string()) { + peer.client_id + } else { + let client_id = guard.clients.len() as i64; + guard.clients.insert(client_id, client_type.to_string()); + client_id + }; + guard.wireguard_peers.insert( + peer.public_key.to_string(), + WireguardPeer::from_defguard_peer(peer.clone(), client_id)?, + ); + Ok(client_id) + } + + async fn get_wireguard_peer( + &self, + peer_public_key: &str, + ) -> Result, GatewayStorageError> { + Ok(self + .read() + .await + .wireguard_peers + .get(peer_public_key) + .cloned()) + } + + async fn get_all_wireguard_peers(&self) -> Result, GatewayStorageError> { + todo!() + } + + async fn remove_wireguard_peer( + &self, + peer_public_key: &str, + ) -> Result<(), GatewayStorageError> { + self.write().await.wireguard_peers.remove(peer_public_key); + Ok(()) + } + } +} diff --git a/common/gateway-storage/src/wireguard_peers.rs b/common/gateway-storage/src/wireguard_peers.rs index 00c41483be..22bf178d99 100644 --- a/common/gateway-storage/src/wireguard_peers.rs +++ b/common/gateway-storage/src/wireguard_peers.rs @@ -27,15 +27,18 @@ impl WgPeerManager { pub(crate) async fn insert_peer(&self, peer: &WireguardPeer) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, client_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + INSERT OR IGNORE INTO wireguard_peer(public_key, allowed_ips, client_id) + VALUES (?, ?, ?); UPDATE wireguard_peer - SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, client_id = ? + SET allowed_ips = ?, client_id = ? WHERE public_key = ? "#, - peer.public_key, peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id, - peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id, + peer.public_key, + peer.allowed_ips, + peer.client_id, + peer.allowed_ips, + peer.client_id, peer.public_key, ) .execute(&self.connection_pool) diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 79a291e827..853b79b9a5 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -10,9 +10,14 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default=["tunneling"] +tunneling=[] + [dependencies] async-trait = { workspace = true } -reqwest = { workspace = true, features = ["json", "gzip"] } +bincode = { workspace = true } +reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] } http.workspace = true url = { workspace = true } once_cell = { workspace = true } @@ -20,17 +25,18 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +itertools = { workspace = true } # used for decoding text responses (they were already implicitly included) bytes = { workspace = true } encoding_rs = { workspace = true } mime = { workspace = true } - +nym-http-api-common = { path = "../http-api-common", default-features = false } nym-bin-common = { path = "../bin-common" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] -hickory-resolver = { workspace = true, features = ["dns-over-https-rustls", "webpki-roots"] } +hickory-resolver = { workspace = true, features = ["https-ring", "tls-ring", "webpki-roots"] } # for request timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed [target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] @@ -39,3 +45,4 @@ features = ["tokio"] [dev-dependencies] tokio = { workspace = true, features = ["rt", "macros"] } + diff --git a/common/http-api-client/src/dns.rs b/common/http-api-client/src/dns.rs index d0af6dee3f..3c68afee1e 100644 --- a/common/http-api-client/src/dns.rs +++ b/common/http-api-client/src/dns.rs @@ -6,9 +6,9 @@ //! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints //! supporting DoH and DoT. //! -//! This resolver implements a fallback mechanism where, should the DNS-over-TLS resolution fail, a +//! This resolver supports a fallback mechanism where, should the DNS-over-TLS resolution fail, a //! followup resolution will be done using the hosts configured default (e.g. `/etc/resolve.conf` on -//! linux). +//! linux). This is disabled by default and can be enabled using [`enable_system_fallback`]. //! //! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the //! `hickory-resolver` crate @@ -35,10 +35,10 @@ use std::{ }; use hickory_resolver::{ - config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ResolverOpts}, - error::{ResolveError, ResolveErrorKind}, + config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ServerOrderingStrategy}, lookup_ip::{LookupIp, LookupIpIntoIter}, - TokioAsyncResolver, + name_server::TokioConnectionProvider, + ResolveError, TokioResolver, }; use once_cell::sync::OnceCell; use reqwest::dns::{Addrs, Name, Resolve, Resolving}; @@ -92,15 +92,15 @@ pub struct HickoryDnsResolver { // Since we might not have been called in the context of a // Tokio Runtime in initialization, so we must delay the actual // construction of the resolver. - state: Arc>, - fallback: Arc>, + state: Arc>, + fallback: Option>>, dont_use_shared: bool, } impl Resolve for HickoryDnsResolver { fn resolve(&self, name: Name) -> Resolving { let resolver = self.state.clone(); - let fallback = self.fallback.clone(); + let maybe_fallback = self.fallback.clone(); let independent = self.dont_use_shared; Box::pin(async move { let resolver = resolver.get_or_try_init(|| { @@ -117,26 +117,30 @@ impl Resolve for HickoryDnsResolver { let lookup = match resolver.lookup_ip(name.as_str()).await { Ok(res) => res, Err(e) => { - // on failure use the fall back system configured DNS resolver - match e.kind() { - ResolveErrorKind::NoRecordsFound { .. } => {} - _ => { + if let Some(ref fallback) = maybe_fallback { + // on failure use the fall back system configured DNS resolver + if !e.is_no_records_found() { warn!("primary DNS failed w/ error {e}: using system fallback"); } + let resolver = fallback.get_or_try_init(|| { + // using a closure here is slightly gross, but this makes sure that if the + // lazy-init returns an error it can be handled by the client + if independent { + new_resolver_system() + } else { + Ok(SHARED_RESOLVER + .fallback + .as_ref() + .ok_or(e)? // if the shared resolver has no fallback return the original error + .get_or_try_init(new_resolver_system)? + .clone()) + } + })?; + + resolver.lookup_ip(name.as_str()).await? + } else { + return Err(e.into()); } - let resolver = fallback.get_or_try_init(|| { - // using a closure here is slightly gross, but this makes sure that if the - // lazy-init returns an error it can be handled by the client - if independent { - new_resolver_system() - } else { - Ok(SHARED_RESOLVER - .fallback - .get_or_try_init(new_resolver_system)? - .clone()) - } - })?; - resolver.lookup_ip(name.as_str()).await? } }; @@ -165,17 +169,17 @@ impl HickoryDnsResolver { let lookup = match resolver.lookup_ip(name).await { Ok(res) => res, Err(e) => { - // on failure use the fall back system configured DNS resolver - match e.kind() { - ResolveErrorKind::NoRecordsFound { .. } => {} - _ => { + if let Some(ref fallback) = self.fallback { + // on failure use the fall back system configured DNS resolver + if !e.is_no_records_found() { warn!("primary DNS failed w/ error {e}: using system fallback"); } + + let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?; + resolver.lookup_ip(name).await? + } else { + return Err(e.into()); } - let resolver = self - .fallback - .get_or_try_init(|| self.new_resolver_system())?; - resolver.lookup_ip(name).await? } }; @@ -190,7 +194,7 @@ impl HickoryDnsResolver { } } - fn new_resolver(&self) -> Result { + fn new_resolver(&self) -> Result { if self.dont_use_shared { new_resolver() } else { @@ -198,43 +202,63 @@ impl HickoryDnsResolver { } } - fn new_resolver_system(&self) -> Result { - if self.dont_use_shared { + fn new_resolver_system(&self) -> Result { + if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() { new_resolver_system() } else { Ok(SHARED_RESOLVER .fallback + .as_ref() + .unwrap() .get_or_try_init(new_resolver_system)? .clone()) } } + + /// Enable fallback to the system default resolver if the primary (DoX) resolver fails + pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> { + self.fallback = Some(Default::default()); + let _ = self + .fallback + .as_ref() + .unwrap() + .get_or_try_init(new_resolver_system)?; + Ok(()) + } + + /// Disable fallback resolution. If the primary resolver fails the error is + /// returned immediately + pub fn disable_system_fallback(&mut self) { + self.fallback = None; + } } /// Create a new resolver with a custom DoT based configuration. The options are overridden to look /// up for both IPv4 and IPv6 addresses to work with "happy eyeballs" algorithm. -fn new_resolver() -> Result { +fn new_resolver() -> Result { let mut name_servers = NameServerConfigGroup::quad9_tls(); name_servers.merge(NameServerConfigGroup::quad9_https()); name_servers.merge(NameServerConfigGroup::cloudflare_tls()); name_servers.merge(NameServerConfigGroup::cloudflare_https()); let config = ResolverConfig::from_parts(None, Vec::new(), name_servers); + let mut resolver_builder = + TokioResolver::builder_with_config(config, TokioConnectionProvider::default()); - let mut opts = ResolverOpts::default(); - opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; - // Would like to enable this when 0.25 stabilizes - // opts.server_ordering_strategy = ServerOrderingStrategy::RoundRobin; + resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6; + resolver_builder.options_mut().server_ordering_strategy = ServerOrderingStrategy::RoundRobin; - Ok(TokioAsyncResolver::tokio(config, opts)) + Ok(resolver_builder.build()) } /// Create a new resolver with the default configuration, which reads from the system DNS config /// (i.e. `/etc/resolve.conf` in unix). The options are overridden to look up for both IPv4 and IPv6 /// addresses to work with "happy eyeballs" algorithm. -fn new_resolver_system() -> Result { - let (config, mut opts) = hickory_resolver::system_conf::read_system_conf()?; - opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; - Ok(TokioAsyncResolver::tokio(config, opts)) +fn new_resolver_system() -> Result { + let mut resolver_builder = TokioResolver::builder_tokio()?; + resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6; + + Ok(resolver_builder.build()) } #[cfg(test)] diff --git a/common/http-api-client/src/fronted.rs b/common/http-api-client/src/fronted.rs new file mode 100644 index 0000000000..f3ff92b129 --- /dev/null +++ b/common/http-api-client/src/fronted.rs @@ -0,0 +1,118 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Utilities for and implementation of request tunneling + +use std::sync::atomic::{AtomicBool, Ordering}; +use tracing::warn; + +use crate::ClientBuilder; + +// #[cfg(feature = "tunneling")] +#[derive(Debug)] +pub(crate) struct Front { + pub(crate) policy: FrontPolicy, + enabled: AtomicBool, +} + +impl Clone for Front { + fn clone(&self) -> Self { + Self { + policy: self.policy.clone(), + enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)), + } + } +} + +impl Front { + pub(crate) fn new(policy: FrontPolicy) -> Self { + Self { + enabled: AtomicBool::new(policy == FrontPolicy::Always), + policy, + } + } + + pub(crate) fn is_enabled(&self) -> bool { + match self.policy { + FrontPolicy::Off => false, + FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed), + FrontPolicy::Always => true, + } + } + + // Used to indicate that the client hit an error that should trigger the retry policy + // to enable fronting. + pub(crate) fn retry_enable(&self) { + if self.is_enabled() { + return; + } + if matches!(self.policy, FrontPolicy::OnRetry) { + self.enabled.store(true, Ordering::Relaxed); + } + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +#[cfg(feature = "tunneling")] +pub enum FrontPolicy { + Always, + OnRetry, + #[default] + Off, +} + +impl ClientBuilder { + /// Enable and configure request tunneling for API requests. + #[cfg(feature = "tunneling")] + pub fn with_fronting(mut self, policy: FrontPolicy) -> Self { + let front = Front::new(policy); + + // Check if any of the supplied urls even support fronting + if !self.urls.iter().any(|url| url.has_front()) { + warn!("fronting is enabled, but none of the supplied urls have configured fronting domains"); + } + + self.front = Some(front); + + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ApiClientCore, Url, NO_PARAMS}; + + #[tokio::test] + async fn nym_api_works() { + let url1 = Url::new( + "https://validator.global.ssl.fastly.net", + Some(vec!["https://yelp.global.ssl.fastly.net"]), + ) + .unwrap(); // fastly + + // let url2 = Url::new( + // "https://validator.nymtech.net", + // Some(vec!["https://cdn77.com"]), + // ).unwrap(); // cdn77 + + let client = ClientBuilder::new::<_, &str>(url1) + .expect("bad url") + .with_fronting(FrontPolicy::Always) + .build::<&str>() + .expect("failed to build client"); + + let response = client + .send_request::<_, (), &str, &str, &str>( + reqwest::Method::GET, + &["api", "v1", "network", "details"], + NO_PARAMS, + None, + ) + .await + .expect("failed get request"); + + // println!("{response:?}"); + assert_eq!(response.status(), 200); + } +} diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 5a360f7be1..60122efaf1 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -136,32 +136,48 @@ //! ``` #![warn(missing_docs)] +pub use reqwest::ClientBuilder as ReqwestClientBuilder; +pub use reqwest::StatusCode; + +use crate::path::RequestPath; use async_trait::async_trait; +use bytes::Bytes; +use http::header::CONTENT_TYPE; +use http::HeaderMap; +use itertools::Itertools; +use mime::Mime; use reqwest::header::HeaderValue; -use reqwest::{RequestBuilder, Response, StatusCode}; +use reqwest::{RequestBuilder, Response}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::fmt::Display; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use thiserror::Error; -use tracing::{instrument, warn}; -use url::Url; +use tracing::{debug, instrument, warn}; -use http::HeaderMap; -pub use reqwest::IntoUrl; #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; -#[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; +#[cfg(feature = "tunneling")] +mod fronted; +mod url; +pub use url::{IntoUrl, Url}; mod user_agent; pub use user_agent::UserAgent; #[cfg(not(target_arch = "wasm32"))] mod dns; +mod path; + #[cfg(not(target_arch = "wasm32"))] pub use dns::{HickoryDnsError, HickoryDnsResolver}; +// helper for generating user agent based on binary information +#[doc(hidden)] +pub use nym_bin_common::bin_info; + /// Default HTTP request connection timeout. /// /// The timeout is relatively high as we are often making requests over the mixnet, where latency is @@ -210,26 +226,151 @@ pub enum HttpClientError { #[error("failed to resolve request. status: '{status}', additional error message: {error}")] EndpointFailure { status: StatusCode, error: E }, - #[error("failed to decode response body: {source} from {content}")] - ResponseDecodeFailure { - source: serde_json::Error, - content: String, - }, + #[error("failed to decode response body: {message} from {content}")] + ResponseDecodeFailure { message: String, content: String }, #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] RequestTimeout, } +impl HttpClientError { + /// Returns true if the error is a timeout. + pub fn is_timeout(&self) -> bool { + match self { + HttpClientError::ReqwestClientError { source } => source.is_timeout(), + #[cfg(target_arch = "wasm32")] + HttpClientError::RequestTimeout => true, + _ => false, + } + } + + /// Returns the HTTP status code if available. + pub fn status_code(&self) -> Option { + match self { + HttpClientError::RequestFailure { status } => Some(*status), + HttpClientError::EmptyResponse { status } => Some(*status), + HttpClientError::EndpointFailure { status, .. } => Some(*status), + _ => None, + } + } +} + +/// Core functionality required for types acting as API clients. +/// +/// This trait defines the "skinny waist" of behaviors that are required by an API client. More +/// likely downstream libraries should use functions from the [`ApiClient`] interface which provide +/// a more ergonomic set of functionalities. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait ApiClientCore { + /// Create an HTTP request using the host configured in this client. + fn create_request( + &self, + method: reqwest::Method, + path: P, + params: Params<'_, K, V>, + json_body: Option<&B>, + ) -> RequestBuilder + where + P: RequestPath, + B: Serialize + ?Sized, + K: AsRef, + V: AsRef; + + /// Create an HTTP request using the host configured in this client and an API endpoint (i.e. + /// `"/api/v1/mixnodes?since=12345"`). If the provided endpoint fails to parse as path (and + /// optionally query parameters). + /// + /// Endpoint Examples + /// - `"/api/v1/mixnodes?since=12345"` + /// - `"/api/v1/mixnodes"` + /// - `"/api/v1/mixnodes/img.png"` + /// - `"/api/v1/mixnodes/img.png?since=12345"` + /// - `"/"` + /// - `"/?since=12345"` + /// - `""` + /// - `"?since=12345"` + /// + /// for more information about URL percent encodings see [`url::Url::set_path()`] + fn create_request_endpoint( + &self, + method: reqwest::Method, + endpoint: S, + json_body: Option<&B>, + ) -> RequestBuilder + where + B: Serialize + ?Sized, + S: AsRef, + { + // Use a stand-in url to extract the path and queries from the provided endpoint string + // which could potentially fail. + // + // This parse cannot fail + let mut standin_url: Url = "http://example.com".parse().unwrap(); + + match endpoint.as_ref().split_once("?") { + Some((path, query)) => { + standin_url.set_path(path); + standin_url.set_query(Some(query)); + } + // There is no query in the provided endpoint + None => standin_url.set_path(endpoint.as_ref()), + } + + let path: Vec<&str> = match standin_url.path_segments() { + Some(segments) => segments.collect(), + None => Vec::new(), + }; + let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect(); + + self.create_request(method, path.as_slice(), ¶ms, json_body) + } + + /// Send a created HTTP request. + /// + /// A [`RequestBuilder`] can be created with [`ApiClientCore::create_request`] or + /// [`ApiClientCore::create_request_endpoint`] or if absolutely necessary, using reqwest + /// tooling directly. + async fn send(&self, request: RequestBuilder) -> Result> + where + E: Display; + + /// Create and send a created HTTP request. + async fn send_request( + &self, + method: reqwest::Method, + path: P, + params: Params<'_, K, V>, + json_body: Option<&B>, + ) -> Result> + where + P: RequestPath + Send + Sync, + B: Serialize + ?Sized + Sync, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display, + { + let req = self.create_request(method, path, params, json_body); + self.send(req).await + } +} + /// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently /// and state tracked across subsequent requests. pub struct ClientBuilder { - url: Url, + urls: Vec, + timeout: Option, custom_user_agent: bool, reqwest_client_builder: reqwest::ClientBuilder, #[allow(dead_code)] // not dead code, just unused in wasm use_secure_dns: bool, + + #[cfg(feature = "tunneling")] + front: Option, + + retry_limit: usize, } impl ClientBuilder { @@ -250,40 +391,67 @@ impl ClientBuilder { // TODO: or should we maybe default to https? Self::new(alt) } else { - Ok(Self::new_with_url(url.into_url()?)) + let url = url.to_url()?; + Ok(Self::new_with_urls(vec![url])) } } /// Constructs a new http `ClientBuilder` from a valid url. - pub fn new_with_url(url: Url) -> Self { - if !url.scheme().starts_with("http") { - warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme"); - } + pub fn new_with_urls(urls: Vec) -> Self { + let urls = Self::check_urls(urls); #[cfg(target_arch = "wasm32")] let reqwest_client_builder = reqwest::ClientBuilder::new(); #[cfg(not(target_arch = "wasm32"))] let reqwest_client_builder = { - let r = reqwest::ClientBuilder::new(); - - // Note this is extra as the `gzip` feature for `reqwest` crate should be enabled which - // `"Enable[s] auto gzip decompression by checking the Content-Encoding response header."` + // Note: I believe the manual enable calls for the compression methods are extra + // as the various compression features for `reqwest` crate should be enabled + // just by including the feature which: + // `"Enable[s] auto decompression by checking the Content-Encoding response header."` // - // I am going to leave it here anyways so that gzip decompression is attempted even if - // that feature is removed. - r.gzip(true) + // I am going to leave these here anyways so that removing a decompression method + // from the features list will throw an error if it is not also removed here. + reqwest::ClientBuilder::new() + .gzip(true) + .deflate(true) + .brotli(true) + .zstd(true) }; ClientBuilder { - url, + urls, timeout: None, custom_user_agent: false, reqwest_client_builder, use_secure_dns: true, + #[cfg(feature = "tunneling")] + front: None, + + retry_limit: 0, } } + /// Add an additional URL to the set usable by this constructed `Client` + pub fn add_url(mut self, url: Url) -> Self { + self.urls.push(url); + self + } + + fn check_urls(mut urls: Vec) -> Vec { + // remove any duplicate URLs + urls = urls.into_iter().unique().collect(); + + // warn about any invalid URLs + urls.iter() + .filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https")) + .for_each(|url| { + warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme"); + }); + + urls + } + /// Enables a total request timeout other than the default. /// /// The timeout is applied from when the request starts connecting until the response body has finished. Also considered a total deadline. @@ -294,6 +462,18 @@ impl ClientBuilder { self } + /// Sets the maximum number of retries for a request. This defaults to 0, indicating no retries. + /// + /// Note that setting a retry limit of 3 (for example) will result in 4 attempts to send the + /// request in the case that all are unsuccessful. + /// + /// If multiple urls (or fronting configurations if enabled) are available, retried requests + /// will be sent to the next URL in the list. + pub fn with_retries(mut self, retry_limit: usize) -> Self { + self.retry_limit = retry_limit; + self + } + /// Provide a pre-configured [`reqwest::ClientBuilder`] pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self { self.reqwest_client_builder = reqwest_builder; @@ -351,24 +531,37 @@ impl ClientBuilder { builder.build()? }; - Ok(Client { - base_url: self.url, + let client = Client { + base_urls: self.urls, + current_idx: Arc::new(AtomicUsize::new(0)), reqwest_client, + #[cfg(feature = "tunneling")] + front: self.front, + #[cfg(target_arch = "wasm32")] request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT), - }) + retry_limit: self.retry_limit, + }; + + Ok(client) } } /// A simple extendable client wrapper for http request with extra url sanitization. #[derive(Debug, Clone)] pub struct Client { - base_url: Url, + base_urls: Vec, + current_idx: Arc, reqwest_client: reqwest::Client, + #[cfg(feature = "tunneling")] + front: Option, + #[cfg(target_arch = "wasm32")] request_timeout: Duration, + + retry_limit: usize, } impl Client { @@ -377,7 +570,7 @@ impl Client { // // In order to prevent interference in API requests at the DNS phase we default to a resolver // that uses DoT and DoH. - pub fn new(base_url: Url, timeout: Option) -> Self { + pub fn new(base_url: ::url::Url, timeout: Option) -> Self { Self::new_url::<_, String>(base_url, timeout).expect( "we provided valid url and we were unwrapping previous construction errors anyway", ) @@ -407,162 +600,211 @@ impl Client { ClientBuilder::new(url) } - /// Update the host that this client uses when sending API requests. - pub fn change_base_url(&mut self, new_url: Url) { - self.base_url = new_url + /// Update the set of hosts that this client uses when sending API requests. + pub fn change_base_urls(&mut self, new_urls: Vec) { + self.current_idx.store(0, Ordering::Relaxed); + self.base_urls = new_urls + } + + /// Create new instance of `Client` using the provided base url and existing client config + pub fn clone_with_new_url(&self, new_url: Url) -> Self { + Client { + base_urls: vec![new_url], + current_idx: Arc::new(Default::default()), + reqwest_client: self.reqwest_client.clone(), + + #[cfg(feature = "tunneling")] + front: self.front.clone(), + retry_limit: self.retry_limit, + + #[cfg(target_arch = "wasm32")] + request_timeout: self.request_timeout, + } } /// Get the currently configured host that this client uses when sending API requests. pub fn current_url(&self) -> &Url { - &self.base_url + &self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)] } -} -/// Core functionality required for types acting as API clients. -/// -/// This trait defines the "skinny waist" of behaviors that are required by an API client. More -/// likely downstream libraries should use functions from the [`ApiClient`] interface which provide -/// a more ergonomic set of functionalities. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait ApiClientCore { - /// Create an HTTP request using the host configured in this client. - fn create_request( - &self, - method: reqwest::Method, - path: PathSegments<'_>, - params: Params<'_, K, V>, - json_body: Option<&B>, - ) -> RequestBuilder - where - B: Serialize + ?Sized, - K: AsRef, - V: AsRef; + /// Get the currently configured host that this client uses when sending API requests. + pub fn base_urls(&self) -> &[Url] { + &self.base_urls + } - /// Create an HTTP request using the host configured in this client and an API endpoint (i.e. - /// `"/api/v1/mixnodes?since=12345"`). If the provided endpoint fails to parse as path (and - /// optionally query parameters). - /// - /// Endpoint Examples - /// - `"/api/v1/mixnodes?since=12345"` - /// - `"/api/v1/mixnodes"` - /// - `"/api/v1/mixnodes/img.png"` - /// - `"/api/v1/mixnodes/img.png?since=12345"` - /// - `"/"` - /// - `"/?since=12345"` - /// - `""` - /// - `"?since=12345"` - /// - /// for more information about URL percent encodings see [`url::Url::set_path()`] - fn create_request_endpoint( - &self, - method: reqwest::Method, - endpoint: S, - json_body: Option<&B>, - ) -> RequestBuilder - where - B: Serialize + ?Sized, - S: AsRef, - { - // Use a stand-in url to extract the path and queries from the provided endpoint string - // which could potentially fail. - // - // This parse cannot fail - let mut standin_url: Url = "http://example.com".parse().unwrap(); + /// Get a mutable reference to the hosts that this client uses when sending API requests. + pub fn base_urls_mut(&mut self) -> &mut [Url] { + &mut self.base_urls + } - match endpoint.as_ref().split_once("?") { - Some((path, query)) => { - standin_url.set_path(path); - standin_url.set_query(Some(query)); + /// Change the currently configured limit on the number of retries for a request. + pub fn change_retry_limit(&mut self, limit: usize) { + self.retry_limit = limit; + } + + /// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error) + fn update_host(&self) { + #[cfg(feature = "tunneling")] + if let Some(ref front) = self.front { + if front.is_enabled() { + // if we are using fronting, try updating to the next front + let url = self.current_url(); + + // try to update the current host to use a next front, if one is available, otherwise + // we move on and try the next base url (if one is available) + if url.has_front() && !url.update() { + // we swapped to the next front for the current host + return; + } } - // There is no query in the provided endpoint - None => standin_url.set_path(endpoint.as_ref()), } - let path: Vec<&str> = match standin_url.path_segments() { - Some(segments) => segments.collect(), - None => Vec::new(), - }; - let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect(); + if self.base_urls.len() > 1 { + let orig = self.current_idx.load(Ordering::Relaxed); + let mut next = (orig + 1) % self.base_urls.len(); - self.create_request(method, &path, ¶ms, json_body) + // if fronting is enabled we want to update to a host that has fronts configured + #[cfg(feature = "tunneling")] + if let Some(ref front) = self.front { + if front.is_enabled() { + while next != orig { + if self.base_urls[next].has_front() { + // we have a front for the next host, so we can use it + break; + } + + next = (next + 1) % self.base_urls.len(); + } + } + } + + self.current_idx.store(next, Ordering::Relaxed); + } } - /// Send a created HTTP request. + /// Make modifications to the request to apply the current state of this client i.e. the + /// currently configured host. This is required as a caller may use this client to create a + /// request, but then have the state of the client change before the caller uses the client to + /// send their request. /// - /// A [`RequestBuilder`] can be created with [`ApiClientCore::create_request`] or - /// [`ApiClientCore::create_request_endpoint`] or if absolutely necessary, using reqwest - /// tooling directly. - async fn send(&self, request: RequestBuilder) -> Result> - where - E: Display; + /// This enures that the outgoing requests benefit from the configured fallback mechanisms, even + /// for requests that were created before the state of the client changed. + /// + /// This method assumes that any updates to the state of the client are made before the call to + /// this method. For example, if the client is configured to rotate hosts after each error, this + /// method should be called after the host has been updated -- i.e. as part of the subsequent + /// send. + fn apply_hosts_to_req(&self, r: &mut reqwest::Request) { + let url = self.current_url(); + r.url_mut().set_host(url.host_str()).unwrap(); - /// Create and send a created HTTP request. - async fn send_request( - &self, - method: reqwest::Method, - path: PathSegments<'_>, - params: Params<'_, K, V>, - json_body: Option<&B>, - ) -> Result> - where - B: Serialize + ?Sized + Sync, - K: AsRef + Sync, - V: AsRef + Sync, - E: Display, - { - let req = self.create_request(method, path, params, json_body); - self.send(req).await + #[cfg(feature = "tunneling")] + if let Some(ref front) = self.front { + if front.is_enabled() { + // this should never fail as we are transplanting the host from one url to another + r.url_mut().set_host(url.front_str()).unwrap(); + + let actual_host: HeaderValue = url + .host_str() + .unwrap_or("") + .parse() + .unwrap_or(HeaderValue::from_static("")); + // If the map did have this key present, the new value is associated with the key + // and all previous values are removed. (reqwest HeaderMap docs) + _ = r.headers_mut().insert(reqwest::header::HOST, actual_host); + } + } } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl ApiClientCore for Client { - fn create_request( + #[instrument(level = "debug", skip_all, fields(path=?path))] + fn create_request( &self, method: reqwest::Method, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: Option<&B>, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef, { - let url = sanitize_url(&self.base_url, path, params); + let url = self.current_url(); + let url = sanitize_url(url, path, params); - let mut request = self.reqwest_client.request(method.clone(), url); + let mut req = reqwest::Request::new(method, url.into()); - // Indicate that compressed responses are preferred, but if not supported other encodings are fine. - // TODO: Down the road we can be more selective about adding this, but it's inclusion here guarantees - // that we use compression when available. - request = request.header(reqwest::header::ACCEPT_ENCODING, "gzip;q=1.0, *;q=0.5"); + self.apply_hosts_to_req(&mut req); + + let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req); if let Some(body) = json_body { - request = request.json(body); + rb = rb.json(body); } - request + rb } async fn send(&self, request: RequestBuilder) -> Result> where E: Display, { - #[cfg(target_arch = "wasm32")] - { - Ok( - wasmtimer::tokio::timeout(self.request_timeout, request.send()) - .await - .map_err(|_timeout| HttpClientError::RequestTimeout)??, - ) - } + let mut attempts = 0; + loop { + // try_clone may fail if the body is a stream in which case using retries is not advised. + let r = request + .try_clone() + .ok_or(HttpClientError::GenericRequestFailure( + "failed to send request".to_string(), + ))?; - #[cfg(not(target_arch = "wasm32"))] - { - Ok(request.send().await?) + // apply any changes based on the current state of the client wrt. hosts, + // fronting domains, etc. + let mut req = r.build()?; + self.apply_hosts_to_req(&mut req); + + #[cfg(target_arch = "wasm32")] + let response: Result> = { + Ok(wasmtimer::tokio::timeout( + self.request_timeout, + self.reqwest_client.execute(req), + ) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??) + }; + + #[cfg(not(target_arch = "wasm32"))] + let response = self.reqwest_client.execute(req).await; + + match response { + Ok(resp) => return Ok(resp), + Err(e) => { + // if we have multiple urls, update to the next + self.update_host(); + + #[cfg(feature = "tunneling")] + if let Some(ref front) = self.front { + // If fronting is set to be enabled on error, enable domain fronting as we + // have encountered an error. + front.retry_enable(); + } + + if attempts < self.retry_limit { + warn!("Retrying request due to http error: {}", e); + attempts += 1; + continue; + } + + // if we have exhausted our attempts, return the error + #[allow(clippy::useless_conversion)] // conversion considered useless in wasm + return Err(e.into()); + } + } } } } @@ -574,12 +816,9 @@ impl ApiClientCore for Client { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait ApiClient: ApiClientCore { /// Create an HTTP GET Request with the provided path and parameters - fn create_get_request( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> RequestBuilder + fn create_get_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder where + P: RequestPath, K: AsRef, V: AsRef, { @@ -587,13 +826,14 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP POST Request with the provided path, parameters, and json body - fn create_post_request( + fn create_post_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef, @@ -602,12 +842,9 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP DELETE Request with the provided path and parameters - fn create_delete_request( - &self, - path: PathSegments<'_>, - params: Params<'_, K, V>, - ) -> RequestBuilder + fn create_delete_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder where + P: RequestPath, K: AsRef, V: AsRef, { @@ -615,13 +852,14 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP PATCH Request with the provided path, parameters, and json body - fn create_patch_request( + fn create_patch_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> RequestBuilder where + P: RequestPath, B: Serialize + ?Sized, K: AsRef, V: AsRef, @@ -631,12 +869,13 @@ pub trait ApiClient: ApiClientCore { /// Create and send an HTTP GET Request with the provided path and parameters #[instrument(level = "debug", skip_all, fields(path=?path))] - async fn send_get_request( + async fn send_get_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, E: Display, @@ -646,13 +885,14 @@ pub trait ApiClient: ApiClientCore { } /// Create and send an HTTP POST Request with the provided path, parameters, and json data - async fn send_post_request( + async fn send_post_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, @@ -663,12 +903,13 @@ pub trait ApiClient: ApiClientCore { } /// Create and send an HTTP DELETE Request with the provided path and parameters - async fn send_delete_request( + async fn send_delete_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, E: Display, @@ -678,13 +919,14 @@ pub trait ApiClient: ApiClientCore { } /// Create and send an HTTP PATCH Request with the provided path, parameters, and json data - async fn send_patch_request( + async fn send_patch_request( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, @@ -697,13 +939,33 @@ pub trait ApiClient: ApiClientCore { /// 'get' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - #[instrument(level = "debug", skip_all)] - async fn get_json( + #[instrument(level = "debug", skip_all, fields(path=?path))] + // TODO: deprecate in favour of get_response that works based on mime type in the response + async fn get_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, + for<'a> T: Deserialize<'a>, + K: AsRef + Sync, + V: AsRef + Sync, + E: Display + DeserializeOwned, + { + self.get_response(path, params).await + } + + /// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple + /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response + /// into the provided type `T` based on the content type header + async fn get_response( + &self, + path: P, + params: Params<'_, K, V>, + ) -> Result> + where + P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, @@ -718,13 +980,14 @@ pub trait ApiClient: ApiClientCore { /// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn post_json( + async fn post_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, @@ -740,12 +1003,13 @@ pub trait ApiClient: ApiClientCore { /// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with /// tuple defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the /// response into the provided type `T`. - async fn delete_json( + async fn delete_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, ) -> Result> where + P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, @@ -760,13 +1024,14 @@ pub trait ApiClient: ApiClientCore { /// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn patch_json( + async fn patch_json( &self, - path: PathSegments<'_>, + path: P, params: Params<'_, K, V>, json_body: &B, ) -> Result> where + P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, @@ -849,7 +1114,7 @@ impl ApiClient for C where C: ApiClientCore + Sync {} /// utility function that should solve the double slash problem in API urls forever. fn sanitize_url, V: AsRef>( base: &Url, - segments: PathSegments<'_>, + request_path: impl RequestPath, params: Params<'_, K, V>, ) -> Url { let mut url = base.clone(); @@ -859,10 +1124,7 @@ fn sanitize_url, V: AsRef>( path_segments.pop_if_empty(); - for segment in segments { - let segment = segment.strip_prefix('/').unwrap_or(segment); - let segment = segment.strip_suffix('/').unwrap_or(segment); - + for segment in request_path.to_sanitized_segments() { path_segments.push(segment); } @@ -877,14 +1139,10 @@ fn sanitize_url, V: AsRef>( url } -fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { +fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String { use encoding_rs::{Encoding, UTF_8}; - use mime::Mime; - let content_type = headers - .get(http::header::CONTENT_TYPE) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.parse::().ok()); + let content_type = try_get_mime_type(headers); let encoding_name = content_type .as_ref() @@ -897,7 +1155,7 @@ fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { text.into_owned() } -/// Attempt to parse a json object from an HTTP response +/// Attempt to parse a response object from an HTTP response #[instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> where @@ -919,16 +1177,7 @@ where // internally reqwest is first retrieving bytes and then performing parsing via serde_json // (and similarly does the same thing for text()) let full = res.bytes().await?; - match serde_json::from_slice(&full) { - Ok(data) => Ok(data), - Err(err) => { - let content = decode_as_text(&full, headers); - Err(HttpClientError::ResponseDecodeFailure { - source: err, - content, - }) - } - } + decode_raw_response(&headers, full) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { @@ -947,77 +1196,70 @@ where } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sanitizing_urls() { - let base_url: Url = "http://foomp.com".parse().unwrap(); - - // works with 1 segment - assert_eq!( - "http://foomp.com/foo", - sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str() - ); - - // works with 2 segments - assert_eq!( - "http://foomp.com/foo/bar", - sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str() - ); - - // works with leading slash - assert_eq!( - "http://foomp.com/foo", - sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str() - ); - - // works with trailing slash - assert_eq!( - "http://foomp.com/foo", - sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str() - ); - - // works with both leading and trailing slash - assert_eq!( - "http://foomp.com/foo", - sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar", - sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str() - ); - - // adds params - assert_eq!( - "http://foomp.com/foo/bar?foomp=baz", - sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str() - ); - assert_eq!( - "http://foomp.com/foo/bar?arg1=val1&arg2=val2", - sanitize_url( - &base_url, - &["/foo/", "/bar/"], - &[("arg1", "val1"), ("arg2", "val2")] - ) - .as_str() - ); +fn decode_as_json(headers: &HeaderMap, content: Bytes) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + match serde_json::from_slice(&content) { + Ok(data) => Ok(data), + Err(err) => { + let content = decode_as_text(&content, headers); + Err(HttpClientError::ResponseDecodeFailure { + message: err.to_string(), + content, + }) + } } } + +fn decode_as_bincode(headers: &HeaderMap, content: Bytes) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + use bincode::Options; + + let opts = nym_http_api_common::make_bincode_serializer(); + match opts.deserialize(&content) { + Ok(data) => Ok(data), + Err(err) => { + let content = decode_as_text(&content, headers); + Err(HttpClientError::ResponseDecodeFailure { + message: err.to_string(), + content, + }) + } + } +} + +fn decode_raw_response(headers: &HeaderMap, content: Bytes) -> Result> +where + T: DeserializeOwned, + E: DeserializeOwned + Display, +{ + // if content type header is missing, fallback to our old default, json + let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON); + + debug!("attempting to parse response as {mime}"); + + // unfortunately we can't use stronger typing for subtype as "bincode" is not a defined mime type + match (mime.type_(), mime.subtype().as_str()) { + (mime::APPLICATION, "json") => decode_as_json(headers, content), + (mime::APPLICATION, "bincode") => decode_as_bincode(headers, content), + (_, _) => { + debug!("unrecognised mime type {mime}. falling back to json decoding..."); + decode_as_json(headers, content) + } + } +} + +fn try_get_mime_type(headers: &HeaderMap) -> Option { + headers + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()) +} + +#[cfg(test)] +mod tests; diff --git a/common/http-api-client/src/path.rs b/common/http-api-client/src/path.rs new file mode 100644 index 0000000000..b7aeea5e47 --- /dev/null +++ b/common/http-api-client/src/path.rs @@ -0,0 +1,59 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Debug; + +/// Collection of URL Path Segments +pub type PathSegments<'a> = &'a [&'a str]; + +fn sanitize_fragment(segment: &str) -> &str { + segment.trim_matches(|c: char| c.is_whitespace() || c == '/') +} + +pub trait RequestPath: Debug { + fn to_sanitized_segments(&self) -> Vec<&str>; +} + +macro_rules! impl_stringified_sanitized_segments { + ($frag_iter:expr) => {{ + let mut path_segments = Vec::new(); + + for segment in $frag_iter { + if !segment.is_empty() { + path_segments.push(sanitize_fragment(segment)); + } + } + + path_segments + }}; +} + +impl RequestPath for PathSegments<'_> { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.iter()) + } +} + +impl RequestPath for &[&str; N] { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.iter()) + } +} + +impl RequestPath for &str { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.split('/')) + } +} + +impl RequestPath for String { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.split('/')) + } +} + +impl RequestPath for &String { + fn to_sanitized_segments(&self) -> Vec<&str> { + impl_stringified_sanitized_segments!(self.split('/')) + } +} diff --git a/common/http-api-client/src/tests.rs b/common/http-api-client/src/tests.rs new file mode 100644 index 0000000000..6f295cd845 --- /dev/null +++ b/common/http-api-client/src/tests.rs @@ -0,0 +1,224 @@ +use super::*; + +#[test] +fn sanitizing_urls() { + let base_url: Url = "http://foomp.com".parse().unwrap(); + + // works with a full string + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str() + ); + + // (and leading slash doesn't matter) + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str() + ); + + // works with 1 segment + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str() + ); + + // works with 2 segments + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str() + ); + + // works with leading slash + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str() + ); + + // works with trailing slash + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str() + ); + + // works with both leading and trailing slash + assert_eq!( + "http://foomp.com/foo", + sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar", + sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str() + ); + + // adds params + assert_eq!( + "http://foomp.com/foo/bar?foomp=baz", + sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str() + ); + assert_eq!( + "http://foomp.com/foo/bar?arg1=val1&arg2=val2", + sanitize_url( + &base_url, + &["/foo/", "/bar/"], + &[("arg1", "val1"), ("arg2", "val2")] + ) + .as_str() + ); +} + +// - Do the retries work +// - Do we use fallback urls on retry if multiple are provided +// - Do we use the next front on retry if multiple are provided +// - If we have more retries than urls, do we wrap back to the first one again +// - on error without retries is where we have multiple urls, is the url updated? + +#[tokio::test] +async fn api_client_retry() -> Result<(), Box> { + let client = ClientBuilder::new_with_urls(vec![ + "http://broken.nym.badurl".parse()?, + "http://example.com/".parse()?, + ]) + .with_retries(3) + .build::()?; + + let req = client.create_get_request(&["/"], NO_PARAMS); + let resp = client.send::(req).await?; + + assert_eq!(resp.status(), 200); + + // check that the url was updated + assert_eq!(client.current_url().as_str(), "http://example.com/"); + + Ok(()) +} + +#[test] +fn host_updating() { + let url = Url::new("http://example.com", None).unwrap(); + let mut client = ClientBuilder::new::<_, &str>(url) + .unwrap() + .build::<&str>() + .unwrap(); + + // check that the url is set correctly + let current_url = client.current_url(); + assert_eq!(current_url.as_str(), "http://example.com/"); + assert_eq!(current_url.front_str(), None); + + // update the url + client.update_host(); + + // check that the url is still the same since there is one URL + assert_eq!(client.current_url().as_str(), "http://example.com/"); + + // ======================================= + // we rotate through urls when available + + let new_urls = vec![ + Url::new("http://example.com", None).unwrap(), + Url::new("http://example.org", None).unwrap(), + ]; + client.change_base_urls(new_urls); + assert_eq!(client.current_url().as_str(), "http://example.com/"); + + client.update_host(); + + // check that the url got updated now that there are multiple URLs + assert_eq!(client.current_url().as_str(), "http://example.org/"); + assert_eq!(client.current_url().front_str(), None); + + client.update_host(); + assert_eq!(client.current_url().as_str(), "http://example.com/"); + + // ======================================= + // we rotate through urls when available if fronting is disabled + + let new_urls = vec![ + Url::new( + "http://example.com", + Some(vec!["http://front1.com", "http://front2.com"]), + ) + .unwrap(), + Url::new("http://example.org", None).unwrap(), + ]; + client.change_base_urls(new_urls); + + assert_eq!(client.current_url().as_str(), "http://example.com/"); + + client.update_host(); + + // check that the url got updated now that there are multiple URLs + assert_eq!(client.current_url().as_str(), "http://example.org/"); +} + +#[test] +#[cfg(feature = "tunneling")] +fn fronted_host_updating() { + let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap(); + let mut client = ClientBuilder::new::<_, &str>(url) + .unwrap() + .with_fronting(crate::fronted::FrontPolicy::Always) + .build::<&str>() + .unwrap(); + + // check that the url is set correctly + let current_url = client.current_url(); + assert_eq!(current_url.as_str(), "http://example.com/"); + assert_eq!(current_url.front_str(), Some("front1.com")); + + // update the url + client.update_host(); + + // check that the url is still the same since there is one URL and one front + let current_url = client.current_url(); + assert_eq!(current_url.as_str(), "http://example.com/"); + assert_eq!(current_url.front_str(), Some("front1.com")); + + // ======================================= + // we rotate through front urls when available if fronting is enabled + + let new_urls = vec![ + Url::new( + "http://example.com", + Some(vec!["http://front1.com", "http://front2.com"]), + ) + .unwrap(), + Url::new("http://example.org", None).unwrap(), + ]; + client.change_base_urls(new_urls); + + let current_url = client.current_url(); + assert_eq!(current_url.as_str(), "http://example.com/"); + assert_eq!(current_url.front_str(), Some("front1.com")); + + // update the url - this should keep the same host but change the front + client.update_host(); + + let current_url = client.current_url(); + // check that the url is still the same since there is one URL + assert_eq!(current_url.as_str(), "http://example.com/"); + assert_eq!(current_url.front_str(), Some("front2.com")); + + // update the url - this should wrap around to the first front as the second url is not fronted + client.update_host(); + + let current_url = client.current_url(); + assert_eq!(current_url.as_str(), "http://example.com/"); + assert_eq!(current_url.front_str(), Some("front1.com")); +} diff --git a/common/http-api-client/src/url.rs b/common/http-api-client/src/url.rs new file mode 100644 index 0000000000..b4a872e109 --- /dev/null +++ b/common/http-api-client/src/url.rs @@ -0,0 +1,291 @@ +//! Url handling for the HTTP API client. +//! +//! This module provides a `Url` struct that wraps around the `url::Url` type and adds +//! functionality for handling front domains, which are used for reverse proxying. + +use std::fmt::Display; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use itertools::Itertools; +use url::form_urlencoded; +pub use url::ParseError; + +/// A trait to try to convert some type into a `Url`. +pub trait IntoUrl { + /// Parse as a valid `Url` + fn to_url(self) -> Result; + + /// Returns the string representation of the URL. + fn as_str(&self) -> &str; +} + +impl IntoUrl for &str { + fn to_url(self) -> Result { + let url = url::Url::parse(self)?; + Ok(url.into()) + } + + fn as_str(&self) -> &str { + self + } +} + +impl IntoUrl for String { + fn to_url(self) -> Result { + let url = url::Url::parse(&self)?; + Ok(url.into()) + } + + fn as_str(&self) -> &str { + self + } +} + +impl IntoUrl for reqwest::Url { + fn to_url(self) -> Result { + Ok(self.into()) + } + + fn as_str(&self) -> &str { + self.as_str() + } +} + +/// When configuring fronting, some configurations will require a specific backend host +/// to be used for the request to be properly reverse proxied. +#[derive(Debug, Clone)] +pub struct Url { + url: url::Url, + fronts: Option>, + current_front: Arc, +} + +impl IntoUrl for Url { + fn to_url(self) -> Result { + Ok(self) + } + + fn as_str(&self) -> &str { + self.url.as_str() + } +} + +impl PartialEq for Url { + fn eq(&self, other: &Self) -> bool { + let current = self.current_front.load(Ordering::Relaxed); + let other_current = other.current_front.load(Ordering::Relaxed); + + self.fronts == other.fronts && self.url == other.url && current == other_current + } +} + +impl Eq for Url {} + +impl std::hash::Hash for Url { + fn hash(&self, state: &mut H) { + let current = self.current_front.load(Ordering::Relaxed); + self.fronts.hash(state); + self.url.hash(state); + current.hash(state); + } +} + +impl Display for Url { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.fronts { + Some(ref fronts) => { + let current = self.current_front.load(Ordering::Relaxed); + if let Some(front) = fronts.get(current) { + write!(f, "{front}=>{}", self.url) + } else { + write!(f, "{}", self.url) + } + } + None => write!(f, "{}", self.url), + } + } +} + +impl From for url::Url { + fn from(val: Url) -> Self { + val.url + } +} + +impl From for Url { + fn from(url: url::Url) -> Self { + Self { + url, + fronts: None, + current_front: Arc::new(AtomicUsize::new(0)), + } + } +} + +impl AsRef for Url { + fn as_ref(&self) -> &url::Url { + &self.url + } +} + +impl AsMut for Url { + fn as_mut(&mut self) -> &mut url::Url { + &mut self.url + } +} + +impl std::str::FromStr for Url { + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + let url = url::Url::parse(s)?; + Ok(Self { + url, + fronts: None, + current_front: Arc::new(AtomicUsize::new(0)), + }) + } +} + +impl Url { + /// Create a new `Url` instance with the given something that can be parsed as a URL and + /// optional tunneling domains + pub fn new( + url: U, + fronts: Option>, + ) -> Result { + let mut url = Self { + url: url.into_url()?, + fronts: None, + current_front: Arc::new(AtomicUsize::new(0)), + }; + + // ensure that the provided URLs are valid + if let Some(front_domains) = fronts { + let f: Vec = front_domains + .into_iter() + .map(|front| front.into_url()) + .try_collect()?; + url.fronts = Some(f); + } + + Ok(url) + } + + /// Parse an absolute URL from a string. + pub fn parse(s: &str) -> Result { + let url = url::Url::parse(s)?; + Ok(Self { + url, + fronts: None, + current_front: Arc::new(AtomicUsize::new(0)), + }) + } + + /// Returns true if the URL has a front domain set + pub fn has_front(&self) -> bool { + if let Some(fronts) = &self.fronts { + return !fronts.is_empty(); + } + false + } + + /// Return the string representation of the current front host (domain or IP address) for this + /// URL, if any. + pub fn front_str(&self) -> Option<&str> { + let current = self.current_front.load(Ordering::Relaxed); + self.fronts + .as_ref() + .and_then(|fronts| fronts.get(current)) + .and_then(|url| url.host_str()) + } + + /// Return the string representation of the host (domain or IP address) for this URL, if any. + pub fn host_str(&self) -> Option<&str> { + self.url.host_str() + } + + /// Return the serialization of this URL. + /// + /// This is fast since that serialization is already stored in the inner url::Url struct. + pub fn as_str(&self) -> &str { + self.url.as_str() + } + + /// Returns true if updating the front wraps back to the first front, or if no fronts are set + pub fn update(&self) -> bool { + if let Some(fronts) = &self.fronts { + if fronts.len() > 1 { + let current = self.current_front.load(Ordering::Relaxed); + let next = (current + 1) % fronts.len(); + self.current_front.store(next, Ordering::Relaxed); + return next == 0; + } + } + true + } + + /// Return the scheme of this URL, lower-cased, as an ASCII string without the ‘:’ delimiter. + pub fn scheme(&self) -> &str { + self.url.scheme() + } + + /// Parse the URL’s query string, if any, as application/x-www-form-urlencoded and return an + /// iterator of (key, value) pairs. + pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> { + self.url.query_pairs() + } + + /// Manipulate this URL’s query string, viewed as a sequence of name/value pairs in + /// application/x-www-form-urlencoded syntax. + pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<'_, ::url::UrlQuery<'_>> { + self.url.query_pairs_mut() + } + + /// Change this URL’s query string. If `query` is `None`, this URL’s query string will be cleared. + pub fn set_query(&mut self, query: Option<&str>) { + self.url.set_query(query); + } + + /// Change this URL’s path. + pub fn set_path(&mut self, path: &str) { + self.url.set_path(path); + } + + /// Change this URL’s scheme. + pub fn set_scheme(&mut self, scheme: &str) { + self.url.set_scheme(scheme).unwrap(); + } + + /// Change this URL’s host. + /// + /// Removing the host (calling this with None) will also remove any username, password, and port number. + pub fn set_host(&mut self, host: &str) { + self.url.set_host(Some(host)).unwrap(); + } + + /// Change this URL’s port number. + /// + /// Note that default port numbers are not reflected in the serialization. + /// + /// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; do nothing and return `Err`. + pub fn set_port(&mut self, port: u16) { + self.url.set_port(Some(port)).unwrap(); + } + + /// Return an object with methods to manipulate this URL’s path segments. + /// + /// Return Err(()) if this URL is cannot-be-a-base. + pub fn path_segments(&self) -> Option> { + self.url.path_segments() + } + + /// Return an object with methods to manipulate this URL’s path segments. + /// + /// Return Err(()) if this URL is cannot-be-a-base. + #[allow(clippy::result_unit_err)] + pub fn path_segments_mut(&mut self) -> Result<::url::PathSegmentsMut<'_>, ()> { + self.url.path_segments_mut() + } +} diff --git a/common/http-api-client/src/user_agent.rs b/common/http-api-client/src/user_agent.rs index fee71752bb..f46a2038ae 100644 --- a/common/http-api-client/src/user_agent.rs +++ b/common/http-api-client/src/user_agent.rs @@ -16,10 +16,20 @@ pub struct UserAgent { pub version: String, /// client platform pub platform: String, - /// source commit version for the calling calling crate / subsystem + /// source commit version for the calling crate / subsystem pub git_commit: String, } +/// Create `UserAgent` based on the caller's crate information +// we can't use normal function as then `application` and `version` would correspond +// of that of `nym-http-api-client` lib +#[macro_export] +macro_rules! generate_user_agent { + () => { + $crate::UserAgent::from($crate::bin_info!()) + }; +} + #[derive(Clone, Debug, thiserror::Error)] #[error("invalid user agent string: {0}")] pub struct UserAgentError(String); @@ -141,7 +151,7 @@ mod tests { }; assert_eq!( - format!("{}", user_agent), + format!("{user_agent}"), "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg" ); } diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index e8cf08b103..4a405c155e 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -11,20 +11,45 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum-client-ip.workspace = true -axum.workspace = true -bytes = { workspace = true } -colored.workspace = true -futures = { workspace = true } -mime = { workspace = true } +axum = { workspace = true, optional = true } +axum-client-ip = { workspace = true, optional = true } +bincode = { workspace = true } +bytes = { workspace = true, optional = true } +colored = { workspace = true, optional = true } +futures = { workspace = true, optional = true } +mime = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -serde_yaml = { workspace = true } -subtle.workspace = true -tower = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true, optional = true } +subtle = { workspace = true, optional = true } +time = { workspace = true, optional = true, features = ["macros"] } +tower = { workspace = true, optional = true } tracing.workspace = true utoipa = { workspace = true, optional = true } -zeroize = { workspace = true } +zeroize = { workspace = true, optional = true } [features] +default = [] +output = [ + "axum", + "bytes", + "mime", + "serde_yaml", + "time", + "time/formatting" +] + +middleware = [ + "axum", + "axum-client-ip", + "colored", + "futures", + "subtle", + "tower", + "zeroize" +] + utoipa = ["dep:utoipa"] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index ffd258de95..9ed715cde2 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -1,92 +1,20 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use axum::http::{header, HeaderValue, StatusCode}; -use axum::response::{IntoResponse, Response}; -use axum::Json; -use bytes::{BufMut, BytesMut}; -use serde::{Deserialize, Serialize}; - +#[cfg(feature = "middleware")] pub mod middleware; -#[derive(Debug, Clone)] -pub enum FormattedResponse { - Json(Json), - Yaml(Yaml), -} +#[cfg(feature = "output")] +pub mod response; -impl IntoResponse for FormattedResponse -where - T: Serialize, -{ - fn into_response(self) -> Response { - match self { - FormattedResponse::Json(json_response) => json_response.into_response(), - FormattedResponse::Yaml(yaml_response) => yaml_response.into_response(), - } - } -} +// don't break existing imports +#[cfg(feature = "output")] +pub use response::*; -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[serde(rename_all = "lowercase")] -pub enum Output { - #[default] - Json, - Yaml, -} - -#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] -#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))] -#[serde(default)] -pub struct OutputParams { - pub output: Option, -} - -impl Output { - pub fn to_response(self, data: T) -> FormattedResponse { - match self { - Output::Json => FormattedResponse::Json(Json(data)), - Output::Yaml => FormattedResponse::Yaml(Yaml(data)), - } - } -} - -#[derive(Debug, Clone, Copy, Default)] -#[must_use] -pub struct Yaml(pub T); - -impl From for Yaml { - fn from(inner: T) -> Self { - Self(inner) - } -} - -impl IntoResponse for Yaml -where - T: Serialize, -{ - // replicates axum's Json - fn into_response(self) -> Response { - let mut buf = BytesMut::with_capacity(128).writer(); - match serde_yaml::to_writer(&mut buf, &self.0) { - Ok(()) => ( - [( - header::CONTENT_TYPE, - HeaderValue::from_static("application/yaml"), - )], - buf.into_inner().freeze(), - ) - .into_response(), - Err(err) => ( - StatusCode::INTERNAL_SERVER_ERROR, - [( - header::CONTENT_TYPE, - HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), - )], - err.to_string(), - ) - .into_response(), - } - } +// be explicit about those values because bincode uses different defaults in different places +pub fn make_bincode_serializer() -> impl ::bincode::Options { + use ::bincode::Options; + ::bincode::DefaultOptions::new() + .with_little_endian() + .with_varint_encoding() } diff --git a/common/http-api-common/src/middleware/logging.rs b/common/http-api-common/src/middleware/logging.rs index fd60ca30b1..84c33c8a7d 100644 --- a/common/http-api-common/src/middleware/logging.rs +++ b/common/http-api-common/src/middleware/logging.rs @@ -9,13 +9,35 @@ use axum::response::IntoResponse; use axum_client_ip::InsecureClientIp; use colored::Colorize; use std::time::Instant; -use tracing::info; +use tracing::{debug, info}; + +enum LogLevel { + Debug, + Info, +} + +pub async fn log_request_info( + insecure_client_ip: InsecureClientIp, + request: Request, + next: Next, +) -> impl IntoResponse { + log_request(insecure_client_ip, request, next, LogLevel::Info).await +} + +pub async fn log_request_debug( + insecure_client_ip: InsecureClientIp, + request: Request, + next: Next, +) -> impl IntoResponse { + log_request(insecure_client_ip, request, next, LogLevel::Debug).await +} /// Simple logger for requests -pub async fn logger( +async fn log_request( InsecureClientIp(addr): InsecureClientIp, request: Request, next: Next, + level: LogLevel, ) -> impl IntoResponse { // TODO dz use `OriginalUri` extractor to get full URI even for nested // routers if routes aren't logged correctly in handlers @@ -58,7 +80,14 @@ pub async fn logger( let agent_str = "agent".bold(); - info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); + match level { + LogLevel::Debug => debug!( + "[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}" + ), + LogLevel::Info => info!( + "[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}" + ), + } res } diff --git a/common/http-api-common/src/response/bincode.rs b/common/http-api-common/src/response/bincode.rs new file mode 100644 index 0000000000..cea2ae10d3 --- /dev/null +++ b/common/http-api-common/src/response/bincode.rs @@ -0,0 +1,53 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::response::{error_response, ResponseWrapper}; +use axum::http::header::IntoHeaderName; +use axum::http::{header, HeaderValue}; +use axum::response::{IntoResponse, Response}; +use bytes::{BufMut, BytesMut}; +use serde::Serialize; + +#[derive(Debug, Clone, Default)] +#[must_use] +pub struct Bincode(pub(crate) ResponseWrapper); + +impl From for Bincode { + fn from(response: T) -> Self { + Bincode(ResponseWrapper::new(response).with_header( + header::CONTENT_TYPE, + HeaderValue::from_static("application/bincode"), + )) + } +} + +impl Bincode { + pub(crate) fn with_header( + mut self, + name: impl IntoHeaderName, + value: impl Into, + ) -> Self { + self.0.headers.insert(name, value.into()); + self + } + + pub(crate) fn map U>(self, op: F) -> Bincode { + Bincode(self.0.map(op)) + } +} + +impl IntoResponse for Bincode +where + T: Serialize, +{ + // replicates axum's Json + fn into_response(self) -> Response { + use bincode::Options; + let mut buf = BytesMut::with_capacity(128).writer(); + + match crate::make_bincode_serializer().serialize_into(&mut buf, &self.0.data) { + Ok(()) => (self.0.headers, buf.into_inner().freeze()).into_response(), + Err(err) => error_response(err), + } + } +} diff --git a/common/http-api-common/src/response/json.rs b/common/http-api-common/src/response/json.rs new file mode 100644 index 0000000000..a1b46e7d50 --- /dev/null +++ b/common/http-api-common/src/response/json.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::response::{error_response, ResponseWrapper}; +use axum::http::header::IntoHeaderName; +use axum::http::{header, HeaderValue}; +use axum::response::{IntoResponse, Response}; +use bytes::{BufMut, BytesMut}; +use serde::Serialize; + +// don't use axum's Json directly as we need to be able to define custom headers +#[derive(Debug, Clone, Default)] +#[must_use] +pub struct Json(pub(crate) ResponseWrapper); + +impl From for Json { + fn from(response: T) -> Self { + Json(ResponseWrapper::new(response).with_header( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), + )) + } +} + +impl Json { + pub(crate) fn with_header( + mut self, + name: impl IntoHeaderName, + value: impl Into, + ) -> Self { + self.0.headers.insert(name, value.into()); + self + } + + pub(crate) fn map U>(self, op: F) -> Json { + Json(self.0.map(op)) + } +} + +impl IntoResponse for Json +where + T: Serialize, +{ + fn into_response(self) -> Response { + let mut buf = BytesMut::with_capacity(128).writer(); + + match serde_json::to_writer(&mut buf, &self.0.data) { + Ok(()) => (self.0.headers, buf.into_inner().freeze()).into_response(), + Err(err) => error_response(err), + } + } +} diff --git a/common/http-api-common/src/response/mod.rs b/common/http-api-common/src/response/mod.rs new file mode 100644 index 0000000000..2753aaf8f0 --- /dev/null +++ b/common/http-api-common/src/response/mod.rs @@ -0,0 +1,210 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::header::IntoHeaderName; +use axum::http::{header, HeaderMap, HeaderValue, StatusCode}; +use axum::response::{IntoResponse, Response}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::format_description::BorrowedFormatItem; +use time::macros::{format_description, offset}; +use time::OffsetDateTime; + +pub mod bincode; +pub mod json; +pub mod yaml; + +pub use bincode::Bincode; +pub use json::Json; +pub use yaml::Yaml; + +#[derive(Debug, Clone, Default)] +pub(crate) struct ResponseWrapper { + data: T, + headers: HeaderMap, +} + +impl ResponseWrapper { + pub(crate) fn new(response: T) -> ResponseWrapper { + ResponseWrapper { + data: response, + headers: Default::default(), + } + } + + pub(crate) fn map U>(self, op: F) -> ResponseWrapper { + ResponseWrapper { + data: op(self.data), + headers: self.headers, + } + } + + #[must_use] + pub(crate) fn with_header( + mut self, + name: impl IntoHeaderName, + value: impl Into, + ) -> Self { + self.headers.insert(name, value.into()); + self + } +} + +#[derive(Debug, Clone)] +pub enum FormattedResponse { + Json(Json), + Yaml(Yaml), + Bincode(Bincode), +} + +impl FormattedResponse { + pub fn into_inner(self) -> T { + match self { + FormattedResponse::Json(inner) => inner.0.data, + FormattedResponse::Yaml(inner) => inner.0.data, + FormattedResponse::Bincode(inner) => inner.0.data, + } + } + + pub fn map U>(self, op: F) -> FormattedResponse { + match self { + FormattedResponse::Json(inner) => FormattedResponse::Json(inner.map(op)), + FormattedResponse::Yaml(inner) => FormattedResponse::Yaml(inner.map(op)), + FormattedResponse::Bincode(inner) => FormattedResponse::Bincode(inner.map(op)), + } + } + + #[must_use] + pub fn with_header( + self, + name: impl IntoHeaderName, + value: impl Into, + ) -> FormattedResponse { + match self { + FormattedResponse::Json(inner) => { + FormattedResponse::Json(inner.with_header(name, value)) + } + FormattedResponse::Yaml(inner) => { + FormattedResponse::Yaml(inner.with_header(name, value)) + } + FormattedResponse::Bincode(inner) => { + FormattedResponse::Bincode(inner.with_header(name, value)) + } + } + } + + /// Set the `expires` header on the response to the provided expiration. + /// Internally it will perform conversions to make sure the value is set in GMT offset, + /// e.g. `Expires: Wed, 21 Oct 2015 07:28:00 GMT` + #[must_use] + pub fn with_expires_header(self, expiration: OffsetDateTime) -> FormattedResponse { + // as per RFC-7234 (section 5.3) EXPIRES header has to use value formatted + // as defined in RFC-7231 (section 7.1.1.1) + // (preferred format (IMF-fixdate) uses RFC-5322 (section 3.3) + let formatted = format_rfc5352(expiration); + + // SAFETY: our formatted datetime doesn't contain forbidden characters + #[allow(clippy::unwrap_used)] + self.with_header(header::EXPIRES, HeaderValue::try_from(formatted).unwrap()) + } + + /// Work similarly to `with_expires_header`, but rather than setting explicit expiration value, + /// it adds the provided time delta to the current time instead. + #[must_use] + pub fn with_expires_header_delta(self, expires_in: Duration) -> FormattedResponse { + self.with_expires_header(OffsetDateTime::now_utc() + expires_in) + } +} + +impl IntoResponse for FormattedResponse +where + T: Serialize, +{ + fn into_response(self) -> Response { + match self { + FormattedResponse::Json(json_response) => json_response.into_response(), + FormattedResponse::Yaml(yaml_response) => yaml_response.into_response(), + FormattedResponse::Bincode(bincode_response) => bincode_response.into_response(), + } + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[serde(rename_all = "lowercase")] +pub enum Output { + #[default] + Json, + Yaml, + Bincode, +} + +#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)] +#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))] +#[serde(default)] +pub struct OutputParams { + pub output: Option, +} + +impl OutputParams { + pub fn get_output(&self) -> Output { + self.output.unwrap_or_default() + } +} + +impl Output { + pub fn to_response(self, data: T) -> FormattedResponse { + match self { + Output::Json => FormattedResponse::Json(Json::from(data)), + Output::Yaml => FormattedResponse::Yaml(Yaml::from(data)), + Output::Bincode => FormattedResponse::Bincode(Bincode::from(data)), + } + } +} + +pub(crate) fn error_response(err: E) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), + )], + err.to_string(), + ) + .into_response() +} + +// SAFETY: this hardcoded datetime formatter is valid +#[allow(clippy::unwrap_used)] +fn format_rfc5352(datetime: OffsetDateTime) -> String { + // the time must be using GMT (UTC) offset + let normalised = datetime.to_offset(offset!(UTC)); + normalised.format(&rfc5322()).unwrap() +} + +// NOTE: this function is purposely not made public as it cannot guarantee caller +// has correctly ensured their date is using correct GMT offset +fn rfc5322() -> &'static [BorrowedFormatItem<'static>] { + // D, d M Y H:i:s T + format_description!( + "[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] GMT" + ) +} + +#[cfg(test)] +mod tests { + use crate::response::format_rfc5352; + use time::macros::datetime; + + #[test] + fn rfc5322_formatting() { + let utc_date = datetime!(2021-08-23 12:13:14 UTC); + let non_utc_date = datetime!(2021-08-23 12:13:14 -1); + + assert_eq!("Mon, 23 Aug 2021 12:13:14 GMT", format_rfc5352(utc_date)); + assert_eq!( + "Mon, 23 Aug 2021 13:13:14 GMT", + format_rfc5352(non_utc_date) + ); + } +} diff --git a/common/http-api-common/src/response/yaml.rs b/common/http-api-common/src/response/yaml.rs new file mode 100644 index 0000000000..d9bc163c1a --- /dev/null +++ b/common/http-api-common/src/response/yaml.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::response::{error_response, ResponseWrapper}; +use axum::http::header::IntoHeaderName; +use axum::http::{header, HeaderValue}; +use axum::response::{IntoResponse, Response}; +use bytes::{BufMut, BytesMut}; +use serde::Serialize; + +#[derive(Debug, Clone, Default)] +#[must_use] +pub struct Yaml(pub(crate) ResponseWrapper); + +impl From for Yaml { + fn from(response: T) -> Self { + Yaml(ResponseWrapper::new(response).with_header( + header::CONTENT_TYPE, + HeaderValue::from_static("application/yaml"), + )) + } +} + +impl Yaml { + pub(crate) fn with_header( + mut self, + name: impl IntoHeaderName, + value: impl Into, + ) -> Self { + self.0.headers.insert(name, value.into()); + self + } + + pub(crate) fn map U>(self, op: F) -> Yaml { + Yaml(self.0.map(op)) + } +} + +impl IntoResponse for Yaml +where + T: Serialize, +{ + // replicates axum's Json + fn into_response(self) -> Response { + let mut buf = BytesMut::with_capacity(128).writer(); + match serde_yaml::to_writer(&mut buf, &self.0.data) { + Ok(()) => (self.0.headers, buf.into_inner().freeze()).into_response(), + Err(err) => error_response(err), + } + } +} diff --git a/common/ip-packet-requests/src/v8/request.rs b/common/ip-packet-requests/src/v8/request.rs index c5e49e83b7..964065578b 100644 --- a/common/ip-packet-requests/src/v8/request.rs +++ b/common/ip-packet-requests/src/v8/request.rs @@ -191,7 +191,7 @@ impl fmt::Display for IpPacketRequestData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { IpPacketRequestData::Data(_) => write!(f, "Data"), - IpPacketRequestData::Control(c) => write!(f, "Control({})", c), + IpPacketRequestData::Control(c) => write!(f, "Control({c})"), } } } diff --git a/common/network-defaults/build.rs b/common/network-defaults/build.rs index aa52baa876..2164bd2820 100644 --- a/common/network-defaults/build.rs +++ b/common/network-defaults/build.rs @@ -23,7 +23,6 @@ fn main() { "REWARDING_VALIDATOR_ADDRESS", "NYM_API", "NYXD_WS", - "EXPLORER_API", "NYM_VPN_API", ]; @@ -31,7 +30,7 @@ fn main() { for var in variables_to_track { // if script fails, debug with `cargo check -vv`` - println!("Looking for {}", var); + println!("Looking for {var}"); // read pattern that looks like: // : &str = "" @@ -42,7 +41,7 @@ fn main() { .captures(source_of_truth) .and_then(|caps| caps.get(1).map(|match_| match_.as_str().to_string())) .expect("Couldn't find var in source file"); - println!("Storing {}={}", var, value); + println!("Storing {var}={value}"); replace_with.insert(var, value); } @@ -58,13 +57,11 @@ fn main() { // = let re = Regex::new(&pattern).unwrap(); contents = re - .replace(&contents, |_: ®ex::Captures| { - format!(r#"{}={}"#, var, value) - }) + .replace(&contents, |_: ®ex::Captures| format!(r#"{var}={value}"#)) .to_string(); } - println!("File contents to write:\n{}", contents); + println!("File contents to write:\n{contents}"); if output_path.exists() { fs::write(output_path, contents).unwrap(); } else { diff --git a/common/network-defaults/src/constants.rs b/common/network-defaults/src/constants.rs index e9b70f54e3..1153fd902d 100644 --- a/common/network-defaults/src/constants.rs +++ b/common/network-defaults/src/constants.rs @@ -47,7 +47,8 @@ pub mod nyx { pub mod wireguard { use std::net::{Ipv4Addr, Ipv6Addr}; - pub const WG_PORT: u16 = 51822; + pub const WG_TUNNEL_PORT: u16 = 51822; + pub const WG_METADATA_PORT: u16 = 51830; // The interface used to route traffic pub const WG_TUN_BASE_NAME: &str = "nymwg"; diff --git a/common/network-defaults/src/env_setup.rs b/common/network-defaults/src/env_setup.rs index 4ab07259a7..fb67056387 100644 --- a/common/network-defaults/src/env_setup.rs +++ b/common/network-defaults/src/env_setup.rs @@ -25,7 +25,7 @@ fn print_env_vars_with_keys_in_file + Copy>(config_env_file: P) { .expect("Invalid path to environment configuration file"); for item in items { let (key, val) = item.expect("Invalid item in environment configuration file"); - log::debug!("{}: {}", key, val); + log::debug!("{key}: {val}"); } } diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 2b54d17406..ab597906c2 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "network")] -use crate::{DenomDetails, ValidatorDetails}; +use crate::{ApiUrlConst, DenomDetails, ValidatorDetails}; pub const NETWORK_NAME: &str = "mainnet"; @@ -17,6 +17,11 @@ pub const MIXNET_CONTRACT_ADDRESS: &str = "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; + +// \/ TODO: this has to be updated once the contract is deployed +pub const PERFORMANCE_CONTRACT_ADDRESS: &str = ""; +// /\ TODO: this has to be updated once the contract is deployed + pub const ECASH_CONTRACT_ADDRESS: &str = "n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun"; pub const GROUP_CONTRACT_ADDRESS: &str = @@ -29,10 +34,37 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub const NYXD_URL: &str = "https://rpc.nymtech.net"; -pub const NYM_API: &str = "https://validator.nymtech.net/api/"; pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket"; -pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; + +pub const NYM_API: &str = "https://validator.nymtech.net/api/"; +#[cfg(feature = "network")] +pub const NYM_APIS: &[ApiUrlConst] = &[ + ApiUrlConst { + url: NYM_API, + front_hosts: None, + }, + ApiUrlConst { + url: "https://nym-fronntdoor.vercel.app/api/", + front_hosts: Some(&["vercel.app", "vercel.com"]), + }, + ApiUrlConst { + url: "https://nym-frontdoor.global.ssl.fastly.net/api/", + front_hosts: Some(&["yelp.global.ssl.fastly.net"]), + }, +]; + pub const NYM_VPN_API: &str = "https://nymvpn.com/api/"; +#[cfg(feature = "network")] +pub const NYM_VPN_APIS: &[ApiUrlConst] = &[ + ApiUrlConst { + url: NYM_VPN_API, + front_hosts: Some(&["vercel.app", "vercel.com"]), + }, + ApiUrlConst { + url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/", + front_hosts: Some(&["yelp.global.ssl.fastly.net"]), + }, +]; // I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging pub const EXIT_POLICY_URL: &str = @@ -123,7 +155,6 @@ pub fn export_to_env() { set_var_to_default(var_names::NYXD, NYXD_URL); set_var_to_default(var_names::NYM_API, NYM_API); set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); - set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API); } @@ -165,6 +196,5 @@ pub fn export_to_env_if_not_set() { set_var_conditionally_to_default(var_names::NYXD, NYXD_URL); set_var_conditionally_to_default(var_names::NYM_API, NYM_API); set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS); - set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API); set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL); } diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index 8cebe53340..e8fd8fcfa8 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -20,6 +20,8 @@ pub struct ChainDetails { pub struct NymContracts { pub mixnet_contract_address: Option, pub vesting_contract_address: Option, + #[serde(default)] + pub performance_contract_address: Option, pub ecash_contract_address: Option, pub group_contract_address: Option, pub multisig_contract_address: Option, @@ -35,8 +37,38 @@ pub struct NymNetworkDetails { pub chain_details: ChainDetails, pub endpoints: Vec, pub contracts: NymContracts, - pub explorer_api: Option, pub nym_vpn_api_url: Option, + pub nym_api_urls: Option>, + pub nym_vpn_api_urls: Option>, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub struct ApiUrl { + /// Expects a string formatted Url + /// + /// see https://docs.rs/url/latest/url/struct.Url.html + pub url: String, + /// Optional alternative equivalent hostnames. Each entry must parse as valid Host + /// + /// see https://docs.rs/url/latest/url/enum.Host.html + pub front_hosts: Option>, +} + +pub struct ApiUrlConst<'a> { + pub url: &'a str, + pub front_hosts: Option<&'a [&'a str]>, +} + +impl From> for ApiUrl { + fn from(value: ApiUrlConst) -> Self { + ApiUrl { + url: value.url.to_string(), + front_hosts: value + .front_hosts + .map(|slice| slice.iter().map(|s| s.to_string()).collect()), + } + } } // by default we assume the same defaults as mainnet, i.e. same prefixes and denoms @@ -65,8 +97,9 @@ impl NymNetworkDetails { }, endpoints: Default::default(), contracts: Default::default(), - explorer_api: Default::default(), nym_vpn_api_url: Default::default(), + nym_api_urls: Default::default(), + nym_vpn_api_urls: Default::default(), } } @@ -86,7 +119,7 @@ impl NymNetworkDetails { } } Err(VarError::NotPresent) => None, - err => panic!("Unable to set: {:?}", err), + err => panic!("Unable to set: {err:?}"), } } @@ -124,7 +157,6 @@ impl NymNetworkDetails { .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS)) .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS)) .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS)) - .with_explorer_api(get_optional_env(var_names::EXPLORER_API)) .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API)) } @@ -145,6 +177,9 @@ impl NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), + performance_contract_address: parse_optional_str( + mainnet::PERFORMANCE_CONTRACT_ADDRESS, + ), ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), @@ -152,8 +187,9 @@ impl NymNetworkDetails { mainnet::COCONUT_DKG_CONTRACT_ADDRESS, ), }, - explorer_api: parse_optional_str(mainnet::EXPLORER_API), nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), + nym_api_urls: None, + nym_vpn_api_urls: None, } } @@ -193,7 +229,6 @@ impl NymNetworkDetails { set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address); set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address); - set_optional_var(var_names::EXPLORER_API, self.explorer_api); set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url); } @@ -297,12 +332,6 @@ impl NymNetworkDetails { self } - #[must_use] - pub fn with_explorer_api>(mut self, endpoint: Option) -> Self { - self.explorer_api = endpoint.map(Into::into); - self - } - #[must_use] pub fn with_nym_vpn_api_url>(mut self, endpoint: Option) -> Self { self.nym_vpn_api_url = endpoint.map(Into::into); diff --git a/nym-browser-extension/CHANGELOG.md b/common/network-defaults/src/sandbox.rs similarity index 100% rename from nym-browser-extension/CHANGELOG.md rename to common/network-defaults/src/sandbox.rs diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index bf92b95799..5bcebe85eb 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -22,7 +22,6 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const NYXD: &str = "NYXD"; pub const NYM_API: &str = "NYM_API"; pub const NYXD_WEBSOCKET: &str = "NYXD_WS"; -pub const EXPLORER_API: &str = "EXPLORER_API"; pub const EXIT_POLICY_URL: &str = "EXIT_POLICY"; pub const NYM_VPN_API: &str = "NYM_VPN_API"; pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER"; diff --git a/common/nym-id/src/error.rs b/common/nym-id/src/error.rs index 9ccf73267b..b76afa47cf 100644 --- a/common/nym-id/src/error.rs +++ b/common/nym-id/src/error.rs @@ -25,8 +25,8 @@ pub enum NymIdError { #[error("attempted to import an expired credential (it expired on {expiration})")] ExpiredCredentialImport { expiration: Date }, - #[error("could not import ticketbook expiring at {date} since we do not have corresponding expiration date signatures")] - MissingExpirationDateSignatures { date: Date }, + #[error("could not import ticketbook expiring at {date} for epoch {epoch_id} since we do not have corresponding expiration date signatures")] + MissingExpirationDateSignatures { date: Date, epoch_id: u64 }, #[error("could not import ticketbook for epoch {epoch_id} since we do not have corresponding coin index signatures")] MissingCoinIndexSignatures { epoch_id: u64 }, diff --git a/common/nym-id/src/import_credential/helpers.rs b/common/nym-id/src/import_credential/helpers.rs index dec6228d79..e5caa1e181 100644 --- a/common/nym-id/src/import_credential/helpers.rs +++ b/common/nym-id/src/import_credential/helpers.rs @@ -99,7 +99,7 @@ where // in order to import the ticketbook we MUST have the appropriate signatures in the storage already if credentials_store - .get_expiration_date_signatures(ticketbook.expiration_date()) + .get_expiration_date_signatures(ticketbook.expiration_date(), ticketbook.epoch_id()) .await .map_err(|source| NymIdError::StorageError { source: Box::new(source), @@ -108,6 +108,7 @@ where { return Err(NymIdError::MissingExpirationDateSignatures { date: ticketbook.expiration_date(), + epoch_id: ticketbook.epoch_id(), }); } diff --git a/common/nym-metrics/src/lib.rs b/common/nym-metrics/src/lib.rs index dbe950e48a..8694ea73d1 100644 --- a/common/nym-metrics/src/lib.rs +++ b/common/nym-metrics/src/lib.rs @@ -344,9 +344,9 @@ impl fmt::Display for MetricsController { let metrics = self.gather(); let output = match String::from_utf8(metrics) { Ok(output) => output, - Err(e) => return write!(f, "Error decoding metrics to String: {}", e), + Err(e) => return write!(f, "Error decoding metrics to String: {e}"), }; - write!(f, "{}", output) + write!(f, "{output}") } } @@ -597,7 +597,7 @@ mod tests { assert_eq!(literal, "nym_metrics_foo"); let bar = "bar"; - let format = format!("foomp_{}", bar); + let format = format!("foomp_{bar}"); let formatted = prepend_package_name!(format); assert_eq!(formatted, "nym_metrics_foomp_bar"); } diff --git a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs index 30c7fe5523..c98047570e 100644 --- a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs +++ b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs @@ -179,7 +179,7 @@ fn _aggregate_indices_signatures( validate_shares: bool, ) -> Result> where - B: Borrow, + B: Borrow + Send + Sync, { // Check if all indices are unique if signatures_shares @@ -271,7 +271,7 @@ pub fn aggregate_indices_signatures( signatures_shares: &[CoinIndexSignatureShare], ) -> Result> where - B: Borrow, + B: Borrow + Send + Sync, { _aggregate_indices_signatures(params, vk, signatures_shares, true) } diff --git a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs index ca0078b012..c475943b7b 100644 --- a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs +++ b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs @@ -38,7 +38,7 @@ impl From for ExpirationDateSignature { pub struct ExpirationDateSignatureShare where - B: Borrow, + B: Borrow + Send + Sync, { pub index: SignerIndex, pub key: VerificationKeyAuth, @@ -205,7 +205,7 @@ fn _aggregate_expiration_signatures( validate_shares: bool, ) -> Result> where - B: Borrow, + B: Borrow + Send + Sync, { // Check if all indices are unique if signatures_shares @@ -304,7 +304,7 @@ pub fn aggregate_expiration_signatures( signatures_shares: &[ExpirationDateSignatureShare], ) -> Result> where - B: Borrow, + B: Borrow + Send + Sync, { _aggregate_expiration_signatures(vk, expiration_date, signatures_shares, true) } diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs index 38489e28c9..06e51e151a 100644 --- a/common/nym_offline_compact_ecash/src/scheme/identify.rs +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -319,9 +319,9 @@ mod tests { let sk = grp.random_scalar(); let sk_user = SecretKeyUser { sk }; let pk_user = sk_user.public_key(); - public_keys.push(pk_user.clone()); + public_keys.push(pk_user); } - public_keys.push(user_keypair.public_key().clone()); + public_keys.push(user_keypair.public_key()); let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); @@ -462,9 +462,9 @@ mod tests { let sk = grp.random_scalar(); let sk_user = SecretKeyUser { sk }; let pk_user = sk_user.public_key(); - public_keys.push(pk_user.clone()); + public_keys.push(pk_user); } - public_keys.push(user_keypair.public_key().clone()); + public_keys.push(user_keypair.public_key()); let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs index 8db1597e61..faa5db6b59 100644 --- a/common/nym_offline_compact_ecash/src/scheme/keygen.rs +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -401,7 +401,7 @@ impl Bytable for SecretKeyUser { impl Base58 for SecretKeyUser {} -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] pub struct PublicKeyUser { pub(crate) pk: G1Projective, } @@ -554,7 +554,7 @@ impl KeyPairUser { } pub fn public_key(&self) -> PublicKeyUser { - self.public_key.clone() + self.public_key } pub fn to_bytes(&self) -> Vec { diff --git a/common/nym_offline_compact_ecash/src/scheme/setup.rs b/common/nym_offline_compact_ecash/src/scheme/setup.rs index dd469fb009..349706fe47 100644 --- a/common/nym_offline_compact_ecash/src/scheme/setup.rs +++ b/common/nym_offline_compact_ecash/src/scheme/setup.rs @@ -26,7 +26,7 @@ impl GroupParameters { pub fn new(attributes: usize) -> GroupParameters { assert!(attributes > 0); let gammas = (1..=attributes) - .map(|i| hash_g1(format!("gamma{}", i))) + .map(|i| hash_g1(format!("gamma{i}"))) .collect(); let delta = hash_g1("delta"); diff --git a/common/nymnoise/Cargo.toml b/common/nymnoise/Cargo.toml new file mode 100644 index 0000000000..63050814ef --- /dev/null +++ b/common/nymnoise/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "nym-noise" +version = "0.1.0" +authors = ["Simon Wicky "] +edition = "2021" +license.workspace = true + +[dependencies] +arc-swap = { workspace = true } +bytes = { workspace = true } +futures = { workspace = true } +tracing = { workspace = true } +pin-project = { workspace = true } +sha2 = { workspace = true } +snow = { workspace = true } +strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["net", "io-util", "time"] } +tokio-util = { workspace = true, features = ["codec"] } + +# internal +nym-crypto = { path = "../crypto" } +nym-noise-keys = { path = "keys" } + +[dev-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["full"] } +rand_chacha = { workspace = true } +nym-crypto = { path = "../crypto", features = ["rand"] } +nym-test-utils = { path = "../test-utils" } + + +[lints] +workspace = true diff --git a/common/nymnoise/keys/Cargo.toml b/common/nymnoise/keys/Cargo.toml new file mode 100644 index 0000000000..94080a004b --- /dev/null +++ b/common/nymnoise/keys/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nym-noise-keys" +version = "0.1.0" +authors = ["Simon Wicky "] +edition = "2021" +license.workspace = true + +[dependencies] +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true, features = ["derive"] } +utoipa = { workspace = true } + +# internal +nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/nymnoise/keys/src/lib.rs b/common/nymnoise/keys/src/lib.rs new file mode 100644 index 0000000000..b4cd70148b --- /dev/null +++ b/common/nymnoise/keys/src/lib.rs @@ -0,0 +1,43 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_crypto::asymmetric::x25519; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(from = "u8", into = "u8")] +pub enum NoiseVersion { + V1, + Unknown(u8), //Implies a newer version we don't know +} + +impl From for NoiseVersion { + fn from(value: u8) -> Self { + match value { + 1 => NoiseVersion::V1, + other => NoiseVersion::Unknown(other), + } + } +} + +impl From for u8 { + fn from(version: NoiseVersion) -> Self { + match version { + NoiseVersion::V1 => 1, + NoiseVersion::Unknown(other) => other, + } + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +pub struct VersionedNoiseKey { + #[schemars(with = "u8")] + #[schema(value_type = u8)] + pub supported_version: NoiseVersion, + + #[schemars(with = "String")] + #[serde(with = "bs58_x25519_pubkey")] + #[schema(value_type = String)] + pub x25519_pubkey: x25519::PublicKey, +} diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs new file mode 100644 index 0000000000..6f5e087d6c --- /dev/null +++ b/common/nymnoise/src/config.rs @@ -0,0 +1,172 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + sync::Arc, + time::Duration, +}; + +use arc_swap::ArcSwap; +use nym_crypto::asymmetric::x25519; +use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; +use snow::params::NoiseParams; + +use strum_macros::{EnumIter, FromRepr}; + +#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)] +#[repr(u8)] +#[non_exhaustive] +pub enum NoisePattern { + #[default] + XKpsk3 = 1, + IKpsk2 = 2, +} + +impl NoisePattern { + pub(crate) const fn as_str(&self) -> &'static str { + match self { + Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256", + Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though) + } + } + + // SAFETY: we have tests to ensure that hardcoded pattern are correct + #[allow(clippy::unwrap_used)] + pub(crate) fn psk_position(&self) -> u8 { + //automatic parsing, works for correct pattern, more convenient + match self.as_str().find("psk") { + Some(n) => { + let psk_index = n + 3; + let psk_char = self.as_str().chars().nth(psk_index).unwrap(); + psk_char.to_string().parse().unwrap() + } + None => 0, + } + } + + // SAFETY : we have tests to ensure that hardcoded pattern are correct + #[allow(clippy::unwrap_used)] + pub(crate) fn as_noise_params(&self) -> NoiseParams { + self.as_str().parse().unwrap() + } +} + +#[derive(Debug, Default)] +struct SocketAddrToKey { + inner: ArcSwap>, +} + +// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise +#[derive(Debug, Default)] +struct IpAddrToVersion { + inner: ArcSwap>, +} + +#[derive(Debug, Clone, Default)] +pub struct NoiseNetworkView { + keys: Arc, + support: Arc, +} + +impl NoiseNetworkView { + pub fn new_empty() -> Self { + NoiseNetworkView { + keys: Default::default(), + support: Default::default(), + } + } + + pub fn swap_view(&self, new: HashMap) { + let noise_support = new + .iter() + .map(|(s_addr, key)| (s_addr.ip(), key.supported_version)) + .collect::>(); + self.keys.inner.store(Arc::new(new)); + self.support.inner.store(Arc::new(noise_support)); + } +} + +#[derive(Clone)] +pub struct NoiseConfig { + network: NoiseNetworkView, + + pub(crate) local_key: Arc, + pub(crate) pattern: NoisePattern, + pub(crate) timeout: Duration, + + pub(crate) unsafe_disabled: bool, // allows for nodes to not attempt to do a noise handshake, VERY UNSAFE, FOR DEBUG PURPOSE ONLY +} + +impl NoiseConfig { + pub fn new( + noise_key: Arc, + network: NoiseNetworkView, + timeout: Duration, + ) -> Self { + NoiseConfig { + network, + local_key: noise_key, + pattern: Default::default(), + timeout, + unsafe_disabled: false, + } + } + + #[must_use] + pub fn with_noise_pattern(mut self, pattern: NoisePattern) -> Self { + self.pattern = pattern; + self + } + + #[must_use] + pub fn with_unsafe_disabled(mut self, disabled: bool) -> Self { + self.unsafe_disabled = disabled; + self + } + + pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option { + self.network.keys.inner.load().get(s_address).copied() + } + + // Only for phased update + //SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other. + // This in only for the progressive update though and there is no workaround + pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option { + let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied(); + + // SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that + let canonical_ip = &ip_addr.to_canonical(); + let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied(); + plain_ip_support.or(canonical_ip_support) + } +} + +#[cfg(test)] +mod tests { + use snow::params::NoiseParams; + + use super::NoisePattern; + use std::str::FromStr; + use strum::IntoEnumIterator; + + // The goal of these is to make sure every NoisePatterns are correct and unwrap can be used on them + + #[test] + fn noise_patterns_are_valid() { + for pattern in NoisePattern::iter() { + assert!(NoiseParams::from_str(pattern.as_str()).is_ok()) + } + } + + #[test] + fn noise_patterns_psk_position_is_valid() { + for pattern in NoisePattern::iter() { + match pattern { + NoisePattern::XKpsk3 => assert_eq!(pattern.psk_position(), 3), + NoisePattern::IKpsk2 => assert_eq!(pattern.psk_position(), 2), + } + } + } +} diff --git a/common/nymnoise/src/connection.rs b/common/nymnoise/src/connection.rs new file mode 100644 index 0000000000..81daa9bc98 --- /dev/null +++ b/common/nymnoise/src/connection.rs @@ -0,0 +1,67 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::io; + +use pin_project::pin_project; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::stream::NoiseStream; + +//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream +#[pin_project(project = ConnectionProj)] +pub enum Connection { + Raw(#[pin] C), + Noise(#[pin] Box>), +} + +impl AsyncRead for Connection +where + C: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_read(cx, buf), + ConnectionProj::Raw(stream) => stream.poll_read(cx, buf), + } + } +} + +impl AsyncWrite for Connection +where + C: AsyncWrite + AsyncRead + Unpin, +{ + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_write(cx, buf), + ConnectionProj::Raw(stream) => stream.poll_write(cx, buf), + } + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_flush(cx), + ConnectionProj::Raw(stream) => stream.poll_flush(cx), + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_shutdown(cx), + ConnectionProj::Raw(stream) => stream.poll_shutdown(cx), + } + } +} diff --git a/common/nymnoise/src/error.rs b/common/nymnoise/src/error.rs new file mode 100644 index 0000000000..675461e983 --- /dev/null +++ b/common/nymnoise/src/error.rs @@ -0,0 +1,91 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_noise_keys::NoiseVersion; +use snow::Error; +use std::io; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NoiseError { + #[error("encountered a Noise decryption error")] + DecryptionError, + + #[error("encountered a Noise Protocol error: {0}")] + ProtocolError(Error), + + #[error("encountered an IO error: {0}")] + IoError(#[from] io::Error), + + #[error("Incorrect state")] + IncorrectStateError, + + #[error("Handshake did not complete")] + HandshakeError, + + #[error("unknown noise version (encoded value: {encoded})")] + UnknownVersion { encoded: u8 }, + + #[error("unknown noise pattern (encoded value: {encoded})")] + UnknownPattern { encoded: u8 }, + + #[error("unknown noise message type (encoded value: {encoded})")] + UnknownMessageType { encoded: u8 }, + + #[error("failed to generate psk for requested version {noise_version}")] + PskGenerationFailure { noise_version: u8 }, + + #[error("noise initiator attempted to use version v{noise_version} of the protocol - we don't know how to handle it")] + UnknownVersionHandshake { noise_version: u8 }, + + #[error("noise initiator attempted to use an unexpected noise pattern. we're configured for {configured} while it requested {received}")] + UnexpectedNoisePattern { + configured: &'static str, + received: &'static str, + }, + + #[error("handshake version has unexpectedly changed. initial was {initial:?} and received {received:?}")] + UnexpectedHandshakeVersion { + initial: NoiseVersion, + received: NoiseVersion, + }, + + #[error("data packet version has unexpectedly changed. initial was {initial:?} and received {received:?}")] + UnexpectedDataVersion { + initial: NoiseVersion, + received: NoiseVersion, + }, + + #[error("received a non-handshake message during noise handshake")] + NonHandshakeMessageReceived, + + #[error("received a non-data message post noise handshake")] + NonDataMessageReceived, + + #[error("handshake message exceeded maximum size (got {size} bytes)")] + HandshakeTooBig { size: usize }, + + #[error("noise message exceeded maximum size (got {size} bytes)")] + DataTooBig { size: usize }, + + #[error("Handshake timeout")] + HandshakeTimeout(#[from] tokio::time::error::Elapsed), +} + +impl NoiseError { + pub(crate) fn naive_to_io_error(self) -> std::io::Error { + match self { + NoiseError::IoError(err) => err, + other => std::io::Error::other(other), + } + } +} + +impl From for NoiseError { + fn from(err: Error) -> Self { + match err { + Error::Decrypt => NoiseError::DecryptionError, + err => NoiseError::ProtocolError(err), + } + } +} diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs new file mode 100644 index 0000000000..5d3ff953cb --- /dev/null +++ b/common/nymnoise/src/lib.rs @@ -0,0 +1,118 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_noise_keys::NoiseVersion; +use snow::error::Prerequisite; +use snow::Error; +use tokio::net::TcpStream; +use tracing::{error, warn}; + +pub mod config; +pub mod connection; +pub mod error; +pub mod stream; + +use crate::config::NoiseConfig; +use crate::connection::Connection; +use crate::error::NoiseError; +use crate::stream::NoiseStreamBuilder; + +const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ"; + +pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1; + +// TODO: this should be behind some trait because presumably, depending on the version, +// other arguments would be needed +mod psk_gen { + use crate::error::NoiseError; + use crate::stream::Psk; + use crate::NOISE_PSK_PREFIX; + use nym_crypto::asymmetric::x25519; + use nym_noise_keys::NoiseVersion; + use sha2::{Digest, Sha256}; + + pub(crate) fn generate_psk( + responder_pub_key: x25519::PublicKey, + version: NoiseVersion, + ) -> Result { + match version { + NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)), + NoiseVersion::Unknown(noise_version) => { + Err(NoiseError::PskGenerationFailure { noise_version }) + } + } + } + + fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(NOISE_PSK_PREFIX); + hasher.update(responder_pub_key.to_bytes()); + hasher.finalize().into() + } +} + +pub async fn upgrade_noise_initiator( + conn: TcpStream, + config: &NoiseConfig, +) -> Result, NoiseError> { + if config.unsafe_disabled { + warn!("Noise is disabled in the config. Not attempting any handshake"); + return Ok(Connection::Raw(conn)); + } + + //Get init material + let responder_addr = conn.peer_addr().map_err(|err| { + error!("Unable to extract peer address from connection - {err}"); + Error::Prereq(Prerequisite::RemotePublicKey) + })?; + + let Some(key) = config.get_noise_key(&responder_addr) else { + warn!("{responder_addr} can't speak Noise yet, falling back to TCP"); + return Ok(Connection::Raw(conn)); + }; + + let handshake_version = match key.supported_version { + NoiseVersion::V1 => NoiseVersion::V1, + + // We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails. + // If that node sees we're older, it will try to adapt too. + NoiseVersion::Unknown(version) => { + warn!("{responder_addr} is announcing an v{version} version of Noise that we don't know how to parse, we will attempt to downgrade to our current highest supported version"); + LATEST_NOISE_VERSION + } + }; + + NoiseStreamBuilder::new(conn) + .perform_initiator_handshake(config, handshake_version, key.x25519_pubkey) + .await + .map(|stream| Connection::Noise(Box::new(stream))) +} +pub async fn upgrade_noise_responder( + conn: TcpStream, + config: &NoiseConfig, +) -> Result, NoiseError> { + if config.unsafe_disabled { + warn!("Noise is disabled in the config. Not attempting any handshake"); + return Ok(Connection::Raw(conn)); + } + + //Get init material + let initiator_addr = match conn.peer_addr() { + Ok(addr) => addr, + Err(err) => { + error!("Unable to extract peer address from connection - {err}"); + return Err(Error::Prereq(Prerequisite::RemotePublicKey).into()); + } + }; + + // if responder doesn't announce noise support, we fallback to tcp + if config.get_noise_support(initiator_addr.ip()).is_none() { + warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",); + return Ok(Connection::Raw(conn)); + }; + + NoiseStreamBuilder::new(conn) + .perform_responder_handshake(config) + .await + .map(|stream| Connection::Noise(Box::new(stream))) +} diff --git a/common/nymnoise/src/stream/codec.rs b/common/nymnoise/src/stream/codec.rs new file mode 100644 index 0000000000..6c075b56a5 --- /dev/null +++ b/common/nymnoise/src/stream/codec.rs @@ -0,0 +1,92 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::NoiseError; +use crate::stream::framing::{NymNoiseFrame, NymNoiseHeader}; +use bytes::{BufMut, BytesMut}; +use tokio_util::codec::{Decoder, Encoder}; + +#[derive(Debug, Clone, Copy)] +enum DecodeState { + Header, + Payload(NymNoiseHeader), +} + +pub struct NymNoiseCodec { + state: DecodeState, +} + +impl NymNoiseCodec { + pub fn new() -> Self { + NymNoiseCodec { + state: DecodeState::Header, + } + } + + fn decode_header(&self, src: &mut BytesMut) -> Result, NoiseError> { + if src.len() < NymNoiseHeader::SIZE { + // Not enough data + return Ok(None); + } + + // note: successful call to 'decode' advances the buffer by NymNoiseHeader::SIZE + let Some(header) = NymNoiseHeader::decode(src)? else { + return Ok(None); + }; + + Ok(Some(header)) + } + + fn decode_data(&self, n: usize, src: &mut BytesMut) -> Option { + // At this point, the buffer has already had the required capacity + // reserved. All there is to do is read. + if src.len() < n { + return None; + } + + Some(src.split_to(n)) + } +} + +impl Decoder for NymNoiseCodec { + type Item = NymNoiseFrame; + type Error = NoiseError; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + let header = match self.state { + DecodeState::Header => match self.decode_header(src)? { + None => return Ok(None), + Some(header) => { + self.state = DecodeState::Payload(header); + header + } + }, + DecodeState::Payload(header) => header, + }; + + let Some(data) = self.decode_data(header.data_len as usize, src) else { + return Ok(None); + }; + + // Update the decode state + self.state = DecodeState::Header; + + // make sure the buffer has enough space to read the next header + src.reserve(NymNoiseHeader::SIZE); + + Ok(Some(NymNoiseFrame { + header, + data: data.freeze(), + })) + } +} + +impl Encoder for NymNoiseCodec { + type Error = NoiseError; + + fn encode(&mut self, frame: NymNoiseFrame, dst: &mut BytesMut) -> Result<(), Self::Error> { + frame.header.encode(dst); + dst.put_slice(frame.data.as_ref()); + Ok(()) + } +} diff --git a/common/nymnoise/src/stream/framing.rs b/common/nymnoise/src/stream/framing.rs new file mode 100644 index 0000000000..8c51daa39a --- /dev/null +++ b/common/nymnoise/src/stream/framing.rs @@ -0,0 +1,161 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::NoisePattern; +use crate::error::NoiseError; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use nym_noise_keys::NoiseVersion; +use strum_macros::FromRepr; + +#[derive(Debug)] +pub struct NymNoiseFrame { + pub header: NymNoiseHeader, + pub data: Bytes, +} + +impl NymNoiseFrame { + pub fn new_handshake_frame( + data: Bytes, + version: NoiseVersion, + pattern: NoisePattern, + ) -> Result { + if data.len() > u16::MAX as usize { + return Err(NoiseError::HandshakeTooBig { size: data.len() }); + } + + Ok(NymNoiseFrame { + header: NymNoiseHeader { + version, + noise_pattern: pattern, + message_type: NymNoiseMessageType::Handshake, + data_len: data.len() as u16, + }, + data, + }) + } + + pub fn new_data_frame( + data: Bytes, + version: NoiseVersion, + pattern: NoisePattern, + ) -> Result { + if data.len() > u16::MAX as usize { + return Err(NoiseError::HandshakeTooBig { size: data.len() }); + } + + Ok(NymNoiseFrame { + header: NymNoiseHeader { + version, + noise_pattern: pattern, + message_type: NymNoiseMessageType::Data, + data_len: data.len() as u16, + }, + data, + }) + } + + pub fn version(&self) -> NoiseVersion { + self.header.version + } + + pub fn is_handshake_message(&self) -> bool { + self.header.is_handshake_message() + } + + pub fn is_data_message(&self) -> bool { + self.header.is_data_message() + } + + pub fn noise_pattern(&self) -> NoisePattern { + self.header.noise_pattern + } +} + +#[derive(Debug, Copy, Clone, FromRepr)] +#[repr(u8)] +#[non_exhaustive] +pub enum NymNoiseMessageType { + Handshake = 0, + Data = 1, +} + +#[derive(Debug, Clone, Copy)] +pub struct NymNoiseHeader { + pub version: NoiseVersion, + pub noise_pattern: NoisePattern, + pub message_type: NymNoiseMessageType, + pub data_len: u16, +} + +impl NymNoiseHeader { + pub(crate) const SIZE: usize = 8; + + pub fn is_handshake_message(&self) -> bool { + matches!(self.message_type, NymNoiseMessageType::Handshake) + } + + pub fn is_data_message(&self) -> bool { + matches!(self.message_type, NymNoiseMessageType::Data) + } + + // 0 1 2 3 4 5 6 7 8 + // +-+-+-+-+-+-+-+-+ + // |V|P|T|Len| Res.| + // +-+-+-+-+-+-+-+-+ + pub(crate) fn encode(&self, dst: &mut BytesMut) { + dst.reserve(Self::SIZE); + + // byte 0 + dst.put_u8(self.version.into()); + + // byte 1 + dst.put_u8(self.noise_pattern as u8); + + // byte 2 + dst.put_u8(self.message_type as u8); + + // byte 3-4 + dst.put_u16(self.data_len); + + // byte 5-7 (RESERVED): + dst.extend_from_slice(&[0u8; 3]) + } + + pub(crate) fn decode(src: &mut BytesMut) -> Result, NoiseError> { + if src.len() < Self::SIZE { + // can't do anything if we don't have enough bytes - but reserve enough for the next call + src.reserve(Self::SIZE); + return Ok(None); + } + + let version = src.get_u8(); + let pattern = src.get_u8(); + let message_type = src.get_u8(); + let data_len = src.get_u16(); + + // reserved + src.advance(3); + + let version = NoiseVersion::from(version); + + // here, based on versions, we could do vary the further parsing + // match version { + // NoiseVersion::V1 => {} + // NoiseVersion::Unknown(_) => {} + // } + + let noise_pattern = NoisePattern::from_repr(pattern) + .ok_or(NoiseError::UnknownPattern { encoded: pattern })?; + let message_type = + NymNoiseMessageType::from_repr(message_type).ok_or(NoiseError::UnknownMessageType { + encoded: message_type, + })?; + + Ok(Some(NymNoiseHeader { + version, + noise_pattern, + message_type, + data_len, + })) + } +} diff --git a/common/nymnoise/src/stream/mod.rs b/common/nymnoise/src/stream/mod.rs new file mode 100644 index 0000000000..e860ef46e0 --- /dev/null +++ b/common/nymnoise/src/stream/mod.rs @@ -0,0 +1,475 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::{NoiseConfig, NoisePattern}; +use crate::error::NoiseError; +use crate::psk_gen::generate_psk; +use crate::stream::codec::NymNoiseCodec; +use crate::stream::framing::NymNoiseFrame; +use bytes::{Bytes, BytesMut}; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use nym_crypto::asymmetric::x25519; +use nym_noise_keys::NoiseVersion; +use snow::{Builder, HandshakeState, TransportState}; +use std::io; +use std::pin::Pin; +use std::task::Poll; +use std::{cmp::min, task::ready}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_util::codec::Framed; + +mod codec; +mod framing; + +const TAGLEN: usize = 16; +const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size + +pub(crate) type Psk = [u8; 32]; + +pub(crate) struct NoiseStreamBuilder { + inner_stream: Framed, +} + +impl NoiseStreamBuilder { + pub(crate) fn new(inner_stream: C) -> Self + where + C: AsyncRead + AsyncWrite, + { + NoiseStreamBuilder { + inner_stream: Framed::new(inner_stream, NymNoiseCodec::new()), + } + } + + async fn perform_initiator_handshake_inner( + self, + pattern: NoisePattern, + local_private_key: impl AsRef<[u8]>, + remote_pub_key: impl AsRef<[u8]>, + psk: Psk, + version: NoiseVersion, + ) -> Result, NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + let handshake = Builder::new(pattern.as_noise_params()) + .local_private_key(local_private_key.as_ref()) + .remote_public_key(remote_pub_key.as_ref()) + .psk(pattern.psk_position(), &psk) + .build_initiator()?; + + self.perform_handshake(handshake, version, pattern).await + } + + pub(crate) async fn perform_initiator_handshake( + self, + config: &NoiseConfig, + version: NoiseVersion, + remote_pub_key: x25519::PublicKey, + ) -> Result, NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + let psk = generate_psk(remote_pub_key, version)?; + + let timeout = config.timeout; + tokio::time::timeout( + timeout, + self.perform_initiator_handshake_inner( + config.pattern, + config.local_key.private_key(), + remote_pub_key, + psk, + version, + ), + ) + .await? + } + + async fn perform_responder_handshake_inner( + mut self, + noise_pattern: NoisePattern, + local_private_key: impl AsRef<[u8]>, + local_pub_key: x25519::PublicKey, + ) -> Result, NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + // 1. we read the first message from the initiator to establish noise version and pattern + // and determine if we can continue with the handshake + let initial_frame = self + .inner_stream + .next() + .await + .ok_or(NoiseError::IoError(io::ErrorKind::BrokenPipe.into()))??; + + if !initial_frame.is_handshake_message() { + return Err(NoiseError::NonHandshakeMessageReceived); + } + + let pattern = initial_frame.noise_pattern(); + + // I can imagine we should be able to handle multiple patterns here, but I guess there's a reason a value is set in the config + // but refactoring this shouldn't be too difficult + if pattern != noise_pattern { + return Err(NoiseError::UnexpectedNoisePattern { + configured: noise_pattern.as_str(), + received: pattern.as_str(), + }); + } + + // 2. generate psk and handshake state + let psk = generate_psk(local_pub_key, initial_frame.header.version)?; + + let mut handshake = Builder::new(pattern.as_noise_params()) + .local_private_key(local_private_key.as_ref()) + .psk(pattern.psk_position(), &psk) + .build_responder()?; + + // update handshake state with initial frame + let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); + handshake.read_message(&initial_frame.data, &mut buf)?; + + // 3. run handshake to completion + self.perform_handshake(handshake, initial_frame.version(), pattern) + .await + } + + pub(crate) async fn perform_responder_handshake( + self, + config: &NoiseConfig, + ) -> Result, NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + let timeout = config.timeout; + tokio::time::timeout( + timeout, + self.perform_responder_handshake_inner( + config.pattern, + config.local_key.private_key(), + *config.local_key.public_key(), + ), + ) + .await? + } + + async fn send_handshake_msg( + &mut self, + handshake: &mut HandshakeState, + version: NoiseVersion, + pattern: NoisePattern, + ) -> Result<(), NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer + let len = handshake.write_message(&[], &mut buf)?; + buf.truncate(len); + + let frame = NymNoiseFrame::new_handshake_frame(buf.freeze(), version, pattern)?; + self.inner_stream.send(frame).await?; + Ok(()) + } + + async fn recv_handshake_msg( + &mut self, + handshake: &mut HandshakeState, + version: NoiseVersion, + pattern: NoisePattern, + ) -> Result<(), NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + match self.inner_stream.next().await { + Some(Ok(frame)) => { + // validate the frame + if !frame.is_handshake_message() { + return Err(NoiseError::NonHandshakeMessageReceived); + } + if frame.version() != version { + return Err(NoiseError::UnexpectedHandshakeVersion { + initial: version, + received: frame.version(), + }); + } + if frame.noise_pattern() != pattern { + return Err(NoiseError::UnexpectedNoisePattern { + configured: pattern.as_str(), + received: frame.noise_pattern().as_str(), + }); + } + + let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer + handshake.read_message(&frame.data, &mut buf)?; + Ok(()) + } + Some(Err(err)) => Err(err), + None => Err(NoiseError::HandshakeError), + } + } + + async fn perform_handshake( + mut self, + mut handshake_state: HandshakeState, + version: NoiseVersion, + pattern: NoisePattern, + ) -> Result, NoiseError> + where + C: AsyncRead + AsyncWrite + Unpin, + { + while !handshake_state.is_handshake_finished() { + if handshake_state.is_my_turn() { + self.send_handshake_msg(&mut handshake_state, version, pattern) + .await?; + } else { + self.recv_handshake_msg(&mut handshake_state, version, pattern) + .await?; + } + } + + let transport = handshake_state.into_transport_mode()?; + Ok(NoiseStream { + inner_stream: self.inner_stream, + negotiated_pattern: pattern, + negotiated_version: version, + transport, + dec_buffer: Default::default(), + }) + } +} + +/// Wrapper around a TcpStream +pub struct NoiseStream { + inner_stream: Framed, + + negotiated_pattern: NoisePattern, + negotiated_version: NoiseVersion, + + transport: TransportState, + dec_buffer: BytesMut, +} + +impl NoiseStream { + fn validate_data_frame(&self, frame: NymNoiseFrame) -> Result { + if !frame.is_data_message() { + return Err(NoiseError::NonDataMessageReceived); + } + // validate the frame + if !frame.is_data_message() { + return Err(NoiseError::NonDataMessageReceived); + } + if frame.version() != self.negotiated_version { + return Err(NoiseError::UnexpectedDataVersion { + initial: self.negotiated_version, + received: frame.version(), + }); + } + if frame.noise_pattern() != self.negotiated_pattern { + return Err(NoiseError::UnexpectedNoisePattern { + configured: self.negotiated_pattern.as_str(), + received: frame.noise_pattern().as_str(), + }); + }; + + Ok(frame.data) + } + + fn poll_data_frame( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> Poll>> + where + C: AsyncRead + AsyncWrite + Unpin, + { + match ready!(Pin::new(&mut self.inner_stream).poll_next(cx)) { + None => Poll::Ready(None), + Some(Err(err)) => Poll::Ready(Some(Err(err.naive_to_io_error()))), + Some(Ok(frame)) => match self.validate_data_frame(frame) { + Err(err) => Poll::Ready(Some(Err(err.naive_to_io_error()))), + Ok(data) => Poll::Ready(Some(Ok(data))), + }, + } + } +} + +impl AsyncRead for NoiseStream +where + C: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let pending = match self.poll_data_frame(cx) { + Poll::Pending => { + //no new data, a return value of Poll::Pending means the waking is already scheduled + //Nothing new to decrypt, only check if we can return something from dec_storage, happens after + true + } + + Poll::Ready(Some(Ok(noise_msg))) => { + // We have a new noise msg + let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN); + + let len = match self.transport.read_message(&noise_msg, &mut dec_msg) { + Ok(len) => len, + Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())), + }; + + self.dec_buffer.extend(&dec_msg[..len]); + + false + } + + Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)), + + Poll::Ready(None) => { + //Stream is done, we might still have data in the buffer though, happens afterward + false + } + }; + + // Checking if there is something to return from the buffer + let read_len = min(buf.remaining(), self.dec_buffer.len()); + if read_len > 0 { + buf.put_slice(&self.dec_buffer.split_to(read_len)); + return Poll::Ready(Ok(())); + } + + // buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream + if pending { + //If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled + Poll::Pending + } else { + Poll::Ready(Ok(())) + } + } +} + +impl AsyncWrite for NoiseStream +where + C: AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + // returns on Poll::Pending and Poll:Ready(Err) + ready!(Pin::new(&mut self.inner_stream).poll_ready(cx)) + .map_err(|err| err.naive_to_io_error())?; + + // we can send at most u16::MAX bytes in a frame, but we also have to include the tag when encoding + let msg_len = min(u16::MAX as usize - TAGLEN, buf.len()); + + // Ready to send, encrypting message + let mut noise_buf = BytesMut::zeroed(msg_len + TAGLEN); + + let Ok(len) = self + .transport + .write_message(&buf[..msg_len], &mut noise_buf) + else { + return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())); + }; + noise_buf.truncate(len); + + let frame = NymNoiseFrame::new_data_frame( + noise_buf.freeze(), + self.negotiated_version, + self.negotiated_pattern, + ) + .map_err(|err| err.naive_to_io_error())?; + + // Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104 + match Pin::new(&mut self.inner_stream).start_send(frame) { + Ok(()) => Poll::Ready(Ok(msg_len)), + Err(e) => Poll::Ready(Err(e.naive_to_io_error())), + } + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner_stream) + .poll_flush(cx) + .map_err(|err| err.naive_to_io_error()) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut self.inner_stream) + .poll_close(cx) + .map_err(|err| err.naive_to_io_error()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::x25519; + use nym_test_utils::helpers::deterministic_rng; + use nym_test_utils::mocks::async_read_write::mock_io_streams; + use nym_test_utils::traits::{Timeboxed, TimeboxedSpawnable}; + use std::sync::Arc; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::join; + + #[tokio::test] + async fn noise_handshake() -> anyhow::Result<()> { + let mut rng = deterministic_rng(); + + let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng)); + let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng)); + + let (initiator_stream, responder_stream) = mock_io_streams(); + + let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?; + let pattern = NoisePattern::default(); + + let stream_initiator = NoiseStreamBuilder::new(initiator_stream) + .perform_initiator_handshake_inner( + pattern, + initiator_keys.private_key().to_bytes(), + responder_keys.public_key().to_bytes(), + psk, + NoiseVersion::V1, + ); + + let stream_responder = NoiseStreamBuilder::new(responder_stream) + .perform_responder_handshake_inner( + pattern, + responder_keys.private_key().to_bytes(), + *responder_keys.public_key(), + ); + + let initiator_fut = stream_initiator.spawn_timeboxed(); + let responder_fut = stream_responder.spawn_timeboxed(); + + let (initiator, responder) = join!(initiator_fut, responder_fut); + + let mut initiator = initiator???; + let mut responder = responder???; + + let msg = b"hello there"; + // if noise was successful we should be able to write a proper message across + initiator.write_all(msg).timeboxed().await??; + initiator.inner_stream.flush().await?; + + let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data(); + + let mut buf = [0u8; 11]; + responder.read(&mut buf).timeboxed().await??; + + assert_eq!(&buf[..], msg); + + // the inner content is different from the actual msg since it was encrypted + assert_ne!(inner_buf, buf); + assert_ne!(inner_buf.len(), msg.len()); + + Ok(()) + } +} diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index a82c9a4272..d6b77f6969 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -8,7 +8,7 @@ license = { workspace = true } repository = { workspace = true } [dependencies] -log = { workspace = true } +tracing = { workspace = true } rand = { workspace = true } rand_distr = { workspace = true } rand_chacha = { workspace = true } diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index 5837dd54d5..982ced55be 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -47,11 +47,17 @@ impl SurbAck { average_delay: time::Duration, topology: &NymRouteProvider, packet_type: PacketType, + disable_mix_hops: bool, ) -> Result where R: RngCore + CryptoRng, { - let route = topology.random_route_to_egress(rng, recipient.gateway())?; + let route = if disable_mix_hops { + topology.empty_route_to_egress(recipient.gateway())? + } else { + topology.random_route_to_egress(rng, recipient.gateway())? + }; + let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index 03ec4d94af..c7f756cb5c 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -16,3 +16,6 @@ thiserror = { workspace = true } [dev-dependencies] rand = { workspace = true } nym-crypto = { path = "../../crypto", features = ["rand"] } +bincode = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true, features = ["derive"] } \ No newline at end of file diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index 0b1bcf7623..aa475dc401 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -7,7 +7,7 @@ use crate::nodes::{NodeIdentity, NODE_IDENTITY_SIZE}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_sphinx_types::Destination; -use serde::de::{Error as SerdeError, Unexpected, Visitor}; +use serde::de::{Error as SerdeError, SeqAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{self, Formatter}; use std::str::FromStr; @@ -64,7 +64,7 @@ impl<'de> Deserialize<'de> for Recipient { { struct RecipientVisitor; - impl Visitor<'_> for RecipientVisitor { + impl<'de> Visitor<'de> for RecipientVisitor { type Value = Recipient; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { @@ -90,6 +90,42 @@ impl<'de> Deserialize<'de> for Recipient { ) }) } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + // if we know the size hint, check if it matches expectation, + // otherwise return an error + if let Some(size_hint) = seq.size_hint() { + if size_hint != Recipient::LEN { + return Err(SerdeError::invalid_length(size_hint, &self)); + } + } + + let mut recipient_bytes = [0u8; Recipient::LEN]; + + // clippy's suggestion is completely wrong and it iterates wrong sequence + #[allow(clippy::needless_range_loop)] + for i in 0..Recipient::LEN { + let Some(elem) = seq.next_element::()? else { + return Err(SerdeError::invalid_length(i + 1, &self)); + }; + recipient_bytes[i] = elem; + } + + // make sure there are no trailing bytes + if seq.next_element::()?.is_some() { + return Err(SerdeError::invalid_length(Recipient::LEN + 1, &self)); + } + + Recipient::try_from_bytes(recipient_bytes).map_err(|_| { + SerdeError::invalid_value( + Unexpected::Other("At least one of the curve points was malformed"), + &self, + ) + }) + } } deserializer.deserialize_bytes(RecipientVisitor) @@ -245,6 +281,18 @@ impl FromStr for Recipient { mod tests { use super::*; + fn mock_recipient() -> Recipient { + Recipient::try_from_bytes([ + 67, 5, 132, 146, 3, 236, 116, 89, 254, 57, 131, 159, 69, 181, 55, 208, 12, 108, 136, + 83, 58, 76, 171, 195, 31, 98, 92, 64, 68, 53, 156, 184, 100, 189, 73, 3, 238, 103, 156, + 108, 124, 199, 42, 79, 172, 98, 81, 177, 182, 100, 167, 164, 74, 183, 199, 213, 162, + 173, 102, 112, 30, 159, 148, 66, 44, 75, 230, 182, 138, 114, 170, 163, 209, 82, 204, + 100, 118, 91, 57, 150, 212, 147, 151, 135, 148, 16, 213, 223, 182, 164, 242, 37, 40, + 73, 137, 228, + ]) + .unwrap() + } + #[test] fn string_conversion_works() { let mut rng = rand::thread_rng(); @@ -308,4 +356,40 @@ mod tests { recovered_recipient.gateway.to_bytes() ); } + + // calls `visit_bytes` + #[test] + fn bincode_serialisation_works() { + let recipient = mock_recipient(); + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] + struct MyStruct { + recipient: Recipient, + } + let a = MyStruct { recipient }; + let s = bincode::serialize(&a).unwrap(); + + let b = bincode::deserialize(&s).unwrap(); + + assert_eq!(a, b); + } + + // calls `visit_seq` + #[test] + fn json_serialisation_works() { + use serde::{Deserialize, Serialize}; + + let recipient = mock_recipient(); + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] + struct MyStruct { + recipient: Recipient, + } + let a = MyStruct { recipient }; + let s = serde_json::to_string(&a).unwrap(); + + let b = serde_json::from_str(&s).unwrap(); + + assert_eq!(a, b); + } } diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index 3d4c8aadc7..ef9c74b73d 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -10,7 +10,6 @@ repository = { workspace = true } [dependencies] rand = { workspace = true } bs58 = { workspace = true } -serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } @@ -22,7 +21,7 @@ nym-sphinx-types = { path = "../types" } nym-topology = { path = "../../topology" } [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] -version = "0.2.95" +workspace = true [dev-dependencies] rand_chacha = { workspace = true } diff --git a/common/nymsphinx/anonymous-replies/src/lib.rs b/common/nymsphinx/anonymous-replies/src/lib.rs index 3e48d0ea1b..2c47a23a76 100644 --- a/common/nymsphinx/anonymous-replies/src/lib.rs +++ b/common/nymsphinx/anonymous-replies/src/lib.rs @@ -6,4 +6,4 @@ pub mod reply_surb; pub mod requests; pub use encryption_key::{SurbEncryptionKey, SurbEncryptionKeySize}; -pub use reply_surb::{ReplySurb, ReplySurbError}; +pub use reply_surb::{ReplySurb, ReplySurbError, ReplySurbWithKeyRotation}; diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 7f7da53805..6f3dd9137e 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -8,17 +8,13 @@ use nym_sphinx_addressing::nodes::{ NymNodeRoutingAddress, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN, }; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; -use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE; +use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, SphinxKeyRotation}; use nym_sphinx_types::{ NymPacket, SURBMaterial, SphinxError, HEADER_SIZE, NODE_ADDRESS_LENGTH, SURB, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION, }; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; -use serde::de::{Error as SerdeError, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::{self, Formatter}; use std::time::Duration; use thiserror::Error; @@ -49,44 +45,6 @@ pub struct ReplySurb { pub(crate) encryption_key: SurbEncryptionKey, } -// Serialize + Deserialize is not really used anymore (it was for a CBOR experiment) -// however, if we decided we needed it again, it's already here -impl Serialize for ReplySurb { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_bytes(&self.to_bytes()) - } -} - -impl<'de> Deserialize<'de> for ReplySurb { - fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - struct ReplySurbVisitor; - - impl Visitor<'_> for ReplySurbVisitor { - type Value = ReplySurb; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - write!(formatter, "A replySURB must contain a valid symmetric encryption key and a correctly formed sphinx header") - } - - fn visit_bytes(self, bytes: &[u8]) -> Result - where - E: SerdeError, - { - ReplySurb::from_bytes(bytes) - .map_err(|_| SerdeError::invalid_length(bytes.len(), &self)) - } - } - - deserializer.deserialize_bytes(ReplySurbVisitor) - } -} - impl ReplySurb { /// base overhead of a reply surb that exists regardless of type or number of key materials. pub(crate) const BASE_OVERHEAD: usize = @@ -98,6 +56,13 @@ impl ReplySurb { packet_size.plaintext_size() - ack_overhead - ReplySurbKeyDigestAlgorithm::output_size() - 1 } + /// Construct a ResplySurb object. Selects mix hops for the surb unique to this + /// individual construction. + /// + /// If mix hops are disabled, the route will consistency of the recipient + /// (i.e. the ingress hop) only. When `disable_mix_hops` is enabled + /// `use_legacy_surb_format` is ignored as disabled mix hops requires use of + /// the updated SURB format. // TODO: should this return `ReplySURBError` for consistency sake // or keep `NymTopologyError` because it's the only error it can actually return? pub fn construct( @@ -106,16 +71,21 @@ impl ReplySurb { average_delay: Duration, use_legacy_surb_format: bool, topology: &NymRouteProvider, + disable_mix_hops: bool, ) -> Result where R: RngCore + CryptoRng, { - let route = topology.random_route_to_egress(rng, recipient.gateway())?; + let route = if disable_mix_hops { + topology.empty_route_to_egress(recipient.gateway())? + } else { + topology.random_route_to_egress(rng, recipient.gateway())? + }; let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); let mut surb_material = SURBMaterial::new(route, delays, destination); - if use_legacy_surb_format { + if use_legacy_surb_format && !disable_mix_hops { surb_material = surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION) } @@ -123,15 +93,10 @@ impl ReplySurb { Ok(ReplySurb { surb: surb_material.construct_SURB().unwrap(), encryption_key: SurbEncryptionKey::new(rng), + // used_key_rotation: SphinxKeyRotation::from(topology.current_key_rotation()), }) } - /// Returns the expected number of bytes the [`ReplySURB`] will take after serialization using the new encoding format. - /// Useful for deserialization from a bytes stream. - pub fn v2_serialised_len(num_hops: u8) -> usize { - Self::BASE_OVERHEAD + num_hops as usize * PAYLOAD_KEY_SEED_SIZE - } - pub fn encryption_key(&self) -> &SurbEncryptionKey { &self.encryption_key } @@ -204,8 +169,75 @@ impl ReplySurb { .use_surb(message_bytes, packet_size.payload_size()) .expect("this error indicates inconsistent message length checking - it shouldn't have happened!"); - let first_hop_address = NymNodeRoutingAddress::try_from(first_hop).unwrap(); + let first_hop_address = NymNodeRoutingAddress::try_from(first_hop)?; Ok((NymPacket::Sphinx(packet), first_hop_address)) } + + pub fn to_legacy(self) -> ReplySurbWithKeyRotation { + self.with_key_rotation(SphinxKeyRotation::Unknown) + } + + pub fn with_key_rotation(self, key_rotation: SphinxKeyRotation) -> ReplySurbWithKeyRotation { + ReplySurbWithKeyRotation { + inner: self, + key_rotation, + } + } +} + +#[derive(Debug)] +pub struct ReplySurbWithKeyRotation { + pub(crate) inner: ReplySurb, + pub(crate) key_rotation: SphinxKeyRotation, +} + +impl ReplySurbWithKeyRotation { + pub fn encryption_key(&self) -> &SurbEncryptionKey { + self.inner.encryption_key() + } + + pub fn inner_reply_surb(&self) -> &ReplySurb { + &self.inner + } + + pub fn key_rotation(&self) -> SphinxKeyRotation { + self.key_rotation + } + + pub fn apply_surb>( + self, + message: M, + packet_size: PacketSize, + _packet_type: PacketType, + ) -> Result { + let (packet, first_hop_address) = + self.inner.apply_surb(message, packet_size, _packet_type)?; + + Ok(AppliedReplySurb { + packet, + first_hop_address, + key_rotation: self.key_rotation, + }) + } +} + +pub struct AppliedReplySurb { + pub(crate) packet: NymPacket, + pub(crate) first_hop_address: NymNodeRoutingAddress, + pub(crate) key_rotation: SphinxKeyRotation, +} + +impl AppliedReplySurb { + pub fn first_hop_address(&self) -> NymNodeRoutingAddress { + self.first_hop_address + } + + pub fn key_rotation(&self) -> SphinxKeyRotation { + self.key_rotation + } + + pub fn into_packet(self) -> NymPacket { + self.packet + } } diff --git a/common/nymsphinx/anonymous-replies/src/requests/mod.rs b/common/nymsphinx/anonymous-replies/src/requests/mod.rs index 2a95236386..8dbe741b91 100644 --- a/common/nymsphinx/anonymous-replies/src/requests/mod.rs +++ b/common/nymsphinx/anonymous-replies/src/requests/mod.rs @@ -1,16 +1,16 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ReplySurb, ReplySurbError}; +use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1}; +use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2}; +use crate::{ReplySurbError, ReplySurbWithKeyRotation}; use nym_sphinx_addressing::clients::{Recipient, RecipientFormattingError}; +use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation; use rand::{CryptoRng, RngCore}; use std::fmt::{Display, Formatter}; use std::mem; use thiserror::Error; -use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1}; -use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2}; - #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -84,7 +84,7 @@ impl AnonymousSenderTag { #[derive(Debug, Error)] pub enum InvalidReplyRequestError { - #[error("Did not provide sufficient number of bytes to deserialize a valid request")] + #[error("Did not provide sufficient number of bytes to deserialise a valid request")] RequestTooShortToDeserialize, #[error("{received} is not a valid content tag for a repliable message")] @@ -93,10 +93,13 @@ pub enum InvalidReplyRequestError { #[error("{received} is not a valid content tag for a reply message")] InvalidReplyContentTag { received: u8 }, - #[error("failed to deserialize recipient information - {0}")] + #[error("failed to deserialise sphinx key rotation details: {0}")] + MalformedSphinxKeyRotation(#[from] InvalidSphinxKeyRotation), + + #[error("failed to deserialise recipient information: {0}")] MalformedRecipient(#[from] RecipientFormattingError), - #[error("failed to deserialize replySURB - {0}")] + #[error("failed to deserialise replySURB: {0}")] MalformedReplySurb(#[from] ReplySurbError), } @@ -136,7 +139,7 @@ impl RepliableMessage { use_legacy_surb_format: bool, data: Vec, sender_tag: AnonymousSenderTag, - reply_surbs: Vec, + reply_surbs: Vec, ) -> Self { let content = if use_legacy_surb_format { RepliableMessageContent::Data(DataV1 { @@ -159,7 +162,7 @@ impl RepliableMessage { pub fn new_additional_surbs( use_legacy_surb_format: bool, sender_tag: AnonymousSenderTag, - reply_surbs: Vec, + reply_surbs: Vec, ) -> Self { let content = if use_legacy_surb_format { RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 { reply_surbs }) @@ -484,9 +487,10 @@ mod tests { use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1}; use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2}; use crate::requests::{AnonymousSenderTag, RepliableMessageContent, ReplyMessageContent}; - use crate::{ReplySurb, SurbEncryptionKey}; + use crate::{ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_sphinx_addressing::clients::Recipient; + use nym_sphinx_params::SphinxKeyRotation; use nym_sphinx_types::{ Delay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, PrivateKey, SURBMaterial, NODE_ADDRESS_LENGTH, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION, @@ -571,10 +575,12 @@ mod tests { n: usize, legacy: bool, hops: u8, - ) -> Vec { + ) -> Vec { let mut surbs = Vec::with_capacity(n); for _ in 0..n { - surbs.push(reply_surb(rng, legacy, hops)) + surbs.push( + reply_surb(rng, legacy, hops).with_key_rotation(SphinxKeyRotation::Unknown), + ) } surbs } diff --git a/common/nymsphinx/anonymous-replies/src/requests/v1.rs b/common/nymsphinx/anonymous-replies/src/requests/v1.rs index fec25025ac..03f3544869 100644 --- a/common/nymsphinx/anonymous-replies/src/requests/v1.rs +++ b/common/nymsphinx/anonymous-replies/src/requests/v1.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::requests::InvalidReplyRequestError; -use crate::ReplySurb; +use crate::{ReplySurb, ReplySurbWithKeyRotation}; use nym_sphinx_types::PAYLOAD_KEY_SIZE; use std::fmt::Display; use std::mem; @@ -14,10 +14,10 @@ const fn v1_reply_surb_serialised_len() -> usize { ReplySurb::BASE_OVERHEAD + 4 * PAYLOAD_KEY_SIZE } -fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize { +fn v1_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize { // sanity checks; this should probably be removed later on if let Some(reply_surb) = surbs.first() { - if reply_surb.surb.uses_key_seeds() { + if reply_surb.inner.surb.uses_key_seeds() { error!("using v1 surbs encoding with updated structure - the surbs will be unusable") } } @@ -30,7 +30,7 @@ fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize { // NUM_SURBS (u32) || SURB_DATA fn recover_reply_surbs_v1( bytes: &[u8], -) -> Result<(Vec, usize), InvalidReplyRequestError> { +) -> Result<(Vec, usize), InvalidReplyRequestError> { let mut consumed = mem::size_of::(); if bytes.len() < consumed { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); @@ -45,7 +45,7 @@ fn recover_reply_surbs_v1( let mut reply_surbs = Vec::with_capacity(num_surbs as usize); for _ in 0..num_surbs as usize { let surb_bytes = &bytes[consumed..consumed + surb_size]; - let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + let reply_surb = ReplySurb::from_bytes(surb_bytes)?.to_legacy(); reply_surbs.push(reply_surb); consumed += surb_size; @@ -55,19 +55,21 @@ fn recover_reply_surbs_v1( } // length (u32) prefixed reply surbs with legacy serialisation of 4 hops and full payload keys attached -fn reply_surbs_bytes_v1(reply_surbs: &[ReplySurb]) -> impl Iterator + use<'_> { +fn reply_surbs_bytes_v1( + reply_surbs: &[ReplySurbWithKeyRotation], +) -> impl Iterator + use<'_> { let num_surbs = reply_surbs.len() as u32; num_surbs .to_be_bytes() .into_iter() - .chain(reply_surbs.iter().flat_map(|s| s.to_bytes())) + .chain(reply_surbs.iter().flat_map(|s| s.inner.to_bytes())) } #[derive(Debug)] pub struct DataV1 { pub message: Vec, - pub reply_surbs: Vec, + pub reply_surbs: Vec, } impl Display for DataV1 { @@ -83,7 +85,7 @@ impl Display for DataV1 { #[derive(Debug)] pub struct AdditionalSurbsV1 { - pub reply_surbs: Vec, + pub reply_surbs: Vec, } impl Display for AdditionalSurbsV1 { @@ -98,7 +100,7 @@ impl Display for AdditionalSurbsV1 { #[derive(Debug)] pub struct HeartbeatV1 { - pub additional_reply_surbs: Vec, + pub additional_reply_surbs: Vec, } impl Display for HeartbeatV1 { diff --git a/common/nymsphinx/anonymous-replies/src/requests/v2.rs b/common/nymsphinx/anonymous-replies/src/requests/v2.rs index 813fabc276..6ed60203b1 100644 --- a/common/nymsphinx/anonymous-replies/src/requests/v2.rs +++ b/common/nymsphinx/anonymous-replies/src/requests/v2.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::requests::InvalidReplyRequestError; -use crate::ReplySurb; +use crate::{ReplySurb, ReplySurbWithKeyRotation}; +use nym_sphinx_params::SphinxKeyRotation; use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE; use std::fmt::Display; use std::iter::once; @@ -13,43 +14,55 @@ const fn v2_reply_surb_serialised_len(num_hops: u8) -> usize { } // sphinx doesn't support more than 5 hops (so cast to u8 is safe) -// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself) -fn reply_surbs_hops(reply_surbs: &[ReplySurb]) -> u8 { +// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself), +// which includes the same number of hops and the same underlying sphinx key rotation +fn reply_surbs_hops(reply_surbs: &[ReplySurbWithKeyRotation]) -> u8 { reply_surbs .first() - .map(|reply_surb| reply_surb.surb.materials_count() as u8) + .map(|reply_surb| reply_surb.inner.surb.materials_count() as u8) .unwrap_or_default() } -fn v2_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize { +fn key_rotation(reply_surbs: &[ReplySurbWithKeyRotation]) -> SphinxKeyRotation { + reply_surbs + .first() + .map(|reply_surb| reply_surb.key_rotation) + .unwrap_or_default() +} + +fn v2_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize { let num_surbs = surbs.len(); let num_hops = reply_surbs_hops(surbs); // sanity checks; this should probably be removed later on if let Some(reply_surb) = surbs.first() { - if !reply_surb.surb.uses_key_seeds() { + if !reply_surb.inner.surb.uses_key_seeds() { error!("using v2 surbs encoding with legacy structure - the surbs will be unusable") } } - // when serialising surbs are always prepended with u16-encoded count an u8-encoded number of hops - 3 + num_surbs * v2_reply_surb_serialised_len(num_hops) + // when serialising surbs are always prepended with: + // - u16-encoded count, + // - u8-encoded number of hops + // - u8-encoded sphinx key rotation (or unused for 'old' variant) + 4 + num_surbs * v2_reply_surb_serialised_len(num_hops) } -// NUM_SURBS (u16) || HOPS (u8) || SURB_DATA +// NUM_SURBS (u16) || HOPS (u8) || KEY ROTATION (u8) || SURB_DATA fn recover_reply_surbs_v2( bytes: &[u8], -) -> Result<(Vec, usize), InvalidReplyRequestError> { - if bytes.len() < 2 { +) -> Result<(Vec, usize), InvalidReplyRequestError> { + if bytes.len() < 4 { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } // we're not attaching more than 65k surbs... let num_surbs = u16::from_be_bytes([bytes[0], bytes[1]]); let num_hops = bytes[2]; - let mut consumed = 3; + let key_rotation = SphinxKeyRotation::try_from(bytes[3])?; + let mut consumed = 4; - let surb_size = ReplySurb::v2_serialised_len(num_hops); + let surb_size = v2_reply_surb_serialised_len(num_hops); if bytes[consumed..].len() < num_surbs as usize * surb_size { return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); } @@ -57,7 +70,7 @@ fn recover_reply_surbs_v2( let mut reply_surbs = Vec::with_capacity(num_surbs as usize); for _ in 0..num_surbs as usize { let surb_bytes = &bytes[consumed..consumed + surb_size]; - let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + let reply_surb = ReplySurb::from_bytes(surb_bytes)?.with_key_rotation(key_rotation); reply_surbs.push(reply_surb); consumed += surb_size; @@ -66,21 +79,25 @@ fn recover_reply_surbs_v2( Ok((reply_surbs, consumed)) } -fn reply_surbs_bytes_v2(reply_surbs: &[ReplySurb]) -> impl Iterator + use<'_> { +fn reply_surbs_bytes_v2( + reply_surbs: &[ReplySurbWithKeyRotation], +) -> impl Iterator + use<'_> { let num_surbs = reply_surbs.len() as u16; let num_hops = reply_surbs_hops(reply_surbs); + let key_rotation = key_rotation(reply_surbs) as u8; num_surbs .to_be_bytes() .into_iter() .chain(once(num_hops)) - .chain(reply_surbs.iter().flat_map(|surb| surb.to_bytes())) + .chain(once(key_rotation)) + .chain(reply_surbs.iter().flat_map(|surb| surb.inner.to_bytes())) } #[derive(Debug)] pub struct DataV2 { pub message: Vec, - pub reply_surbs: Vec, + pub reply_surbs: Vec, } impl Display for DataV2 { @@ -96,7 +113,7 @@ impl Display for DataV2 { #[derive(Debug)] pub struct AdditionalSurbsV2 { - pub reply_surbs: Vec, + pub reply_surbs: Vec, } impl Display for AdditionalSurbsV2 { @@ -111,7 +128,7 @@ impl Display for AdditionalSurbsV2 { #[derive(Debug)] pub struct HeartbeatV2 { - pub additional_reply_surbs: Vec, + pub additional_reply_surbs: Vec, } impl Display for HeartbeatV2 { diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs index 715a7ea400..68419cac56 100644 --- a/common/nymsphinx/chunking/src/fragment.rs +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -72,11 +72,7 @@ pub struct FragmentIdentifier { impl fmt::Display for FragmentIdentifier { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "Fragment Identifier: id: {} position: {}", - self.set_id, self.fragment_position - ) + write!(f, "{} @ {}", self.set_id, self.fragment_position) } } @@ -272,6 +268,10 @@ impl Fragment { self.payload } + pub fn payload(&self) -> &[u8] { + &self.payload + } + /// Tries to recover `Fragment` from slice of bytes extracted from received sphinx packet. /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata /// is malformed or self-contradictory, for example if current_fragment > total_fragments. diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 337fa87bf6..42be1db410 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -10,7 +10,9 @@ use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_chunking::fragment::COVER_FRAG_ID; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType}; +use nym_sphinx_params::{ + PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType, SphinxKeyRotation, +}; use nym_sphinx_types::NymPacket; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; @@ -53,6 +55,7 @@ where average_ack_delay, topology, packet_type, + false, // make sure mix hops are enabled )?) } @@ -124,6 +127,9 @@ where let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len()); let destination = full_address.as_sphinx_destination(); + let rotation_id = topology.current_key_rotation(); + let sphinx_key_rotation = SphinxKeyRotation::from(rotation_id); + let first_hop_address = NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap(); @@ -145,7 +151,12 @@ where )?, }; - Ok(MixPacket::new(first_hop_address, packet, packet_type)) + Ok(MixPacket::new( + first_hop_address, + packet, + packet_type, + sphinx_key_rotation, + )) } /// Helper function used to determine if given message represents a loop cover message. diff --git a/common/nymsphinx/forwarding/Cargo.toml b/common/nymsphinx/forwarding/Cargo.toml index c3f3e0dc9f..c8beb33b12 100644 --- a/common/nymsphinx/forwarding/Cargo.toml +++ b/common/nymsphinx/forwarding/Cargo.toml @@ -11,5 +11,5 @@ repository = { workspace = true } nym-sphinx-addressing = { path = "../addressing" } nym-sphinx-params = { path = "../params" } nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] } -nym-outfox = { path = "../../../nym-outfox" } +nym-sphinx-anonymous-replies = { path = "../anonymous-replies" } thiserror = { workspace = true } diff --git a/common/nymsphinx/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 97b1419aa1..9fb9f3a915 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -2,24 +2,36 @@ // SPDX-License-Identifier: Apache-2.0 use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; -use nym_sphinx_params::{PacketSize, PacketType}; +use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation}; use nym_sphinx_types::{NymPacket, NymPacketError}; -use std::fmt::{self, Debug, Formatter}; +use nym_sphinx_anonymous_replies::reply_surb::AppliedReplySurb; +use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation; +use nym_sphinx_params::packet_sizes::InvalidPacketSize; +use nym_sphinx_params::packet_types::InvalidPacketType; +use std::net::SocketAddr; use thiserror::Error; #[derive(Debug, Error)] pub enum MixPacketFormattingError { #[error("too few bytes provided to recover from bytes")] TooFewBytesProvided, - #[error("provided packet mode is invalid")] - InvalidPacketType, - #[error("received request had invalid size - received {0}")] - InvalidPacketSize(usize), + + #[error("provided packet mode is invalid: {0}")] + InvalidPacketType(#[from] InvalidPacketType), + + #[error("received request had an invalid packet size: {0}")] + InvalidPacketSize(#[from] InvalidPacketSize), + + #[error("provided key rotation is invalid: {0}")] + InvalidKeyRotation(#[from] InvalidSphinxKeyRotation), + #[error("address field was incorrectly encoded")] InvalidAddress, + #[error("received sphinx packet was malformed")] MalformedSphinxPacket, + #[error("Packet: {0}")] Packet(#[from] NymPacketError), } @@ -30,20 +42,12 @@ impl From for MixPacketFormattingError { } } +#[derive(Debug)] pub struct MixPacket { next_hop: NymNodeRoutingAddress, packet: NymPacket, packet_type: PacketType, -} - -impl Debug for MixPacket { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "MixPacket to {:?} with packet_type {:?}. Packet {:?}", - self.next_hop, self.packet_type, self.packet - ) - } + key_rotation: SphinxKeyRotation, } impl MixPacket { @@ -51,11 +55,25 @@ impl MixPacket { next_hop: NymNodeRoutingAddress, packet: NymPacket, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Self { MixPacket { next_hop, packet, packet_type, + key_rotation, + } + } + + pub fn from_applied_surb( + applied_reply_surb: AppliedReplySurb, + packet_type: PacketType, + ) -> Self { + MixPacket { + next_hop: applied_reply_surb.first_hop_address(), + key_rotation: applied_reply_surb.key_rotation(), + packet: applied_reply_surb.into_packet(), + packet_type, } } @@ -63,6 +81,10 @@ impl MixPacket { self.next_hop } + pub fn next_hop_address(&self) -> SocketAddr { + self.next_hop.into() + } + pub fn packet(&self) -> &NymPacket { &self.packet } @@ -71,45 +93,94 @@ impl MixPacket { self.packet } + pub fn key_rotation(&self) -> SphinxKeyRotation { + self.key_rotation + } + pub fn packet_type(&self) -> PacketType { self.packet_type } // the message is formatted as follows: // packet_type || FIRST_HOP || packet - pub fn try_from_bytes(b: &[u8]) -> Result { - let packet_type = match PacketType::try_from(b[0]) { - Ok(mode) => mode, - Err(_) => return Err(MixPacketFormattingError::InvalidPacketType), - }; + pub fn try_from_v1_bytes(b: &[u8]) -> Result { + // we need at least 1 byte to read packet type and another one to read type of the encoded first hop address + if b.len() < 2 { + return Err(MixPacketFormattingError::TooFewBytesProvided); + } + + let packet_type = PacketType::try_from(b[0])?; let next_hop = NymNodeRoutingAddress::try_from_bytes(&b[1..])?; let addr_offset = next_hop.bytes_min_len(); let packet_data = &b[addr_offset + 1..]; let packet_size = packet_data.len(); - if PacketSize::get_type(packet_size).is_err() { - Err(MixPacketFormattingError::InvalidPacketSize(packet_size)) - } else { - let packet = match packet_type { - PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?, - _ => NymPacket::sphinx_from_bytes(packet_data)?, - }; - Ok(MixPacket { - next_hop, - packet, - packet_type, - }) - } + // make sure the received data length corresponds to a valid packet + let _ = PacketSize::get_type(packet_size)?; + + let packet = match packet_type { + PacketType::Mix => NymPacket::sphinx_from_bytes(packet_data)?, + PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?, + }; + + Ok(MixPacket { + next_hop, + packet, + packet_type, + key_rotation: SphinxKeyRotation::Unknown, + }) } - pub fn into_bytes(self) -> Result, MixPacketFormattingError> { + pub fn into_v1_bytes(self) -> Result, MixPacketFormattingError> { Ok(std::iter::once(self.packet_type as u8) .chain(self.next_hop.as_bytes()) .chain(self.packet.to_bytes()?) .collect()) } + + // the message is formatted as follows: + // packet_type || KEY_ROTATION || FIRST_HOP || packet + pub fn try_from_v2_bytes(b: &[u8]) -> Result { + // we need at least 1 byte to read packet type, 1 byte to read key rotation + // and finally another one to read type of the encoded first hop address + if b.len() < 3 { + return Err(MixPacketFormattingError::TooFewBytesProvided); + } + + let packet_type = PacketType::try_from(b[0])?; + let key_rotation = SphinxKeyRotation::try_from(b[1])?; + + let next_hop = NymNodeRoutingAddress::try_from_bytes(&b[2..])?; + let addr_offset = next_hop.bytes_min_len(); + + let packet_data = &b[addr_offset + 2..]; + let packet_size = packet_data.len(); + + // make sure the received data length corresponds to a valid packet + let _ = PacketSize::get_type(packet_size)?; + + let packet = match packet_type { + PacketType::Mix => NymPacket::sphinx_from_bytes(packet_data)?, + PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?, + }; + + Ok(MixPacket { + next_hop, + packet, + packet_type, + key_rotation, + }) + } + + pub fn into_v2_bytes(self) -> Result, MixPacketFormattingError> { + Ok(std::iter::once(self.packet_type as u8) + .chain(std::iter::once(self.key_rotation as u8)) + .chain(self.next_hop.as_bytes()) + .chain(self.packet.to_bytes()?) + .collect()) + } } // TODO: test for serialization and errors! diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index 85685581aa..da148908e4 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -3,6 +3,7 @@ use crate::packet::{FramedNymPacket, Header}; use bytes::{Buf, BufMut, BytesMut}; +use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation; use nym_sphinx_params::packet_sizes::{InvalidPacketSize, PacketSize}; use nym_sphinx_params::packet_types::InvalidPacketType; use nym_sphinx_params::packet_version::{InvalidPacketVersion, PacketVersion}; @@ -23,6 +24,9 @@ pub enum NymCodecError { #[error("the packet version information was malformed: {0}")] InvalidPacketVersion(#[from] InvalidPacketVersion), + #[error("the sphinx key rotation information was malformed: {0}")] + InvalidSphinxKeyRotation(#[from] InvalidSphinxKeyRotation), + #[error("received unsupported packet version {received}. max supported is {max_supported}")] UnsupportedPacketVersion { received: PacketVersion, @@ -65,8 +69,8 @@ impl Decoder for NymCodec { fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { if src.is_empty() { // can't do anything if we have no bytes, but let's reserve enough for the most - // conservative case, i.e. receiving an ack packet - src.reserve(Header::SIZE + PacketSize::AckPacket.size()); + // conservative case, i.e. receiving a legacy ack packet + src.reserve(Header::INITIAL_SIZE + PacketSize::AckPacket.size()); return Ok(None); } @@ -77,17 +81,20 @@ impl Decoder for NymCodec { None => return Ok(None), // we have some data but not enough to get header back }; + let header_size = header.encoded_size(); let packet_size = header.packet_size.size(); - let frame_len = Header::SIZE + packet_size; - if src.len() < frame_len { + let frame_size = header_size + packet_size; + + if src.len() < frame_size { // we don't have enough bytes to read the rest of frame + // (we have already read the full header) src.reserve(packet_size); return Ok(None); } // advance buffer past the header - at this point we have enough bytes - src.advance(Header::SIZE); + src.advance(header_size); let packet_bytes = src.split_to(packet_size); let packet = if let Some(slice) = packet_bytes.get(..) { // here it could be debatable whether stream is corrupt or not, @@ -100,8 +107,7 @@ impl Decoder for NymCodec { return Ok(None); }; - // let packet = SphinxPacket::from_bytes(&sphinx_packet_bytes)?; - let nymsphinx_packet = FramedNymPacket { header, packet }; + let framed_packet = FramedNymPacket { header, packet }; // As per docs: // Before returning from the function, implementations should ensure that the buffer @@ -114,11 +120,11 @@ impl Decoder for NymCodec { // we also assume the next packet coming from the same client will use exactly the same versioning // as the current packet - let mut allocate_for_next_packet = Header::SIZE + PacketSize::AckPacket.size(); + let mut allocate_for_next_packet = header.encoded_size() + PacketSize::AckPacket.size(); if !src.is_empty() { match Header::decode(src) { Ok(Some(next_header)) => { - allocate_for_next_packet = Header::SIZE + next_header.packet_size.size(); + allocate_for_next_packet = next_header.frame_size(); } Ok(None) => { // we don't have enough information to know how much to reserve, fallback to the ack case @@ -126,22 +132,52 @@ impl Decoder for NymCodec { // the next frame will be malformed but let's leave handling the error to the next // call to 'decode', as presumably, the current sphinx packet is still valid - Err(_) => return Ok(Some(nymsphinx_packet)), + Err(_) => return Ok(Some(framed_packet)), }; } src.reserve(allocate_for_next_packet); - Ok(Some(nymsphinx_packet)) + Ok(Some(framed_packet)) } } #[cfg(test)] mod packet_encoding { use super::*; + use nym_sphinx_params::packet_version::{ + CURRENT_PACKET_VERSION, INITIAL_PACKET_VERSION_NUMBER, + }; + use nym_sphinx_params::PacketType; use nym_sphinx_types::{ Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, - PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH, + NymPacket, PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH, }; + fn dummy_header() -> Header { + Header { + packet_version: CURRENT_PACKET_VERSION, + packet_size: Default::default(), + key_rotation: Default::default(), + packet_type: Default::default(), + } + } + + fn dummy_outfox() -> Header { + Header { + packet_type: PacketType::Outfox, + packet_size: PacketSize::OutfoxRegularPacket, + ..dummy_legacy_header() + } + } + + fn dummy_legacy_header() -> Header { + Header { + packet_version: PacketVersion::try_from(INITIAL_PACKET_VERSION_NUMBER).unwrap(), + packet_size: Default::default(), + key_rotation: Default::default(), + packet_type: Default::default(), + } + } + fn random_pubkey() -> nym_sphinx_types::PublicKey { let private_key = PrivateKey::random(); (&private_key).into() @@ -222,7 +258,7 @@ mod packet_encoding { #[test] fn whole_packet_can_be_decoded_from_a_valid_encoded_instance() { - let header = Default::default(); + let header = dummy_header(); let sphinx_packet = make_valid_sphinx_packet(Default::default()); let sphinx_bytes = sphinx_packet.to_bytes().unwrap(); @@ -241,7 +277,7 @@ mod packet_encoding { #[test] fn whole_outfox_can_be_decoded_from_a_valid_encoded_instance() { - let header = Header::outfox(); + let header = dummy_outfox(); let packet = make_valid_outfox_packet(PacketSize::OutfoxRegularPacket); let packet_bytes = packet.to_bytes().unwrap(); @@ -269,7 +305,7 @@ mod packet_encoding { assert!(NymCodec.decode(&mut empty_bytes).unwrap().is_none()); assert_eq!( empty_bytes.capacity(), - Header::SIZE + PacketSize::AckPacket.size() + Header::INITIAL_SIZE + PacketSize::AckPacket.size() ); } @@ -287,13 +323,14 @@ mod packet_encoding { let header = Header { packet_version: PacketVersion::new(), packet_size, - ..Default::default() + key_rotation: Default::default(), + packet_type: Default::default(), }; let mut bytes = BytesMut::new(); header.encode(&mut bytes); assert!(NymCodec.decode(&mut bytes).unwrap().is_none()); - assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size()) + assert_eq!(bytes.capacity(), Header::V8_SIZE + packet_size.size()) } } @@ -301,7 +338,7 @@ mod packet_encoding { fn for_full_frame_with_versioned_header() { // if full frame is used exactly, there should be enough space for header + ack packet let packet = FramedNymPacket { - header: Header::default(), + header: dummy_header(), packet: make_valid_sphinx_packet(Default::default()), }; @@ -310,7 +347,7 @@ mod packet_encoding { assert!(NymCodec.decode(&mut bytes).unwrap().is_some()); assert_eq!( bytes.capacity(), - Header::SIZE + PacketSize::AckPacket.size() + Header::V8_SIZE + PacketSize::AckPacket.size() ); } @@ -327,7 +364,7 @@ mod packet_encoding { for packet_size in packet_sizes { let first_packet = FramedNymPacket { - header: Header::default(), + header: dummy_header(), packet: make_valid_sphinx_packet(Default::default()), }; @@ -346,12 +383,12 @@ mod packet_encoding { #[test] fn can_decode_two_packets_immediately() { let packet1 = FramedNymPacket { - header: Header::default(), + header: dummy_header(), packet: make_valid_sphinx_packet(Default::default()), }; let packet2 = FramedNymPacket { - header: Header::default(), + header: dummy_header(), packet: make_valid_sphinx_packet(Default::default()), }; @@ -368,12 +405,12 @@ mod packet_encoding { #[test] fn can_decode_two_packets_in_separate_calls() { let packet1 = FramedNymPacket { - header: Header::default(), + header: dummy_header(), packet: make_valid_sphinx_packet(Default::default()), }; let packet2 = FramedNymPacket { - header: Header::default(), + header: dummy_header(), packet: make_valid_sphinx_packet(Default::default()), }; diff --git a/common/nymsphinx/framing/src/packet.rs b/common/nymsphinx/framing/src/packet.rs index 184444f807..f8b86b7988 100644 --- a/common/nymsphinx/framing/src/packet.rs +++ b/common/nymsphinx/framing/src/packet.rs @@ -3,8 +3,12 @@ use crate::codec::NymCodecError; use bytes::{BufMut, BytesMut}; +use nym_sphinx_forwarding::packet::MixPacket; +use nym_sphinx_params::key_rotation::SphinxKeyRotation; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION}; +use nym_sphinx_params::packet_version::{ + PacketVersion, CURRENT_PACKET_VERSION, LEGACY_PACKET_VERSION, +}; use nym_sphinx_params::PacketType; use nym_sphinx_types::NymPacket; @@ -18,20 +22,38 @@ pub struct FramedNymPacket { } impl FramedNymPacket { - pub fn new(packet: NymPacket, packet_type: PacketType) -> Self { + pub fn new( + packet: NymPacket, + packet_type: PacketType, + key_rotation: SphinxKeyRotation, + use_legacy_packet_encoding: bool, + ) -> Self { // If this fails somebody is using the library in a super incorrect way, because they // already managed to somehow create a sphinx packet let packet_size = PacketSize::get_type(packet.len()).unwrap(); + let packet_version = if use_legacy_packet_encoding { + LEGACY_PACKET_VERSION + } else { + PacketVersion::new() + }; + let header = Header { - packet_version: PacketVersion::new(), + packet_version, packet_size, + key_rotation, packet_type, }; FramedNymPacket { header, packet } } + pub fn from_mix_packet(packet: MixPacket, use_legacy_packet_encoding: bool) -> Self { + let typ = packet.packet_type(); + let rot = packet.key_rotation(); + FramedNymPacket::new(packet.into_packet(), typ, rot, use_legacy_packet_encoding) + } + pub fn header(&self) -> Header { self.header } @@ -52,6 +74,10 @@ impl FramedNymPacket { &self.packet } + pub fn key_rotation(&self) -> SphinxKeyRotation { + self.header.key_rotation + } + pub fn is_sphinx(&self) -> bool { self.packet.is_sphinx() } @@ -60,13 +86,16 @@ impl FramedNymPacket { // Contains any metadata that might be useful for sending between mix nodes. // TODO: in theory all those data could be put in a single `u8` by setting appropriate bits, // but would that really be worth it? -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct Header { /// Represents the wire format version used to construct this packet. - pub(crate) packet_version: PacketVersion, + pub packet_version: PacketVersion, /// Represents type and consequently size of the included SphinxPacket. - pub(crate) packet_size: PacketSize, + pub packet_size: PacketSize, + + /// Represents information regarding which key rotation has been used for constructing this packet. + pub key_rotation: SphinxKeyRotation, /// Represents whether this packet is sent in a `vpn_mode` meaning it should not get delayed /// and shared keys might get reused. Mixnodes are capable of inferring this mode from the @@ -77,35 +106,48 @@ pub struct Header { /// (note: this will be behind some encryption, either something implemented by us or some SSL action) // Note: currently packet_type is deprecated but is still left as a concept behind to not break // compatibility with existing network - pub(crate) packet_type: PacketType, + pub packet_type: PacketType, } impl Header { - pub(crate) const SIZE: usize = 3; - - pub fn outfox() -> Header { - Header { - packet_version: PacketVersion::default(), - packet_size: PacketSize::OutfoxRegularPacket, - packet_type: PacketType::Outfox, - } - } + pub(crate) const INITIAL_SIZE: usize = 3; + pub(crate) const V8_SIZE: usize = 4; pub(crate) fn encode(&self, dst: &mut BytesMut) { - dst.reserve(Self::SIZE); + let len = self.encoded_size(); + + if dst.len() < len { + dst.reserve(len); + } dst.put_u8(self.packet_version.as_u8()); dst.put_u8(self.packet_size as u8); dst.put_u8(self.packet_type as u8); + if !self.packet_version.is_initial() { + dst.put_u8(self.key_rotation as u8) + } + // reserve bytes for the actual packet dst.reserve(self.packet_size.size()); } + pub(crate) fn frame_size(&self) -> usize { + self.encoded_size() + self.packet_size.size() + } + + pub(crate) fn encoded_size(&self) -> usize { + if self.packet_version.is_initial() { + Self::INITIAL_SIZE + } else { + Self::V8_SIZE + } + } + pub(crate) fn decode(src: &mut BytesMut) -> Result, NymCodecError> { - if src.len() < Self::SIZE { + if src.len() < Self::INITIAL_SIZE { // can't do anything if we don't have enough bytes - but reserve enough for the next call - src.reserve(Self::SIZE); + src.reserve(Self::INITIAL_SIZE); return Ok(None); } @@ -119,10 +161,23 @@ impl Header { }); } + // we need to be able to decode the full header + if !packet_version.is_initial() && src.len() < Self::V8_SIZE { + src.reserve(1); + return Ok(None); + } + + let key_rotation = if packet_version.is_initial() { + SphinxKeyRotation::Unknown + } else { + SphinxKeyRotation::try_from(src[3])? + }; + Ok(Some(Header { packet_version, packet_size: PacketSize::try_from(src[1])?, packet_type: PacketType::try_from(src[2])?, + key_rotation, })) } } @@ -130,10 +185,20 @@ impl Header { #[cfg(test)] mod header_encoding { use super::*; + use nym_sphinx_params::packet_version::INITIAL_PACKET_VERSION_NUMBER; + + fn dummy_header() -> Header { + Header { + packet_version: CURRENT_PACKET_VERSION, + packet_size: Default::default(), + key_rotation: Default::default(), + packet_type: Default::default(), + } + } #[test] fn header_can_be_decoded_from_a_valid_encoded_instance() { - let header = Header::default(); + let header = dummy_header(); let mut bytes = BytesMut::new(); header.encode(&mut bytes); let decoded = Header::decode(&mut bytes).unwrap().unwrap(); @@ -153,6 +218,7 @@ mod header_encoding { PacketVersion::new().as_u8(), unknown_packet_size, PacketType::default() as u8, + SphinxKeyRotation::EvenRotation as u8, ] .as_ref(), ); @@ -167,7 +233,9 @@ mod header_encoding { let mut bytes = BytesMut::from( [ - PacketVersion::new().as_u8(), + PacketVersion::try_from(INITIAL_PACKET_VERSION_NUMBER) + .unwrap() + .as_u8(), PacketSize::default() as u8, unknown_packet_type, ] @@ -181,12 +249,12 @@ mod header_encoding { let mut empty_bytes = BytesMut::new(); let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_1.is_none()); - assert!(empty_bytes.capacity() > Header::SIZE); + assert!(empty_bytes.capacity() > Header::V8_SIZE); let mut empty_bytes = BytesMut::with_capacity(1); let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_2.is_none()); - assert!(empty_bytes.capacity() > Header::SIZE); + assert!(empty_bytes.capacity() > Header::V8_SIZE); } #[test] @@ -202,7 +270,7 @@ mod header_encoding { let header = Header { packet_version: PacketVersion::new(), packet_size, - ..Default::default() + ..dummy_header() }; let mut bytes = BytesMut::new(); header.encode(&mut bytes); @@ -217,6 +285,7 @@ mod header_encoding { let unchecked_header = Header { packet_version: future_version, packet_size: PacketSize::RegularPacket, + key_rotation: SphinxKeyRotation::EvenRotation, packet_type: PacketType::Mix, }; let mut bytes = BytesMut::new(); diff --git a/common/nymsphinx/framing/src/processing.rs b/common/nymsphinx/framing/src/processing.rs index 1dc9ebf066..3703f36d84 100644 --- a/common/nymsphinx/framing/src/processing.rs +++ b/common/nymsphinx/framing/src/processing.rs @@ -5,7 +5,7 @@ use crate::packet::FramedNymPacket; use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError}; use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; use nym_sphinx_forwarding::packet::MixPacket; -use nym_sphinx_params::{PacketSize, PacketType}; +use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation}; use nym_sphinx_types::header::shared_secret::ExpandedSharedSecret; use nym_sphinx_types::{ Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError, @@ -103,10 +103,18 @@ pub enum PacketProcessingError { #[error("attempted to partially process an outfox packet")] PartialOutfoxProcessing, + #[error("the key needed for unwrapping this packet has already expired")] + ExpiredKey, + #[error("this packet has already been processed before")] PacketReplay, } +pub struct PartialyUnwrappedPacketWithKeyRotation { + pub packet: PartiallyUnwrappedPacket, + pub used_key_rotation: u32, +} + pub struct PartiallyUnwrappedPacket { received_data: FramedNymPacket, partial_result: PartialMixProcessingResult, @@ -119,16 +127,19 @@ impl PartiallyUnwrappedPacket { pub fn new( received_data: FramedNymPacket, sphinx_key: &PrivateKey, - ) -> Result { + ) -> Result { let partial_result = match received_data.packet() { NymPacket::Sphinx(packet) => { let expanded_shared_secret = packet.header.compute_expanded_shared_secret(sphinx_key); // don't continue if the header is malformed - packet + if let Err(err) = packet .header - .ensure_header_integrity(&expanded_shared_secret)?; + .ensure_header_integrity(&expanded_shared_secret) + { + return Err((received_data, err.into())); + } PartialMixProcessingResult::Sphinx { expanded_shared_secret, @@ -147,6 +158,7 @@ impl PartiallyUnwrappedPacket { let packet_size = self.received_data.packet_size(); let packet_type = self.received_data.packet_type(); + let key_rotation = self.received_data.header.key_rotation; let packet = self.received_data.into_inner(); // currently partial unwrapping is only implemented for sphinx packets. @@ -161,12 +173,22 @@ impl PartiallyUnwrappedPacket { return Err(PacketProcessingError::PartialOutfoxProcessing); }; let processed_packet = packet.process_with_expanded_secret(&expanded_shared_secret)?; - wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type) + wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type, key_rotation) } pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> { self.partial_result.replay_tag() } + + pub fn with_key_rotation( + self, + used_key_rotation: u32, + ) -> PartialyUnwrappedPacketWithKeyRotation { + PartialyUnwrappedPacketWithKeyRotation { + packet: self, + used_key_rotation, + } + } } impl From<(FramedNymPacket, PartialMixProcessingResult)> for PartiallyUnwrappedPacket { @@ -186,13 +208,14 @@ pub fn process_framed_packet( ) -> Result { let packet_size = received.packet_size(); let packet_type = received.packet_type(); + let key_rotation = received.key_rotation(); // unwrap the sphinx packet let processed_packet = perform_framed_unwrapping(received, sphinx_key)?; // for forward packets, extract next hop and set delay (but do NOT delay here) // for final packets, extract SURBAck - perform_final_processing(processed_packet, packet_size, packet_type) + perform_final_processing(processed_packet, packet_size, packet_type, key_rotation) } fn perform_framed_unwrapping( @@ -217,6 +240,7 @@ fn wrap_processed_sphinx_packet( packet: nym_sphinx_types::ProcessedPacket, packet_size: PacketSize, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Result { let processing_data = match packet.data { ProcessedPacketData::ForwardHop { @@ -228,6 +252,7 @@ fn wrap_processed_sphinx_packet( next_hop_address, delay, packet_type, + key_rotation, ), // right now there's no use for the surb_id included in the header - probably it should get removed from the // sphinx all together? @@ -240,6 +265,7 @@ fn wrap_processed_sphinx_packet( payload.recover_plaintext()?, packet_size, packet_type, + key_rotation, ), }?; @@ -253,6 +279,7 @@ fn wrap_processed_outfox_packet( packet: OutfoxProcessedPacket, packet_size: PacketSize, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Result { let next_address = *packet.next_address(); let packet = packet.into_packet(); @@ -262,6 +289,7 @@ fn wrap_processed_outfox_packet( packet.recover_plaintext()?.to_vec(), packet_size, packet_type, + key_rotation, )?; Ok(MixProcessingResult { packet_version: MixPacketVersion::Outfox, @@ -272,6 +300,7 @@ fn wrap_processed_outfox_packet( NymNodeRoutingAddress::try_from_bytes(&next_address)?, NymPacket::Outfox(packet), PacketType::Outfox, + SphinxKeyRotation::Unknown, ); Ok(MixProcessingResult { packet_version: MixPacketVersion::Outfox, @@ -287,13 +316,14 @@ fn perform_final_processing( packet: NymProcessedPacket, packet_size: PacketSize, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Result { match packet { NymProcessedPacket::Sphinx(packet) => { - wrap_processed_sphinx_packet(packet, packet_size, packet_type) + wrap_processed_sphinx_packet(packet, packet_size, packet_type, key_rotation) } NymProcessedPacket::Outfox(packet) => { - wrap_processed_outfox_packet(packet, packet_size, packet_type) + wrap_processed_outfox_packet(packet, packet_size, packet_type, key_rotation) } } } @@ -303,8 +333,10 @@ fn process_final_hop( payload: Vec, packet_size: PacketSize, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Result { - let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?; + let (forward_ack, message) = + split_into_ack_and_message(payload, packet_size, packet_type, key_rotation)?; Ok(MixProcessingResultData::FinalHop { final_hop_data: ProcessedFinalHop { @@ -319,6 +351,7 @@ fn split_into_ack_and_message( data: Vec, packet_size: PacketSize, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Result<(Option, Vec), PacketProcessingError> { match packet_size { PacketSize::AckPacket | PacketSize::OutfoxAckPacket => { @@ -340,7 +373,7 @@ fn split_into_ack_and_message( return Err(err.into()); } }; - let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type); + let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type, key_rotation); Ok((Some(forward_ack), message)) } } @@ -368,10 +401,11 @@ fn process_forward_hop( forward_address: NodeAddressBytes, delay: SphinxDelay, packet_type: PacketType, + key_rotation: SphinxKeyRotation, ) -> Result { let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?; - let packet = MixPacket::new(next_hop_address, packet, packet_type); + let packet = MixPacket::new(next_hop_address, packet, packet_type, key_rotation); Ok(MixProcessingResultData::ForwardHop { packet, delay: Some(delay), @@ -422,9 +456,13 @@ mod tests { #[tokio::test] async fn splitting_into_ack_and_message_returns_whole_data_for_ack() { let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10]; - let (ack, message) = - split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix) - .unwrap(); + let (ack, message) = split_into_ack_and_message( + data.clone(), + PacketSize::AckPacket, + PacketType::Mix, + SphinxKeyRotation::EvenRotation, + ) + .unwrap(); assert!(ack.is_none()); assert_eq!(data, message) } @@ -436,6 +474,7 @@ mod tests { data.clone(), PacketSize::OutfoxAckPacket, PacketType::Outfox, + SphinxKeyRotation::EvenRotation, ) .unwrap(); assert!(ack.is_none()); diff --git a/common/nymsphinx/params/src/key_rotation.rs b/common/nymsphinx/params/src/key_rotation.rs new file mode 100644 index 0000000000..2a1ba7135f --- /dev/null +++ b/common/nymsphinx/params/src/key_rotation.rs @@ -0,0 +1,68 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum SphinxKeyRotation { + // for legacy packets, where there's no explicit information which key has been used + #[default] + Unknown = 0, + + OddRotation = 1, + + EvenRotation = 2, +} + +impl SphinxKeyRotation { + pub fn from_key_rotation_id(rotation_id: u32) -> Self { + rotation_id.into() + } + + pub fn is_unknown(&self) -> bool { + matches!(self, SphinxKeyRotation::Unknown) + } + + pub fn is_even(&self) -> bool { + matches!(self, SphinxKeyRotation::EvenRotation) + } + + pub fn is_odd(&self) -> bool { + matches!(self, SphinxKeyRotation::OddRotation) + } +} + +#[derive(Debug, Error)] +#[error("{received} is not a valid encoding of a sphinx key rotation")] +pub struct InvalidSphinxKeyRotation { + received: u8, +} + +// convert from particular rotation id into SphinxKeyRotation variant +impl From for SphinxKeyRotation { + fn from(value: u32) -> Self { + if value == 0 || value == u32::MAX { + SphinxKeyRotation::Unknown + } else if value % 2 == 0 { + SphinxKeyRotation::EvenRotation + } else { + SphinxKeyRotation::OddRotation + } + } +} + +// convert from an encoded SphinxKeyRotation into particular variant +// if value is actually provided, it MUST be one of the two. otherwise is invalid +impl TryFrom for SphinxKeyRotation { + type Error = InvalidSphinxKeyRotation; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (Self::Unknown as u8) => Ok(Self::Unknown), + _ if value == (Self::OddRotation as u8) => Ok(Self::OddRotation), + _ if value == (Self::EvenRotation as u8) => Ok(Self::EvenRotation), + received => Err(InvalidSphinxKeyRotation { received }), + } + } +} diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index 69c8368e99..9046425a40 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -9,9 +9,12 @@ use nym_crypto::Aes256GcmSiv; type Aes128Ctr = ctr::Ctr64BE; // Re-export for ease of use +pub use key_rotation::SphinxKeyRotation; pub use packet_sizes::PacketSize; pub use packet_types::PacketType; +pub use packet_version::PacketVersion; +pub mod key_rotation; pub mod packet_sizes; pub mod packet_types; pub mod packet_version; diff --git a/common/nymsphinx/params/src/packet_types.rs b/common/nymsphinx/params/src/packet_types.rs index f3ad3d54b9..c597d9fda8 100644 --- a/common/nymsphinx/params/src/packet_types.rs +++ b/common/nymsphinx/params/src/packet_types.rs @@ -11,7 +11,7 @@ use std::fmt; use thiserror::Error; #[derive(Error, Debug)] -#[error("{received} is not a valid packet mode tag")] +#[error("{received} is not a valid packet type tag")] pub struct InvalidPacketType { received: u8, } diff --git a/common/nymsphinx/params/src/packet_version.rs b/common/nymsphinx/params/src/packet_version.rs index d0d3f89fc6..81044a8b79 100644 --- a/common/nymsphinx/params/src/packet_version.rs +++ b/common/nymsphinx/params/src/packet_version.rs @@ -10,16 +10,21 @@ use thiserror::Error; // - packet_version (starting with v1.1.0) // - packet_size indicator // - packet_type +// - sphinx key rotation (starting with v1.13.0 - the Dolcelatte release) + // it also just so happens that the only valid values for packet_size indicator include values 1-6 // therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet, // otherwise we should treat it as legacy /// Increment it whenever we perform any breaking change in the wire format! pub const INITIAL_PACKET_VERSION_NUMBER: u8 = 7; - -pub const CURRENT_PACKET_VERSION_NUMBER: u8 = INITIAL_PACKET_VERSION_NUMBER; +pub const KEY_ROTATION_VERSION_NUMBER: u8 = 8; +pub const CURRENT_PACKET_VERSION_NUMBER: u8 = KEY_ROTATION_VERSION_NUMBER; pub const CURRENT_PACKET_VERSION: PacketVersion = PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER); +pub const LEGACY_PACKET_VERSION: PacketVersion = + PacketVersion::unchecked(INITIAL_PACKET_VERSION_NUMBER); + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct PacketVersion(u8); @@ -38,6 +43,10 @@ impl PacketVersion { PacketVersion(CURRENT_PACKET_VERSION_NUMBER) } + pub fn is_initial(&self) -> bool { + self.0 == INITIAL_PACKET_VERSION_NUMBER + } + const fn unchecked(version: u8) -> PacketVersion { PacketVersion(version) } diff --git a/common/nymsphinx/src/message.rs b/common/nymsphinx/src/message.rs index 666b2f9336..b1066fd872 100644 --- a/common/nymsphinx/src/message.rs +++ b/common/nymsphinx/src/message.rs @@ -216,7 +216,7 @@ impl NymMessage { chunking::number_of_required_fragments(serialized_len, plaintext_per_packet); // by chunking I mean that currently the fragments hold variable amount of plaintext in them (I wish I had time to rewrite it...) - log::trace!( + tracing::trace!( "this message will use {serialized_len} bytes of PLAINTEXT (This does not account for Ack or chunking overhead). \ With {packet_size:?} PacketSize ({plaintext_per_packet} of usable plaintext available) it will require {num_fragments} packet(s).", ); @@ -242,7 +242,7 @@ impl NymMessage { let wasted_space_percentage = (space_left as f32 / (bytes.len() + 1 + space_left) as f32) * 100.0; - log::trace!( + tracing::trace!( "Padding {self_display}: {} of raw plaintext bytes are required. \ They're going to be put into {packets_used} sphinx packets with {space_left} bytes \ of leftover space. {wasted_space_percentage:.1}% of packet capacity is going to \ diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 0d7915ea14..038f1c4b7a 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -3,7 +3,6 @@ use crate::message::{NymMessage, ACK_OVERHEAD, OUTFOX_ACK_OVERHEAD}; use crate::NymPayloadBuilder; -use log::debug; use nym_crypto::asymmetric::x25519; use nym_crypto::Digest; use nym_sphinx_acknowledgements::surb_ack::SurbAck; @@ -14,12 +13,14 @@ use nym_sphinx_anonymous_replies::reply_surb::ReplySurb; use nym_sphinx_chunking::fragment::{Fragment, FragmentIdentifier}; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; -use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; +use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, SphinxKeyRotation}; use nym_sphinx_types::{Delay, NymPacket}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; +use tracing::*; +use nym_sphinx_anonymous_replies::ReplySurbWithKeyRotation; use nym_sphinx_chunking::monitoring; use std::time::Duration; @@ -52,6 +53,10 @@ pub trait FragmentPreparer { type Rng: CryptoRng + Rng; fn use_legacy_sphinx_format(&self) -> bool; + fn mix_hops_disabled(&self) -> bool { + // Unless otherwise configured, mix hops are enabled + false + } fn deterministic_route_selection(&self) -> bool; fn rng(&mut self) -> &mut Self::Rng; @@ -69,6 +74,7 @@ pub trait FragmentPreparer { ) -> Result { let ack_delay = self.average_ack_delay(); let use_legacy_sphinx_format = self.use_legacy_sphinx_format(); + let disable_mix_hops = self.mix_hops_disabled(); SurbAck::construct( self.rng(), @@ -79,6 +85,7 @@ pub trait FragmentPreparer { ack_delay, topology, packet_type, + disable_mix_hops, ) } @@ -97,10 +104,12 @@ pub trait FragmentPreparer { fragment: Fragment, topology: &NymRouteProvider, ack_key: &AckKey, - reply_surb: ReplySurb, + reply_surb: ReplySurbWithKeyRotation, packet_sender: &Recipient, packet_type: PacketType, ) -> Result { + debug!("Preparing reply chunk for sending"); + // each reply attaches the digest of the encryption key so that the recipient could // lookup correct key for decryption, let reply_overhead = ReplySurbKeyDigestAlgorithm::output_size(); @@ -140,7 +149,7 @@ pub trait FragmentPreparer { // the unwrap here is fine as the failures can only originate from attempting to use invalid payload lengths // and we just very carefully constructed a (presumably) valid one - let (sphinx_packet, first_hop_address) = reply_surb + let applied_surb = reply_surb .apply_surb(packet_payload, packet_size, packet_type) .unwrap(); @@ -149,7 +158,7 @@ pub trait FragmentPreparer { // well as the total delay of the ack packet. // we don't know the delays inside the reply surbs so we use best-effort estimation from our poisson distribution total_delay: expected_forward_delay + ack_delay, - mix_packet: MixPacket::new(first_hop_address, sphinx_packet, packet_type), + mix_packet: MixPacket::from_applied_surb(applied_surb, packet_type), fragment_identifier, }) } @@ -203,6 +212,9 @@ pub trait FragmentPreparer { let packet_size = PacketSize::get_type_from_plaintext(expected_plaintext, packet_type) .expect("the message has been incorrectly fragmented"); + let rotation_id = topology.current_key_rotation(); + let sphinx_key_rotation = SphinxKeyRotation::from(rotation_id); + let fragment_identifier = fragment.fragment_identifier(); // create an ack @@ -222,15 +234,17 @@ pub trait FragmentPreparer { Err(_e) => return Err(NymTopologyError::PayloadBuilder), }; - // generate pseudorandom route for the packet - log::trace!("Preparing chunk for sending"); - let route = if self.deterministic_route_selection() { - log::trace!("using deterministic route selection"); + // generate pseudorandom route for the packet. Unless mix hops are disabled then build an empty route. + trace!("Preparing chunk for sending"); + let route = if self.mix_hops_disabled() { + topology.empty_route_to_egress(destination)? + } else if self.deterministic_route_selection() { + trace!("using deterministic route selection"); let seed = fragment_header.seed().wrapping_mul(self.nonce()); let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); topology.random_route_to_egress(&mut rng, destination)? } else { - log::trace!("using pseudorandom route selection"); + trace!("using pseudorandom route selection"); let mut rng = self.rng(); topology.random_route_to_egress(&mut rng, destination)? }; @@ -269,7 +283,7 @@ pub trait FragmentPreparer { // well as the total delay of the ack packet. // note that the last hop of the packet is a gateway that does not do any delays total_delay: delays.iter().take(delays.len() - 1).sum::() + ack_delay, - mix_packet: MixPacket::new(first_hop_address, packet, packet_type), + mix_packet: MixPacket::new(first_hop_address, packet, packet_type, sphinx_key_rotation), fragment_identifier, }) } @@ -316,6 +330,15 @@ pub struct MessagePreparer { use_legacy_sphinx_format: bool, nonce: i32, + + /// Indicates whether to mix hops or not. If mix hops are enabled, traffic + /// will be routed as usual, to the entry gateway, through three mix nodes, egressing + /// through the exit gateway. If mix hops are disabled, traffic will be routed directly + /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced/disabled mix hops + /// requires use of the updated SURB packet format. + pub disable_mix_hops: bool, } impl MessagePreparer @@ -329,6 +352,7 @@ where average_packet_delay: Duration, average_ack_delay: Duration, use_legacy_sphinx_format: bool, + disable_mix_hops: bool, ) -> Self { let mut rng = rng; let nonce = rng.gen(); @@ -340,6 +364,7 @@ where average_ack_delay, use_legacy_sphinx_format, nonce, + disable_mix_hops, } } @@ -353,8 +378,12 @@ where use_legacy_reply_surb_format: bool, amount: usize, topology: &NymRouteProvider, - ) -> Result, NymTopologyError> { + ) -> Result, NymTopologyError> { let mut reply_surbs = Vec::with_capacity(amount); + let disabled_mix_hops = self.mix_hops_disabled(); + + let key_rotation = SphinxKeyRotation::from(topology.current_key_rotation()); + for _ in 0..amount { let reply_surb = ReplySurb::construct( &mut self.rng, @@ -362,7 +391,9 @@ where self.average_packet_delay, use_legacy_reply_surb_format, topology, - )?; + disabled_mix_hops, + )? + .with_key_rotation(key_rotation); reply_surbs.push(reply_surb) } @@ -374,7 +405,7 @@ where fragment: Fragment, topology: &NymRouteProvider, ack_key: &AckKey, - reply_surb: ReplySurb, + reply_surb: ReplySurbWithKeyRotation, packet_type: PacketType, ) -> Result { let sender = self.sender_address; @@ -442,6 +473,10 @@ where impl FragmentPreparer for MessagePreparer { type Rng = R; + fn mix_hops_disabled(&self) -> bool { + self.disable_mix_hops + } + fn use_legacy_sphinx_format(&self) -> bool { self.use_legacy_sphinx_format } diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index cd620e32b3..aa1a82b848 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -50,6 +50,7 @@ pub enum NymPacketError { FromSlice(#[from] TryFromSliceError), } +// TODO: wrap that guy and add extra metadata to indicate key rotation? #[allow(clippy::large_enum_variant)] pub enum NymPacket { #[cfg(feature = "sphinx")] @@ -179,6 +180,7 @@ impl NymPacket { } #[cfg(feature = "sphinx")] + #[allow(unreachable_patterns)] pub fn sphinx_packet_ref(&self) -> Option<&SphinxPacket> { match self { NymPacket::Sphinx(packet) => Some(packet), @@ -187,6 +189,7 @@ impl NymPacket { } #[cfg(feature = "sphinx")] + #[allow(unreachable_patterns)] pub fn to_sphinx_packet(self) -> Option { match self { NymPacket::Sphinx(packet) => Some(packet), diff --git a/common/nyxd-scraper/Cargo.toml b/common/nyxd-scraper/Cargo.toml index 9eee598af7..025e906d56 100644 --- a/common/nyxd-scraper/Cargo.toml +++ b/common/nyxd-scraper/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -36,5 +37,6 @@ url.workspace = true [build-dependencies] +anyhow = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/common/nyxd-scraper/build.rs b/common/nyxd-scraper/build.rs index cfe3b9c079..89a7681b98 100644 --- a/common/nyxd-scraper/build.rs +++ b/common/nyxd-scraper/build.rs @@ -1,22 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/scraper-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./sql_migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -25,4 +33,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/nyxd-scraper/src/error.rs b/common/nyxd-scraper/src/error.rs index 3337ee969c..9f9bfd12d2 100644 --- a/common/nyxd-scraper/src/error.rs +++ b/common/nyxd-scraper/src/error.rs @@ -26,54 +26,54 @@ pub enum ScraperError { WebSocketConnectionFailure { url: String, #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("failed to establish rpc connection to {url}: {source}")] HttpConnectionFailure { url: String, #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("failed to create chain subscription: {source}")] ChainSubscriptionFailure { #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("could not obtain basic block information at height: {height}: {source}")] BlockQueryFailure { height: u32, #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("could not obtain block results information at height: {height}: {source}")] BlockResultsQueryFailure { height: u32, #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("could not obtain validators information at height: {height}: {source}")] ValidatorsQueryFailure { height: u32, #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("could not obtain tx results for tx: {hash}: {source}")] TxResultsQueryFailure { hash: Hash, #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("could not obtain current abci info: {source}")] AbciInfoQueryFailure { #[source] - source: tendermint_rpc::Error, + source: Box, }, #[error("could not parse tx {hash}: {source}")] diff --git a/common/nyxd-scraper/src/rpc_client.rs b/common/nyxd-scraper/src/rpc_client.rs index f5e3b8c035..b20a89a098 100644 --- a/common/nyxd-scraper/src/rpc_client.rs +++ b/common/nyxd-scraper/src/rpc_client.rs @@ -29,7 +29,7 @@ impl RpcClient { let http_client = HttpClient::new(url.as_str()).map_err(|source| { ScraperError::HttpConnectionFailure { url: url.to_string(), - source, + source: Box::new(source), } })?; @@ -90,7 +90,10 @@ impl RpcClient { self.inner .block(height) .await - .map_err(|source| ScraperError::BlockQueryFailure { height, source }) + .map_err(|source| ScraperError::BlockQueryFailure { + height, + source: Box::new(source), + }) } #[instrument(skip(self), err(Display))] @@ -100,31 +103,37 @@ impl RpcClient { ) -> Result { debug!("getting block results"); - self.inner - .block_results(height) - .await - .map_err(|source| ScraperError::BlockResultsQueryFailure { height, source }) + self.inner.block_results(height).await.map_err(|source| { + ScraperError::BlockResultsQueryFailure { + height, + source: Box::new(source), + } + }) } pub(crate) async fn current_block_height(&self) -> Result { debug!("getting current block height"); - let info = self - .inner - .abci_info() - .await - .map_err(|source| ScraperError::AbciInfoQueryFailure { source })?; + let info = + self.inner + .abci_info() + .await + .map_err(|source| ScraperError::AbciInfoQueryFailure { + source: Box::new(source), + })?; Ok(info.last_block_height.value()) } pub(crate) async fn earliest_available_block_height(&self) -> Result { debug!("getting earliest available block height"); - let status = self - .inner - .status() - .await - .map_err(|source| ScraperError::AbciInfoQueryFailure { source })?; + let status = + self.inner + .status() + .await + .map_err(|source| ScraperError::AbciInfoQueryFailure { + source: Box::new(source), + })?; Ok(status.sync_info.earliest_block_height.value()) } @@ -167,7 +176,7 @@ impl RpcClient { .await .map_err(|source| ScraperError::TxResultsQueryFailure { hash: tx_hash, - source, + source: Box::new(source), }) } @@ -181,6 +190,9 @@ impl RpcClient { self.inner .validators(height, Paging::All) .await - .map_err(|source| ScraperError::ValidatorsQueryFailure { height, source }) + .map_err(|source| ScraperError::ValidatorsQueryFailure { + height, + source: Box::new(source), + }) } } diff --git a/common/nyxd-scraper/src/scraper/subscriber.rs b/common/nyxd-scraper/src/scraper/subscriber.rs index e296a80713..f3a9411647 100644 --- a/common/nyxd-scraper/src/scraper/subscriber.rs +++ b/common/nyxd-scraper/src/scraper/subscriber.rs @@ -42,7 +42,7 @@ impl ChainSubscriber { let websocket_url = websocket_endpoint.as_str().try_into().map_err(|source| { ScraperError::WebSocketConnectionFailure { url: websocket_endpoint.to_string(), - source, + source: Box::new(source), } })?; @@ -52,7 +52,7 @@ impl ChainSubscriber { .await .map_err(|source| ScraperError::WebSocketConnectionFailure { url: websocket_endpoint.to_string(), - source, + source: Box::new(source), })?; Ok(ChainSubscriber { @@ -83,7 +83,7 @@ impl ChainSubscriber { .await .map_err(|source| ScraperError::WebSocketConnectionFailure { url: self.websocket_endpoint.to_string(), - source, + source: Box::new(source), })?; self.websocket_client = client; self.websocket_driver = Some(driver); @@ -121,7 +121,9 @@ impl ChainSubscriber { .websocket_client .subscribe(EventType::NewBlock.into()) .await - .map_err(|source| ScraperError::ChainSubscriptionFailure { source })?; + .map_err(|source| ScraperError::ChainSubscriptionFailure { + source: Box::new(source), + })?; let mut failures = 0; diff --git a/common/nyxd-scraper/src/storage/manager.rs b/common/nyxd-scraper/src/storage/manager.rs index fb40a065b8..fcc3485b95 100644 --- a/common/nyxd-scraper/src/storage/manager.rs +++ b/common/nyxd-scraper/src/storage/manager.rs @@ -111,7 +111,7 @@ impl StorageManager { consensus_address: &str, start_height: i64, end_height: i64, - ) -> Result { + ) -> Result { trace!("get_signed_between"); let start = Instant::now(); diff --git a/common/nyxd-scraper/src/storage/mod.rs b/common/nyxd-scraper/src/storage/mod.rs index 2095bde1e3..e1ff17e5a6 100644 --- a/common/nyxd-scraper/src/storage/mod.rs +++ b/common/nyxd-scraper/src/storage/mod.rs @@ -170,7 +170,7 @@ impl ScraperStorage { consensus_address: &str, start_height: i64, end_height: i64, - ) -> Result { + ) -> Result { Ok(self .manager .get_signed_between(consensus_address, start_height, end_height) @@ -182,7 +182,7 @@ impl ScraperStorage { consensus_address: &str, start_time: OffsetDateTime, end_time: OffsetDateTime, - ) -> Result { + ) -> Result { let Some(block_start) = self.get_first_block_height_after(start_time).await? else { return Ok(0); }; @@ -324,7 +324,13 @@ async fn persist_commits( } => (validator_address, timestamp, signature), }; - let validator = crate::helpers::validator_info(*validator_id, validators)?; + let validator = match crate::helpers::validator_info(*validator_id, validators) { + Ok(validator_info) => validator_info, + Err(err) => { + error!("{err}"); + continue; + } + }; let validator_address = crate::helpers::validator_consensus_address(*validator_id)?; if signature.is_none() { diff --git a/common/pemstore/Cargo.toml b/common/pemstore/Cargo.toml index f8d13391dd..9c93dc122d 100644 --- a/common/pemstore/Cargo.toml +++ b/common/pemstore/Cargo.toml @@ -9,4 +9,5 @@ repository = { workspace = true } [dependencies] pem = { workspace = true } -tracing = { workspace = true } \ No newline at end of file +tracing = { workspace = true } +zeroize = { workspace = true } \ No newline at end of file diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index 9f6d3e283b..91edc5fef5 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -5,12 +5,35 @@ use crate::traits::{PemStorableKey, PemStorableKeyPair}; use pem::Pem; use std::fs::File; use std::io::{self, Read, Write}; +use std::ops::Deref; use std::path::{Path, PathBuf}; use tracing::debug; +use zeroize::{Zeroize, Zeroizing}; pub mod traits; -#[derive(Debug, Default)] +struct ZeroizingPem(Pem); + +impl Zeroize for ZeroizingPem { + fn zeroize(&mut self) { + self.0.tag.zeroize(); + self.0.contents.zeroize(); + } +} +impl Drop for ZeroizingPem { + fn drop(&mut self) { + self.zeroize(); + } +} + +impl Deref for ZeroizingPem { + type Target = Pem; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone, Default)] pub struct KeyPairPath { pub private_key_path: PathBuf, pub public_key_path: PathBuf, @@ -54,14 +77,11 @@ where let key_pem = read_pem_file(path)?; if T::pem_type() != key_pem.tag { - return Err(io::Error::new( - io::ErrorKind::Other, - format!( - "unexpected key pem tag. Got '{}', expected: '{}'", - key_pem.tag, - T::pem_type() - ), - )); + return Err(io::Error::other(format!( + "unexpected key pem tag. Got '{}', expected: '{}'", + key_pem.0.tag, + T::pem_type() + ))); } let key = match T::from_bytes(&key_pem.contents) { @@ -80,25 +100,31 @@ where write_pem_file(path, key.to_bytes(), T::pem_type()) } -fn read_pem_file>(filepath: P) -> io::Result { +fn read_pem_file>(filepath: P) -> io::Result { let mut pem_bytes = File::open(filepath)?; - let mut buf = Vec::new(); + let mut buf = Zeroizing::new(Vec::new()); pem_bytes.read_to_end(&mut buf)?; - pem::parse(&buf).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + pem::parse(&buf).map(ZeroizingPem).map_err(io::Error::other) } -fn write_pem_file>(filepath: P, data: Vec, tag: &str) -> io::Result<()> { +fn write_pem_file>(filepath: P, mut data: Vec, tag: &str) -> io::Result<()> { // ensure the whole directory structure exists if let Some(parent_dir) = filepath.as_ref().parent() { - std::fs::create_dir_all(parent_dir)?; + if let Err(err) = std::fs::create_dir_all(parent_dir) { + // in case of a failure, make sure to zeroize the data before returning + // (we can't wrap it in `Zeroize` due to `Pem` requirements) + data.zeroize(); + return Err(err); + } } - let pem = Pem { - tag: tag.to_string(), - contents: data, - }; - let key = pem::encode(&pem); let mut file = File::create(filepath.as_ref())?; + + let pem = ZeroizingPem(Pem { + tag: tag.to_string(), + contents: data, + }); + let key = Zeroizing::new(pem::encode(&pem)); file.write_all(key.as_bytes())?; // note: this is only supported on unix (on different systems, like Windows, it will just diff --git a/common/socks5-client-core/src/config/mod.rs b/common/socks5-client-core/src/config/mod.rs index 4767eabcdd..9c5e504f1b 100644 --- a/common/socks5-client-core/src/config/mod.rs +++ b/common/socks5-client-core/src/config/mod.rs @@ -13,6 +13,7 @@ use std::str::FromStr; pub mod old_config_v1_1_20_2; pub mod old_config_v1_1_30; pub mod old_config_v1_1_33; +pub mod old_config_v1_1_54; pub use nym_service_providers_common::interface::ProviderInterfaceVersion; pub use nym_socks5_requests::Socks5ProtocolVersion; diff --git a/common/socks5-client-core/src/config/old_config_v1_1_33.rs b/common/socks5-client-core/src/config/old_config_v1_1_33.rs index 715b9d108a..b493cf9113 100644 --- a/common/socks5-client-core/src/config/old_config_v1_1_33.rs +++ b/common/socks5-client-core/src/config/old_config_v1_1_33.rs @@ -1,7 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::{Config, Socks5, Socks5Debug}; +use super::old_config_v1_1_54::ConfigV1_1_54; +use super::{Socks5, Socks5Debug}; pub use nym_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as BaseClientConfigV1_1_33; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -23,9 +24,9 @@ pub struct ConfigV1_1_33 { pub socks5: Socks5V1_1_33, } -impl From for Config { +impl From for ConfigV1_1_54 { fn from(value: ConfigV1_1_33) -> Self { - Config { + ConfigV1_1_54 { base: value.base.into(), socks5: value.socks5.into(), } diff --git a/common/socks5-client-core/src/config/old_config_v1_1_54.rs b/common/socks5-client-core/src/config/old_config_v1_1_54.rs new file mode 100644 index 0000000000..262e21ab2c --- /dev/null +++ b/common/socks5-client-core/src/config/old_config_v1_1_54.rs @@ -0,0 +1,23 @@ +use super::Config; +pub use nym_client_core::config::old_config_v1_1_54::ConfigV1_1_54 as BaseClientConfigV1_1_54; +use serde::{Deserialize, Serialize}; + +use super::Socks5; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_54 { + #[serde(flatten)] + pub base: BaseClientConfigV1_1_54, + + pub socks5: Socks5, +} + +impl From for Config { + fn from(value: ConfigV1_1_54) -> Self { + Config { + base: value.base.into(), + socks5: value.socks5, + } + } +} diff --git a/common/socks5-client-core/src/lib.rs b/common/socks5-client-core/src/lib.rs index 9c112e7928..3c7a4b658d 100644 --- a/common/socks5-client-core/src/lib.rs +++ b/common/socks5-client-core/src/lib.rs @@ -197,7 +197,7 @@ where let res = tokio::select! { biased; message = receiver.next() => { - log::debug!("Received message: {:?}", message); + log::debug!("Received message: {message:?}"); match message { Some(Socks5ControlMessage::Stop) => { log::info!("Received stop message"); @@ -209,7 +209,7 @@ where Ok(()) } Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {:?}", msg); + log::info!("Task error: {msg:?}"); Err(msg) } _ = tokio::signal::ctrl_c() => { @@ -233,7 +233,7 @@ where let dkg_query_client = if self.config.base.client.disabled_credentials_mode { None } else { - Some(default_query_dkg_client_from_config(&self.config.base)) + Some(default_query_dkg_client_from_config(&self.config.base)?) }; let mut base_builder = diff --git a/common/socks5-client-core/src/socks/client.rs b/common/socks5-client-core/src/socks/client.rs index 572849d296..5d79d66380 100644 --- a/common/socks5-client-core/src/socks/client.rs +++ b/common/socks5-client-core/src/socks/client.rs @@ -579,7 +579,7 @@ impl SocksClient { ); // Get valid auth methods let methods = self.get_available_methods().await?; - trace!("methods: {:?}", methods); + trace!("methods: {methods:?}"); let mut response = [0u8; 2]; diff --git a/common/socks5-client-core/src/socks/mixnet_responses.rs b/common/socks5-client-core/src/socks/mixnet_responses.rs index 05cb5bd48d..f74681c8de 100644 --- a/common/socks5-client-core/src/socks/mixnet_responses.rs +++ b/common/socks5-client-core/src/socks/mixnet_responses.rs @@ -61,7 +61,7 @@ impl MixnetResponseListener { control_response: ControlResponse, ) -> Result<(), Socks5ClientCoreError> { error!("received a control response which we don't know how to handle yet!"); - error!("got: {:?}", control_response); + error!("got: {control_response:?}"); // I guess we'd need another channel here to forward those to where they need to go @@ -88,7 +88,7 @@ impl MixnetResponseListener { } Socks5ResponseContent::Query(response) => { error!("received a query response which we don't know how to handle yet!"); - error!("got: {:?}", response); + error!("got: {response:?}"); // I guess we'd need another channel here to forward those to where they need to go diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index e63df37851..6cdf0efa9c 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -122,8 +122,7 @@ where biased; _ = &mut shutdown_future => { debug!( - "closing inbound proxy after outbound was closed {:?} ago", - SHUTDOWN_TIMEOUT + "closing inbound proxy after outbound was closed {SHUTDOWN_TIMEOUT:?} ago" ); // inform remote just in case it was closed because of lack of heartbeat. // worst case the remote will just have couple of false negatives @@ -169,7 +168,7 @@ where } } } - trace!("{} - inbound closed", connection_id); + trace!("{connection_id} - inbound closed"); shutdown_notify.notify_one(); shutdown_listener.disarm(); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index 63d49fb311..dddae8d894 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -72,12 +72,12 @@ pub(super) async fn run_outbound( } } _ = &mut mix_timeout => { - warn!("didn't get anything from the client on {} mixnet in {:?}. Shutting down the proxy.", connection_id, MIX_TTL); + warn!("didn't get anything from the client on {connection_id} mixnet in {MIX_TTL:?}. Shutting down the proxy."); // If they were online it's kinda their fault they didn't send any heartbeat messages. break; } _ = &mut shutdown_future => { - debug!("closing outbound proxy after inbound was closed {:?} ago", SHUTDOWN_TIMEOUT); + debug!("closing outbound proxy after inbound was closed {SHUTDOWN_TIMEOUT:?} ago"); break; } _ = shutdown_listener.recv() => { @@ -87,7 +87,7 @@ pub(super) async fn run_outbound( } } - trace!("{} - outbound closed", connection_id); + trace!("{connection_id} - outbound closed"); shutdown_notify.notify_one(); shutdown_listener.disarm(); diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index 28a5182d00..32d9b4e9e2 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -360,7 +360,7 @@ impl Socks5RequestContent { let query_bytes: Vec = make_bincode_serializer() .serialize(&query) .tap_err(|err| { - log::error!("Failed to serialize query request: {:?}: {err}", query); + log::error!("Failed to serialize query request: {query:?}: {err}"); }) .unwrap_or_default(); std::iter::once(RequestFlag::Query as u8) diff --git a/common/socks5/requests/src/response.rs b/common/socks5/requests/src/response.rs index 584f39df5b..08e45c1de9 100644 --- a/common/socks5/requests/src/response.rs +++ b/common/socks5/requests/src/response.rs @@ -213,7 +213,7 @@ impl Socks5ResponseContent { let query_bytes: Vec = make_bincode_serializer() .serialize(&query) .tap_err(|err| { - log::error!("Failed to serialize query response: {:?}: {err}", query); + log::error!("Failed to serialize query response: {query:?}: {err}"); }) .unwrap_or_default(); std::iter::once(ResponseFlag::Query as u8) diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 892c461861..18a8ca8fc9 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -20,6 +20,8 @@ thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true } si-scale = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } nym-crypto = { path = "../crypto" } nym-sphinx = { path = "../nymsphinx" } @@ -27,5 +29,11 @@ nym-credentials-interface = { path = "../credentials-interface" } nym-metrics = { path = "../nym-metrics" } nym-task = { path = "../task" } +utoipa = { workspace = true, optional = true } + [target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] workspace = true + +[features] +default = [] +openapi = ["dep:utoipa"] diff --git a/common/statistics/src/clients/gateway_conn_statistics.rs b/common/statistics/src/clients/gateway_conn_statistics.rs index 961c7a0f7f..bb42c676d5 100644 --- a/common/statistics/src/clients/gateway_conn_statistics.rs +++ b/common/statistics/src/clients/gateway_conn_statistics.rs @@ -77,7 +77,7 @@ impl GatewayStatsControl { fn report_counters(&self) { log::trace!("packet statistics: {:?}", &self.stats); let (summary_sent, summary_recv) = self.stats.summary(); - log::debug!("{}", summary_sent); - log::debug!("{}", summary_recv); + log::debug!("{summary_sent}"); + log::debug!("{summary_recv}"); } } diff --git a/common/statistics/src/clients/mod.rs b/common/statistics/src/clients/mod.rs index d9c29b48c2..2ca1fa006d 100644 --- a/common/statistics/src/clients/mod.rs +++ b/common/statistics/src/clients/mod.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::report::{ClientStatsReport, OsInformation}; +use crate::report::client::{ClientStatsReport, OsInformation}; use nym_task::TaskClient; use time::{OffsetDateTime, Time}; diff --git a/common/statistics/src/clients/nym_api_statistics.rs b/common/statistics/src/clients/nym_api_statistics.rs index 9c3ee609c0..ddba38c0a8 100644 --- a/common/statistics/src/clients/nym_api_statistics.rs +++ b/common/statistics/src/clients/nym_api_statistics.rs @@ -77,7 +77,7 @@ impl NymApiStatsControl { fn report_counters(&self) { log::trace!("packet statistics: {:?}", &self.stats); let (summary_sent, summary_recv) = self.stats.summary(); - log::debug!("{}", summary_sent); - log::debug!("{}", summary_recv); + log::debug!("{summary_sent}"); + log::debug!("{summary_recv}"); } } diff --git a/common/statistics/src/clients/packet_statistics.rs b/common/statistics/src/clients/packet_statistics.rs index 866215c751..b2c6f56d5c 100644 --- a/common/statistics/src/clients/packet_statistics.rs +++ b/common/statistics/src/clients/packet_statistics.rs @@ -529,8 +529,8 @@ impl PacketStatisticsControl { fn report_counters(&self) { log::trace!("packet statistics: {:?}", &self.stats); let (summary_sent, summary_recv) = self.stats.summary(); - log::debug!("{}", summary_sent); - log::debug!("{}", summary_recv); + log::debug!("{summary_sent}"); + log::debug!("{summary_recv}"); } fn check_for_notable_events(&self) { diff --git a/common/statistics/src/gateways.rs b/common/statistics/src/gateways.rs index 4e8e701de1..79820a6f54 100644 --- a/common/statistics/src/gateways.rs +++ b/common/statistics/src/gateways.rs @@ -1,10 +1,11 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::TicketType; use nym_sphinx::DestinationAddressBytes; use time::OffsetDateTime; +use crate::types::SessionType; + /// Channel for receiving incoming Stats events pub type GatewayStatsReceiver = tokio::sync::mpsc::UnboundedReceiver; @@ -51,15 +52,9 @@ pub enum GatewaySessionEvent { /// Address of the remote client opening the connection client: DestinationAddressBytes, }, - /// A new ecash ticket has been added / requested - EcashTicket { - /// Type of ecash ticket that has been created as part of the session - ticket_type: TicketType, - /// Address of the remote client opening the connection - client: DestinationAddressBytes, - }, - SessionDelete { - /// Address of the remote client opening the connection + /// An active session should be given a type and remembered + SessionRemember { + session_type: SessionType, client: DestinationAddressBytes, }, } @@ -81,18 +76,13 @@ impl GatewaySessionEvent { } } - /// A new ecash ticket has been added / requested - pub fn new_ecash_ticket( + pub fn new_session_remember( + session_type: SessionType, client: DestinationAddressBytes, - ticket_type: TicketType, ) -> GatewaySessionEvent { - GatewaySessionEvent::EcashTicket { - ticket_type, + GatewaySessionEvent::SessionRemember { + session_type, client, } } - - pub fn new_session_delete(client: DestinationAddressBytes) -> GatewaySessionEvent { - GatewaySessionEvent::SessionDelete { client } - } } diff --git a/common/statistics/src/lib.rs b/common/statistics/src/lib.rs index c0ee03dddc..ad105c9f45 100644 --- a/common/statistics/src/lib.rs +++ b/common/statistics/src/lib.rs @@ -22,19 +22,26 @@ pub mod error; pub mod gateways; /// Statistics reporting abstractions and implementations. pub mod report; +/// Statistics related types. +pub mod types; const CLIENT_ID_PREFIX: &str = "client_stats_id"; +const VPN_CLIENT_ID_PREFIX: &str = "vpnclient_stats_id"; pub fn generate_client_stats_id(id_key: ed25519::PublicKey) -> String { generate_stats_id(CLIENT_ID_PREFIX, id_key.to_base58_string()) } +pub fn generate_vpn_client_stats_id>(seed: M) -> String { + generate_stats_id(VPN_CLIENT_ID_PREFIX, seed) +} + fn generate_stats_id>(prefix: &str, id_seed: M) -> String { let mut hasher = sha2::Sha256::new(); hasher.update(prefix); hasher.update(&id_seed); let output = hasher.finalize(); - format!("{:x}", output) + format!("{output:x}") } pub fn hash_identifier>(identifier: M) -> String { diff --git a/common/statistics/src/report.rs b/common/statistics/src/report/client.rs similarity index 98% rename from common/statistics/src/report.rs rename to common/statistics/src/report/client.rs index f01cf2be06..0a1e61364e 100644 --- a/common/statistics/src/report.rs +++ b/common/statistics/src/report/client.rs @@ -6,7 +6,7 @@ use crate::clients::{ nym_api_statistics::NymApiStats, packet_statistics::PacketStatistics, }; -use super::error::StatsError; +use crate::error::StatsError; use serde::{Deserialize, Serialize}; use sysinfo::System; diff --git a/common/statistics/src/report/mod.rs b/common/statistics/src/report/mod.rs new file mode 100644 index 0000000000..27f117f9ff --- /dev/null +++ b/common/statistics/src/report/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod client; +pub mod vpn_client; diff --git a/common/statistics/src/report/vpn_client.rs b/common/statistics/src/report/vpn_client.rs new file mode 100644 index 0000000000..ab1260cae5 --- /dev/null +++ b/common/statistics/src/report/vpn_client.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; + +const KIND: &str = "vpn_client_stats_report"; +const VERSION: &str = "v1"; + +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpnClientStatsReport { + pub kind: String, + pub api_version: String, + pub stats_id: String, + pub static_information: StaticInformationReport, + //SW called it basic so we can swap it easily down the line for more data + pub basic_usage: Option, +} + +impl VpnClientStatsReport { + pub fn new(stats_id: String, static_information: StaticInformationReport) -> Self { + VpnClientStatsReport { + kind: KIND.into(), + api_version: VERSION.into(), + stats_id, + static_information, + basic_usage: None, + } + } + + #[must_use] + pub fn with_usage_report(mut self, usage_report: UsageReport) -> Self { + self.basic_usage = Some(usage_report); + self + } +} +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StaticInformationReport { + pub os_type: String, + pub os_version: Option, + pub os_arch: String, + pub app_version: String, +} + +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UsageReport { + pub connection_time_ms: Option, + pub two_hop: bool, +} diff --git a/common/statistics/src/types.rs b/common/statistics/src/types.rs new file mode 100644 index 0000000000..127738c1cd --- /dev/null +++ b/common/statistics/src/types.rs @@ -0,0 +1,31 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive( + PartialEq, + Copy, + Clone, + strum_macros::Display, + strum_macros::EnumString, + Serialize, + Deserialize, + Default, + Debug, +)] +pub enum SessionType { + Vpn, + Mixnet, + Wasm, + Native, + Socks5, + #[default] + Unknown, +} + +impl SessionType { + pub fn from_string>(s: S) -> Self { + s.as_ref().parse().unwrap_or_default() + } +} diff --git a/common/task/src/cancellation.rs b/common/task/src/cancellation.rs index 8639a272f1..ba60f79308 100644 --- a/common/task/src/cancellation.rs +++ b/common/task/src/cancellation.rs @@ -5,6 +5,7 @@ use crate::{TaskClient, TaskManager}; use futures::stream::FuturesUnordered; use futures::StreamExt; use std::future::Future; +use std::mem; use std::ops::Deref; use std::pin::Pin; use std::time::Duration; @@ -70,6 +71,10 @@ impl ShutdownToken { } } + pub fn ephemeral() -> Self { + ShutdownToken::new("ephemeral-token") + } + // Creates a ShutdownToken which will get cancelled whenever the current token gets cancelled. // Unlike a cloned/forked ShutdownToken, cancelling a child token does not cancel the parent token. #[must_use] @@ -105,6 +110,7 @@ impl ShutdownToken { // exposed method with the old name for easier migration // it will eventually be removed so please try to use `.clone_with_suffix` instead #[must_use] + #[deprecated(note = "use .clone_with_suffix instead")] pub fn fork>(&self, child_suffix: S) -> Self { self.clone_with_suffix(child_suffix) } @@ -112,6 +118,7 @@ impl ShutdownToken { // exposed method with the old name for easier migration // it will eventually be removed so please try to use `.clone().named(name)` instead #[must_use] + #[deprecated(note = "use .clone().named(name) instead")] pub fn fork_named>(&self, name: S) -> Self { self.clone().named(name) } @@ -181,12 +188,21 @@ impl ShutdownDropGuard { } } +#[derive(Default)] +pub struct ShutdownSignals(JoinSet<()>); + +impl ShutdownSignals { + pub async fn wait_for_signal(&mut self) { + self.0.join_next().await; + } +} + pub struct ShutdownManager { pub root_token: ShutdownToken, legacy_task_manager: Option, - shutdown_signals: JoinSet<()>, + shutdown_signals: ShutdownSignals, // the reason I'm not using a `JoinSet` is because it forces us to use futures with the same `::Output` type tracker: TaskTracker, @@ -218,6 +234,16 @@ impl ShutdownManager { manager.with_shutdown(async move { cancel_watcher.cancelled().await }) } + pub fn empty_mock() -> Self { + ShutdownManager { + root_token: ShutdownToken::ephemeral(), + legacy_task_manager: None, + shutdown_signals: Default::default(), + tracker: Default::default(), + max_shutdown_duration: Default::default(), + } + } + pub fn with_legacy_task_manager(mut self) -> Self { let mut legacy_manager = TaskManager::default().named(format!("{}-legacy", self.root_token.name())); @@ -251,13 +277,14 @@ impl ShutdownManager { } #[must_use] + #[track_caller] pub fn with_shutdown(mut self, shutdown: F) -> Self where F: Future, F: Send + 'static, { let shutdown_token = self.root_token.clone(); - self.shutdown_signals.spawn(async move { + self.shutdown_signals.0.spawn(async move { shutdown.await; info!("sending cancellation after receiving shutdown signal"); @@ -267,6 +294,7 @@ impl ShutdownManager { } #[cfg(unix)] + #[track_caller] pub fn with_shutdown_signal(self, signal_kind: SignalKind) -> std::io::Result { let mut sig = signal(signal_kind)?; Ok(self.with_shutdown(async move { @@ -275,6 +303,7 @@ impl ShutdownManager { } #[cfg(not(target_arch = "wasm32"))] + #[track_caller] pub fn with_interrupt_signal(self) -> Self { self.with_shutdown(async move { let _ = tokio::signal::ctrl_c().await; @@ -282,11 +311,13 @@ impl ShutdownManager { } #[cfg(unix)] + #[track_caller] pub fn with_terminate_signal(self) -> std::io::Result { self.with_shutdown_signal(SignalKind::terminate()) } #[cfg(unix)] + #[track_caller] pub fn with_quit_signal(self) -> std::io::Result { self.with_shutdown_signal(SignalKind::quit()) } @@ -352,9 +383,20 @@ impl ShutdownManager { wait_futures.next().await; } - pub async fn wait_for_shutdown_signal(mut self) { - self.shutdown_signals.join_next().await; + pub fn detach_shutdown_signals(&mut self) -> ShutdownSignals { + mem::take(&mut self.shutdown_signals) + } + pub fn replace_shutdown_signals(&mut self, signals: ShutdownSignals) { + self.shutdown_signals = signals; + } + + // cancellation safe + pub async fn wait_for_shutdown_signal(&mut self) { + self.shutdown_signals.0.join_next().await; + } + + pub async fn perform_shutdown(mut self) { if let Some(legacy_manager) = self.legacy_task_manager.as_mut() { info!("attempting to shutdown legacy tasks"); let _ = legacy_manager.signal_shutdown(); @@ -363,4 +405,10 @@ impl ShutdownManager { info!("waiting for tasks to finish... (press ctrl-c to force)"); self.finish_shutdown().await; } + + pub async fn run_until_shutdown(mut self) { + self.wait_for_shutdown_signal().await; + + self.perform_shutdown().await; + } } diff --git a/common/task/src/connections.rs b/common/task/src/connections.rs index 8557b84676..35f448c622 100644 --- a/common/task/src/connections.rs +++ b/common/task/src/connections.rs @@ -94,7 +94,7 @@ impl LaneQueueLengths { log::warn!("Timeout reached while waiting for queue to clear"); break; } - log::trace!("Waiting for queue to clear ({} items left)", lane_length); + log::trace!("Waiting for queue to clear ({lane_length} items left)"); tokio::time::sleep(Duration::from_millis(100)).await; } } diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index ba605f5a55..d5af78fcc0 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -163,7 +163,7 @@ impl TaskManager { // Announce that we are operational. This means that in the application where this is used, // everything is up and running and ready to go. if let Err(msg) = sender.send(Box::new(start_status)).await { - log::error!("Error sending status message: {}", msg); + log::error!("Error sending status message: {msg}"); }; if let Some(mut task_status_rx) = self.task_status_rx.take() { diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs index 483c7c630b..ebaab7b20f 100644 --- a/common/task/src/signal.rs +++ b/common/task/src/signal.rs @@ -49,7 +49,7 @@ pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), Ok(()) } Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {:?}", msg); + log::info!("Task error: {msg:?}"); Err(msg) } } @@ -63,7 +63,7 @@ pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), Ok(()) }, Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {:?}", msg); + log::info!("Task error: {msg:?}"); Err(msg) } } diff --git a/common/task/src/spawn.rs b/common/task/src/spawn.rs index 16f77299d1..e0fe98c5de 100644 --- a/common/task/src/spawn.rs +++ b/common/task/src/spawn.rs @@ -10,6 +10,7 @@ where } #[cfg(not(target_arch = "wasm32"))] +#[track_caller] pub fn spawn(future: F) where F: Future + Send + 'static, @@ -18,6 +19,7 @@ where tokio::spawn(future); } +#[track_caller] pub fn spawn_with_report_error(future: F, mut shutdown: TaskClient) where F: Future> + Send + 'static, diff --git a/common/test-utils/Cargo.toml b/common/test-utils/Cargo.toml new file mode 100644 index 0000000000..8c93779702 --- /dev/null +++ b/common/test-utils/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nym-test-utils" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +futures = { workspace = true } +rand_chacha = { workspace = true } +tokio = { workspace = true, features = ["sync", "time", "rt"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/common/test-utils/src/helpers.rs b/common/test-utils/src/helpers.rs new file mode 100644 index 0000000000..26dd19f3b1 --- /dev/null +++ b/common/test-utils/src/helpers.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::traits::Timeboxed; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; +use std::future::Future; +use tokio::task::JoinHandle; +use tokio::time::error::Elapsed; + +pub fn leak(val: T) -> &'static mut T { + Box::leak(Box::new(val)) +} + +pub fn spawn_timeboxed(fut: F) -> JoinHandle> +where + F: Future + Send + 'static, + ::Output: Send, +{ + tokio::spawn(async move { fut.timeboxed().await }) +} + +pub fn deterministic_rng() -> ChaCha20Rng { + seeded_rng([42u8; 32]) +} + +pub fn seeded_rng(seed: [u8; 32]) -> ChaCha20Rng { + ChaCha20Rng::from_seed(seed) +} + +pub fn u64_seeded_rng(seed: u64) -> ChaCha20Rng { + ChaCha20Rng::seed_from_u64(seed) +} diff --git a/common/test-utils/src/lib.rs b/common/test-utils/src/lib.rs new file mode 100644 index 0000000000..b1818473cc --- /dev/null +++ b/common/test-utils/src/lib.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod helpers; +pub mod mocks; +pub mod traits; diff --git a/common/test-utils/src/mocks/async_read_write.rs b/common/test-utils/src/mocks/async_read_write.rs new file mode 100644 index 0000000000..859c815882 --- /dev/null +++ b/common/test-utils/src/mocks/async_read_write.rs @@ -0,0 +1,161 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mocks::shared::InnerWrapper; +use futures::ready; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; + +// sending buffer of the first stream is the receiving buffer of the second stream +// and vice versa +pub fn mock_io_streams() -> (MockIOStream, MockIOStream) { + let ch1 = MockIOStream::default(); + let ch2 = ch1.make_connection(); + + (ch1, ch2) +} + +#[derive(Default)] +pub struct MockIOStream { + // messages to send + tx: InnerWrapper>, + + // messages to receive + rx: InnerWrapper>, +} + +impl MockIOStream { + fn make_connection(&self) -> Self { + MockIOStream { + tx: self.rx.cloned_buffer(), + rx: self.tx.cloned_buffer(), + } + } + + // unwrap in test code is fine + #[allow(clippy::unwrap_used)] + pub fn unchecked_tx_data(&self) -> Vec { + self.tx.buffer.try_lock().unwrap().content.clone() + } + + // unwrap in test code is fine + #[allow(clippy::unwrap_used)] + pub fn unchecked_rx_data(&self) -> Vec { + self.rx.buffer.try_lock().unwrap().content.clone() + } +} + +impl AsyncRead for MockIOStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + ready!(Pin::new(&mut self.rx).poll_guard_ready(cx)); + + // SAFETY: guard is ready + #[allow(clippy::unwrap_used)] + let guard = self.rx.guard().unwrap(); + + let data = guard.take_content(); + if data.is_empty() { + // nothing to retrieve - store the waiter so that the sender could trigger it + guard.waker = Some(cx.waker().clone()); + + // drop the guard so that the sender could actually put messages in + self.rx.transition_to_idle(); + return Poll::Pending; + } + + // if let Some(waker) = guard.waker.take() { + // waker.wake(); + // } + + self.rx.transition_to_idle(); + + buf.put_slice(&data); + Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for MockIOStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + // wait until we transition to the locked state + ready!(Pin::new(&mut self.tx).poll_guard_ready(cx)); + + // SAFETY: guard is ready + #[allow(clippy::unwrap_used)] + let guard = self.tx.guard().unwrap(); + + let len = buf.len(); + guard.content.extend_from_slice(buf); + + // TODO: if we wanted the behaviour of always reading everything before writing anything extra + // if !guard.content.is_empty() { + // // sanity check + // assert!(guard.waker.is_none()); + // guard.waker = Some(cx.waker().clone()); + // self.tx.transition_to_idle(); + // return Poll::Pending; + // } + + Poll::Ready(Ok(len)) + } + + fn poll_flush(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + let Some(guard) = self.tx.guard() else { + return Poll::Ready(Err(io::Error::other( + "invalid lock state to send/flush messages", + ))); + }; + + if let Some(waker) = guard.waker.take() { + // notify the receiver if it was waiting for messages + waker.wake(); + } + + // release the guard + self.tx.transition_to_idle(); + + Poll::Ready(Ok(())) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + // make sure our guard is always dropped on close + self.tx.transition_to_idle(); + + Poll::Ready(Ok(())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + #[tokio::test] + async fn basic() { + let (mut stream1, mut stream2) = mock_io_streams(); + stream1.write_all(&[1, 2, 3, 4, 5]).await.unwrap(); + stream1.flush().await.unwrap(); + + let mut buf = [0u8; 5]; + let read = stream2.read(&mut buf).await.unwrap(); + assert_eq!(read, 5); + assert_eq!(&buf[0..5], &[1, 2, 3, 4, 5]); + + let mut buf = [0u8; 5]; + stream2.write_all(&[6, 7, 8, 9, 10]).await.unwrap(); + stream2.flush().await.unwrap(); + + let read = stream1.read(&mut buf).await.unwrap(); + assert_eq!(read, 5); + assert_eq!(&buf[0..5], &[6, 7, 8, 9, 10]); + } +} diff --git a/common/test-utils/src/mocks/mod.rs b/common/test-utils/src/mocks/mod.rs new file mode 100644 index 0000000000..9962160d2a --- /dev/null +++ b/common/test-utils/src/mocks/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod async_read_write; +mod shared; +pub mod stream_sink; diff --git a/common/test-utils/src/mocks/shared.rs b/common/test-utils/src/mocks/shared.rs new file mode 100644 index 0000000000..c760cd9b5d --- /dev/null +++ b/common/test-utils/src/mocks/shared.rs @@ -0,0 +1,109 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::future::BoxFuture; +use futures::{ready, FutureExt}; +use std::mem; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll, Waker}; +use tokio::sync::{Mutex, OwnedMutexGuard}; + +#[derive(Default)] +pub(crate) struct InnerWrapper { + pub(crate) buffer: Arc>>, + lock_state: LockState, +} + +impl InnerWrapper { + pub(crate) fn clone_buffer(&self) -> Arc>> { + Arc::clone(&self.buffer) + } + + pub(crate) fn cloned_buffer(&self) -> Self { + assert!(matches!(self.lock_state, LockState::Idle)); + InnerWrapper { + buffer: self.clone_buffer(), + lock_state: LockState::Idle, + } + } + + // NOTE: it's responsibility of the caller to ensure the guard is released and state transitions to idle! + pub(crate) fn poll_guard_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + match &mut self.lock_state { + LockState::Idle => { + // 1. first try to obtain the guard without locking + let Ok(guard) = self.buffer.clone().try_lock_owned() else { + // 2. if that fails, create the future for obtaining it + self.lock_state = + LockState::TryingToLock(self.buffer.clone().lock_owned().boxed()); + return Poll::Pending; + }; + + // correctly transition to locked state and poll ourselves again + self.lock_state = LockState::Locked(guard); + cx.waker().wake_by_ref(); + Poll::Ready(()) + } + + LockState::TryingToLock(lock_fut) => { + // see if the guard future has resolved, if so, transition to locked state and schedule for another poll + let guard = ready!(lock_fut.as_mut().poll(cx)); + self.lock_state = LockState::Locked(guard); + cx.waker().wake_by_ref(); + Poll::Pending + } + + LockState::Locked(_) => Poll::Ready(()), + } + } + + pub(crate) fn guard(&mut self) -> Option<&mut OwnedMutexGuard>> { + match &mut self.lock_state { + LockState::Locked(guard) => Some(guard), + _ => None, + } + } + + pub(crate) fn transition_to_idle(&mut self) { + self.lock_state = LockState::Idle + } +} + +#[derive(Default)] +pub(crate) enum LockState { + // We haven’t started locking yet + #[default] + Idle, + + // Waiting for the mutex lock future to resolve + TryingToLock(BoxFuture<'static, OwnedMutexGuard>>), + + // We hold the mutex guard + Locked(OwnedMutexGuard>), +} + +#[derive(Default)] +pub struct ContentWrapper { + pub(crate) content: T, + pub(crate) waker: Option, +} + +impl ContentWrapper { + pub fn into_content(self) -> T { + self.content + } + + pub fn content(&self) -> &T { + &self.content + } + + pub(crate) fn take_content(&mut self) -> T + where + T: Default, + { + mem::take(&mut self.content) + } +} + +impl LockState {} diff --git a/common/test-utils/src/mocks/stream_sink.rs b/common/test-utils/src/mocks/stream_sink.rs new file mode 100644 index 0000000000..0cd866092e --- /dev/null +++ b/common/test-utils/src/mocks/stream_sink.rs @@ -0,0 +1,181 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mocks::shared::{ContentWrapper, InnerWrapper}; +use anyhow::{anyhow, bail}; +use futures::{ready, Sink, Stream}; +use std::collections::VecDeque; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::sync::Mutex; + +// sending buffer of the first stream is the receiving buffer of the second stream +// and vice versa +pub fn mock_streams() -> (MockStream, MockStream) +where + T: Send, +{ + let ch1 = MockStream::default(); + let ch2 = ch1.make_connection(); + + (ch1, ch2) +} + +pub struct MockStream { + // messages to send + tx: InnerWrapper>, + + // messages to receive + rx: InnerWrapper>, +} + +impl MockStream { + pub fn clone_tx_buffer(&self) -> Arc>>> + where + T: Send, + { + self.tx.clone_buffer() + } + + pub fn clone_rx_buffer(&self) -> Arc>>> + where + T: Send, + { + self.rx.clone_buffer() + } + + fn make_connection(&self) -> Self + where + T: Send, + { + MockStream { + tx: self.rx.cloned_buffer(), + rx: self.tx.cloned_buffer(), + } + } +} + +impl Default for MockStream { + fn default() -> Self { + MockStream { + tx: InnerWrapper::default(), + rx: InnerWrapper::default(), + } + } +} + +impl Stream for MockStream +where + T: Send, +{ + type Item = T; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + ready!(Pin::new(&mut self.rx).poll_guard_ready(cx)); + + // SAFETY: guard is ready + #[allow(clippy::unwrap_used)] + let guard = self.rx.guard().unwrap(); + + let Some(next) = guard.content.pop_front() else { + // nothing to retrieve - store the waiter so that the sender could trigger it + guard.waker = Some(cx.waker().clone()); + + // drop the guard so that the sender could actually put messages in + self.rx.transition_to_idle(); + return Poll::Pending; + }; + + // there are more messages buffered waiting for us to retrieve + // keep the guard! + if !guard.content.is_empty() { + cx.waker().wake_by_ref(); + } else { + // no more messages, drop the guard + self.rx.transition_to_idle(); + } + + Poll::Ready(Some(next)) + } + + fn size_hint(&self) -> (usize, Option) { + // that's just a minor optimisation, so don't sweat about it too much, + // if we can obtain the mutex, give precise information, otherwise return default values + let Ok(guard) = self.rx.buffer.try_lock() else { + return (0, None); + }; + let items = guard.content.len(); + (items, Some(items)) + } +} + +impl Sink for MockStream +where + T: Send, +{ + type Error = anyhow::Error; + + fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // wait until we transition to the locked state + ready!(Pin::new(&mut self.tx).poll_guard_ready(cx)); + Poll::Ready(Ok(())) + } + + fn start_send(mut self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> { + let Some(guard) = self.tx.guard() else { + bail!("invalid lock state to send messages"); + }; + guard.content.push_back(item); + + Ok(()) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + let Some(guard) = self.tx.guard() else { + return Poll::Ready(Err(anyhow!("invalid lock state to send/flush messages"))); + }; + + if let Some(waker) = guard.waker.take() { + // notify the receiver if it was waiting for messages + waker.wake(); + } + + // release the guard + self.tx.transition_to_idle(); + + Poll::Ready(Ok(())) + } + + fn poll_close( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + // make sure our guard is always dropped on close + self.tx.transition_to_idle(); + + Poll::Ready(Ok(())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::{SinkExt, StreamExt}; + + #[tokio::test] + async fn basic() { + let (mut stream1, mut stream2) = mock_streams(); + stream1.send("foomp").await.unwrap(); + + let received = stream2.next().await.unwrap(); + assert_eq!(received, "foomp"); + + stream2.send("bar").await.unwrap(); + let received = stream1.next().await.unwrap(); + assert_eq!(received, "bar"); + } +} diff --git a/common/test-utils/src/traits.rs b/common/test-utils/src/traits.rs new file mode 100644 index 0000000000..9b95dbd6bf --- /dev/null +++ b/common/test-utils/src/traits.rs @@ -0,0 +1,57 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::{leak, spawn_timeboxed}; +use std::future::{Future, IntoFuture}; +use std::time::Duration; +use tokio::task::JoinHandle; +use tokio::time::error::Elapsed; + +// a helper trait for use in tests to easily convert `T` into `&'static mut T` +pub trait Leak { + fn leak(self) -> &'static mut Self; +} + +impl Leak for T { + fn leak(self) -> &'static mut T { + leak(self) + } +} + +// those are internal testing traits so we're not concerned about auto traits +#[allow(async_fn_in_trait)] +pub trait Timeboxed: IntoFuture + Sized { + async fn timeboxed(self) -> Result { + self.execute_with_deadline(Duration::from_millis(200)).await + } + + async fn execute_with_deadline(self, timeout: Duration) -> Result { + tokio::time::timeout(timeout, self).await + } +} + +impl Timeboxed for T where T: IntoFuture + Sized {} + +// those are internal testing traits so we're not concerned about auto traits +#[allow(async_fn_in_trait)] +pub trait Spawnable: Future + Sized + Send + 'static { + fn spawn(self) -> JoinHandle + where + ::Output: Send + 'static, + { + tokio::spawn(self) + } +} + +impl Spawnable for T where T: Future + Sized + Send + 'static {} + +pub trait TimeboxedSpawnable: Timeboxed + Spawnable { + fn spawn_timeboxed(self) -> JoinHandle::Output, Elapsed>> + where + ::Output: Send, + { + spawn_timeboxed(self) + } +} + +impl TimeboxedSpawnable for T where T: Spawnable + Future + Send {} diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index ba34832b04..64d60171f4 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -17,6 +17,7 @@ rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +time = { workspace = true, features = ["serde"] } # 'serde' feature serde_json = { workspace = true, optional = true } @@ -26,7 +27,6 @@ tsify = { workspace = true, features = ["js"], optional = true } wasm-bindgen = { workspace = true, optional = true } ## internal -nym-config = { path = "../config" } nym-crypto = { path = "../crypto" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-sphinx-addressing = { path = "../nymsphinx/addressing" } @@ -34,7 +34,6 @@ nym-sphinx-types = { path = "../nymsphinx/types", features = [ "sphinx", "outfox", ] } -nym-sphinx-routing = { path = "../nymsphinx/routing" } # I'm not sure how to feel about pulling in this dependency here... diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 4354721bc5..b6d95e356e 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,6 +3,8 @@ use ::serde::{Deserialize, Serialize}; use nym_api_requests::nym_nodes::SkimmedNode; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::EpochId; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::IteratorRandom; @@ -11,6 +13,7 @@ use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::net::IpAddr; +use time::OffsetDateTime; use tracing::{debug, trace, warn}; pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles}; @@ -91,8 +94,48 @@ mod deprecated_network_address_impls { pub type MixLayer = u8; +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct NymTopologyMetadata { + pub key_rotation_id: u32, + // we have to keep track of key rotation id anyway, so we might as well also include the epoch id + // to keep track of the data staleness + pub absolute_epoch_id: EpochId, + + #[serde(with = "time::serde::rfc3339")] + pub refreshed_at: OffsetDateTime, +} + +impl NymTopologyMetadata { + pub fn new( + key_rotation_id: u32, + absolute_epoch_id: EpochId, + refreshed_at: impl Into, + ) -> Self { + NymTopologyMetadata { + key_rotation_id, + absolute_epoch_id, + refreshed_at: refreshed_at.into(), + } + } +} + +impl Default for NymTopologyMetadata { + fn default() -> Self { + // that's not ideal, but we don't want to break backwards compatibility : / + NymTopologyMetadata { + key_rotation_id: u32::MAX, + absolute_epoch_id: 0, + refreshed_at: OffsetDateTime::now_utc(), + } + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct NymTopology { + // while this is not ideal, use empty values as default to not break backwards compatibility + #[serde(default)] + metadata: NymTopologyMetadata, + // for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering // so we use the same 'master' rewarded set information for that // @@ -128,6 +171,18 @@ impl NymRouteProvider { } } + pub fn current_key_rotation(&self) -> u32 { + self.topology.metadata.key_rotation_id + } + + pub fn absolute_epoch_id(&self) -> EpochId { + self.topology.metadata.absolute_epoch_id + } + + pub fn metadata(&self) -> NymTopologyMetadata { + self.topology.metadata + } + pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider { let this: Self = NymTopology::default().into(); this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles) @@ -176,6 +231,17 @@ impl NymRouteProvider { .random_route_to_egress(rng, egress_identity, self.ignore_egress_epoch_roles) } + /// Returns a route directly to the egress point, which can be any known node + pub fn empty_route_to_egress( + &self, + egress_identity: NodeIdentity, + ) -> Result, NymTopologyError> { + let egress = self + .topology + .egress_node_by_identity(egress_identity, self.ignore_egress_epoch_roles)?; + Ok(vec![egress]) + } + pub fn random_path_to_egress( &self, rng: &mut R, @@ -190,18 +256,22 @@ impl NymRouteProvider { } impl NymTopology { + #[deprecated] pub fn new_empty(rewarded_set: impl Into) -> Self { NymTopology { + metadata: NymTopologyMetadata::default(), rewarded_set: rewarded_set.into(), node_details: Default::default(), } } pub fn new( + metadata: NymTopologyMetadata, rewarded_set: impl Into, node_details: Vec, ) -> Self { NymTopology { + metadata, rewarded_set: rewarded_set.into(), node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(), } @@ -217,6 +287,11 @@ impl NymTopology { self.add_additional_nodes(nodes.iter()) } + pub fn with_skimmed_nodes(mut self, nodes: &[SkimmedNode]) -> Self { + self.add_skimmed_nodes(nodes); + self + } + pub fn add_routing_nodes>( &mut self, nodes: impl IntoIterator, @@ -267,6 +342,12 @@ impl NymTopology { self.node_details.contains_key(&node_id) } + pub fn has_node(&self, identity: ed25519::PublicKey) -> bool { + self.node_details + .values() + .any(|node_details| node_details.identity_key == identity) + } + pub fn insert_node_details(&mut self, node_details: RoutingNode) { self.node_details.insert(node_details.node_id, node_details); } diff --git a/common/topology/src/provider_trait.rs b/common/topology/src/provider_trait.rs index ad8381fa7d..b0b9126525 100644 --- a/common/topology/src/provider_trait.rs +++ b/common/topology/src/provider_trait.rs @@ -1,8 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::NymTopology; +use crate::{NymTopology, NymTopologyMetadata}; pub use async_trait::async_trait; +use nym_api_requests::nym_nodes::NodesResponseMetadata; // hehe, wasm #[cfg(not(target_arch = "wasm32"))] @@ -47,3 +48,15 @@ impl TopologyProvider for HardcodedTopologyProvider { Some(self.topology.clone()) } } + +// helper trait to convert between nym-api response and the topology metadata +// (we don't want to be importing any of those in the other crates) +pub trait ToTopologyMetadata { + fn to_topology_metadata(&self) -> NymTopologyMetadata; +} + +impl ToTopologyMetadata for NodesResponseMetadata { + fn to_topology_metadata(&self) -> NymTopologyMetadata { + NymTopologyMetadata::new(self.rotation_id, self.absolute_epoch_id, self.refreshed_at) + } +} diff --git a/common/topology/src/wasm_helpers.rs b/common/topology/src/wasm_helpers.rs index ecb451e8be..5ebce45455 100644 --- a/common/topology/src/wasm_helpers.rs +++ b/common/topology/src/wasm_helpers.rs @@ -5,7 +5,7 @@ #![allow(clippy::empty_docs)] use crate::node::{EntryDetails, RoutingNode, RoutingNodeError, SupportedRoles}; -use crate::{CachedEpochRewardedSet, NymTopology}; +use crate::{CachedEpochRewardedSet, NymTopology, NymTopologyMetadata}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; @@ -38,6 +38,8 @@ impl From for JsValue { #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub struct WasmFriendlyNymTopology { + pub metadata: NymTopologyMetadata, + pub rewarded_set: CachedEpochRewardedSet, pub node_details: HashMap, @@ -53,13 +55,18 @@ impl TryFrom for NymTopology { .map(|details| details.try_into()) .collect::>()?; - Ok(NymTopology::new(value.rewarded_set, node_details)) + Ok(NymTopology::new( + value.metadata, + value.rewarded_set, + node_details, + )) } } impl From for WasmFriendlyNymTopology { fn from(value: NymTopology) -> Self { WasmFriendlyNymTopology { + metadata: value.metadata, rewarded_set: value.rewarded_set, node_details: value .node_details diff --git a/common/tun/src/linux/tun_device.rs b/common/tun/src/linux/tun_device.rs index ab3bbd71b7..936a141b3f 100644 --- a/common/tun/src/linux/tun_device.rs +++ b/common/tun/src/linux/tun_device.rs @@ -157,7 +157,7 @@ impl TunDevice { "-6", "addr", "add", - &format!("{}/{}", ipv6, netmaskv6), + &format!("{ipv6}/{netmaskv6}"), "dev", (tun.name()), ]) diff --git a/common/types/Cargo.toml b/common/types/Cargo.toml index 6ca8a3d9e4..620ec264d1 100644 --- a/common/types/Cargo.toml +++ b/common/types/Cargo.toml @@ -19,6 +19,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } thiserror = { workspace = true } ts-rs = { workspace = true } url = { workspace = true } diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index 9d13bfbc56..0810f278de 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; use std::fmt::{Display, Formatter}; -use strum::{Display, EnumString, VariantNames}; +use strum_macros::{Display, EnumString, VariantNames}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 2a4595a495..823ec8b375 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -84,6 +84,8 @@ pub enum TypesError { NotADelegationEvent, #[error("Unknown network - {0}")] UnknownNetwork(String), + #[error("the response metadata has changed between pages")] + InconsistentPagedMetadata, } impl Serialize for TypesError { @@ -103,6 +105,9 @@ impl From for TypesError { ValidatorClientError::NyxdError(e) => e.into(), ValidatorClientError::NoAPIUrlAvailable => TypesError::NoNymApiUrlConfigured, ValidatorClientError::TendermintErrorRpc(err) => err.into(), + ValidatorClientError::InconsistentPagedMetadata => { + TypesError::InconsistentPagedMetadata + } } } } diff --git a/common/upgrade-mode-check/Cargo.toml b/common/upgrade-mode-check/Cargo.toml new file mode 100644 index 0000000000..50e0cb3f7c --- /dev/null +++ b/common/upgrade-mode-check/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "nym-upgrade-mode-check" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +jwt-simple = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +time = { workspace = true, features = ["serde"] } +thiserror = { workspace = true } +tracing = { workspace = true } + +nym-http-api-client = { path = "../http-api-client", default-features = false } +nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] } + +[dev-dependencies] +anyhow = { workspace = true } +time = { workspace = true, features = ["macros"] } + +[lints] +workspace = true diff --git a/common/upgrade-mode-check/src/attestation.rs b/common/upgrade-mode-check/src/attestation.rs new file mode 100644 index 0000000000..42c115f3cd --- /dev/null +++ b/common/upgrade-mode-check/src/attestation.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::UpgradeModeCheckError; +use nym_crypto::asymmetric::ed25519; +use nym_http_api_client::generate_user_agent; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +pub struct UpgradeModeAttestation { + #[serde(flatten)] + pub content: UpgradeModeAttestationContent, + + #[serde(with = "ed25519::bs58_ed25519_signature")] + pub signature: ed25519::Signature, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +#[serde(tag = "type")] +#[serde(rename = "upgrade_mode")] +pub struct UpgradeModeAttestationContent { + #[serde(with = "time::serde::timestamp")] + pub starting_time: OffsetDateTime, + + #[serde(with = "ed25519::bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, +} + +impl UpgradeModeAttestation { + pub fn verify(&self) -> bool { + self.content + .attester_public_key + .verify(self.content.as_json(), &self.signature) + .is_ok() + } +} + +impl UpgradeModeAttestationContent { + pub fn as_json(&self) -> String { + // SAFETY: Serialize impl is valid and we have no non-string map keys + #[allow(clippy::unwrap_used)] + serde_json::to_string(&self).unwrap() + } +} + +pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation { + generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc()) +} + +pub fn generate_new_attestation_with_starting_time( + key: &ed25519::PrivateKey, + starting_time: OffsetDateTime, +) -> UpgradeModeAttestation { + let content = UpgradeModeAttestationContent { + starting_time, + attester_public_key: key.into(), + }; + UpgradeModeAttestation { + signature: key.sign(content.as_json()), + content, + } +} + +pub async fn attempt_retrieve( + url: &str, +) -> Result, UpgradeModeCheckError> { + let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure { + url: url.to_string(), + source, + }; + + let attestation = reqwest::ClientBuilder::new() + .user_agent(generate_user_agent!()) + .timeout(Duration::from_secs(5)) + .build() + .map_err(retrieval_failure)? + .get(url) + .send() + .await + .map_err(retrieval_failure)? + .json::>() + .await + .map_err(retrieval_failure)?; + + Ok(attestation) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn upgrade_mode_attestation_serde_json() -> anyhow::Result<()> { + // unix timestamp: 1629720000 + let starting_time = time::macros::datetime!(2021-08-23 12:00 UTC); + + let key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ])?; + + let attestation = generate_new_attestation_with_starting_time(&key, starting_time); + + let attestation_json = serde_json::to_string(&attestation)?; + let attestation_content_json = attestation.content.as_json(); + + let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#; + let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#; + + assert_eq!(attestation_content_json, expected_content); + assert_eq!(attestation_json, expected_attestation); + + let recovered_attestation = serde_json::from_str(&attestation_json)?; + assert_eq!(attestation, recovered_attestation); + + let recovered_content = serde_json::from_str(&attestation_content_json)?; + assert_eq!(attestation.content, recovered_content); + + Ok(()) + } +} diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs new file mode 100644 index 0000000000..d1f26b7c5f --- /dev/null +++ b/common/upgrade-mode-check/src/error.rs @@ -0,0 +1,23 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum UpgradeModeCheckError { + #[error("failed to decode jwt metadata")] + TokenMetadataDecodeFailure { source: jwt_simple::Error }, + + #[error("the jwt metadata didn't contain explicit public key")] + MissingTokenPublicKey, + + #[error("the attached public key was not valid ed25519 public key")] + MalformedEd25519PublicKey { source: Ed25519RecoveryError }, + + #[error("failed to verify the jwt: {source}")] + JwtVerificationFailure { source: jwt_simple::Error }, + + #[error("failed to retrieve attestation from {url}:{source}")] + AttestationRetrievalFailure { url: String, source: reqwest::Error }, +} diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs new file mode 100644 index 0000000000..93be406ab2 --- /dev/null +++ b/common/upgrade-mode-check/src/jwt.rs @@ -0,0 +1,119 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{UpgradeModeAttestation, UpgradeModeCheckError}; +use jwt_simple::claims::Claims; +use jwt_simple::common::{KeyMetadata, VerificationOptions}; +use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike}; +use jwt_simple::token::Token; +use nym_crypto::asymmetric::ed25519; +use std::collections::HashSet; +use std::time::Duration; + +// for now use static issuer such as "nym-credential-proxy" +pub fn generate_jwt_for_upgrade_mode_attestation( + attestation: UpgradeModeAttestation, + validity: Duration, + keys: &ed25519::KeyPair, + issuer: Option<&'static str>, +) -> String { + let claim = Claims::with_custom_claims(attestation, validity.into()); + let mut claim = if let Some(issuer) = issuer { + claim.with_issuer(issuer) + } else { + claim + }; + claim.create_nonce(); + + let md = KeyMetadata::default().with_public_key(keys.public_key().to_base58_string()); + + let mut jwt_keys = keys.to_jwt_compatible_keys(); + // SAFETY: trait impl for EdDSA is infallible + #[allow(clippy::unwrap_used)] + jwt_keys.attach_metadata(md).unwrap(); + + // SAFETY: our construction of the jwt is valid + #[allow(clippy::unwrap_used)] + jwt_keys.sign(claim).unwrap() +} + +pub fn validate_upgrade_mode_jwt( + token: &str, + expected_issuer: Option<&'static str>, +) -> Result { + // for now, we completely ignore the validity of the pubkey (I know, I know). + // that will be changed later on + // so as a bypass we have to extract the claimed issuer from the jwt to verify against it + let metadata = Token::decode_metadata(token) + .map_err(|source| UpgradeModeCheckError::TokenMetadataDecodeFailure { source })?; + + let pub_key = metadata + .public_key() + .ok_or(UpgradeModeCheckError::MissingTokenPublicKey)?; + + let ed25519_pub_key = ed25519::PublicKey::from_base58_string(pub_key) + .map_err(|source| UpgradeModeCheckError::MalformedEd25519PublicKey { source })?; + + let mut opts = VerificationOptions::default(); + if let Some(issuer) = expected_issuer { + opts.allowed_issuers = Some(HashSet::from_iter(vec![issuer.to_string()])); + } + + let attestation = ed25519_pub_key + .to_jwt_compatible_key() + .verify_token::(token, Some(opts)) + .map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })? + .custom; + + Ok(attestation) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::generate_new_attestation; + use nym_crypto::asymmetric::ed25519; + + #[test] + fn generate_and_validate_jwt() { + let attestation_key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + let jwt_key = ed25519::PrivateKey::from_bytes(&[ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, + 2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]) + .unwrap(); + let keys = ed25519::KeyPair::from(jwt_key); + + let attestation = generate_new_attestation(&attestation_key); + let jwt_issuer = generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + Some("nym-credential-proxy"), + ); + // we expect 'nym-credential-proxy' issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok()); + + // we don't care about issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok()); + + // we expect another-issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err()); + + let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + None, + ); + // we expect 'nym-credential-proxy' issuer + assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err()); + + // we don't care about issuer + assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); + } +} diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs new file mode 100644 index 0000000000..21bc5402ec --- /dev/null +++ b/common/upgrade-mode-check/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod attestation; +pub(crate) mod error; +pub(crate) mod jwt; + +pub use attestation::{ + attempt_retrieve, generate_new_attestation, generate_new_attestation_with_starting_time, + UpgradeModeAttestation, +}; +pub use error::UpgradeModeCheckError; +pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt}; diff --git a/common/verloc/src/models.rs b/common/verloc/src/models.rs index 0bedab812c..970fc507b5 100644 --- a/common/verloc/src/models.rs +++ b/common/verloc/src/models.rs @@ -130,12 +130,7 @@ impl VerlocMeasurement { let variance_micros = data .iter() .map(|&value| { - // make sure we don't underflow - let diff = if mean > value { - mean - value - } else { - value - mean - }; + let diff = mean.abs_diff(value); // we don't need nanos precision let diff_micros = diff.as_micros(); diff_micros * diff_micros diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index a8f4f30662..96362c852c 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -5,9 +5,10 @@ #![allow(clippy::drop_non_drop)] use crate::error::WasmCoreError; -use nym_client_core::config::ForgetMe; +use nym_client_core::config::{ForgetMe, RememberMe}; use nym_config::helpers::OptionalSet; use nym_sphinx::params::{PacketSize, PacketType}; +use nym_statistics_common::types::SessionType; use serde::{Deserialize, Serialize}; use std::time::Duration; use wasm_bindgen::prelude::*; @@ -113,6 +114,8 @@ pub struct DebugWasm { pub stats_reporting: StatsReportingWasm, pub forget_me: ForgetMeWasm, + + pub remember_me: RememberMeWasm, } impl Default for DebugWasm { @@ -132,6 +135,7 @@ impl From for ConfigDebug { reply_surbs: debug.reply_surbs.into(), stats_reporting: debug.stats_reporting.into(), forget_me: debug.forget_me.into(), + remember_me: debug.remember_me.into(), } } } @@ -147,6 +151,7 @@ impl From for DebugWasm { reply_surbs: debug.reply_surbs.into(), stats_reporting: debug.stats_reporting.into(), forget_me: ForgetMeWasm::from(debug.forget_me), + remember_me: RememberMeWasm::from(debug.remember_me), } } } @@ -191,6 +196,15 @@ pub struct TrafficWasm { /// Controls whether the sent packets should use outfox as opposed to the default sphinx. pub use_outfox: bool, + + /// Indicates whether to mix hops or not. If mix hops are enabled, traffic + /// will be routed as usual, to the entry gateway, through three mix nodes, egressing + /// through the exit gateway. If mix hops are disabled, traffic will be routed directly + /// from the entry gateway to the exit gateway, bypassing the mix nodes. + /// + /// This overrides the `use_legacy_sphinx_format` setting as reduced/disabeld mix hops + /// requires use of the updated SURB packet format. + pub disable_mix_hops: bool, } impl Default for TrafficWasm { @@ -224,6 +238,7 @@ impl From for ConfigTraffic { secondary_packet_size: use_extended_packet_size, use_legacy_sphinx_format: traffic.use_legacy_sphinx_format, packet_type, + disable_mix_hops: traffic.disable_mix_hops, } } } @@ -241,6 +256,7 @@ impl From for TrafficWasm { use_legacy_sphinx_format: traffic.use_legacy_sphinx_format, use_extended_packet_size: traffic.secondary_packet_size.is_some(), use_outfox: traffic.packet_type == PacketType::Outfox, + disable_mix_hops: traffic.disable_mix_hops, } } } @@ -492,9 +508,9 @@ pub struct ReplySurbsWasm { /// deciding it's never going to get them and would drop all pending messages pub maximum_reply_surb_drop_waiting_period_ms: u32, - /// Defines maximum amount of time given reply surb is going to be valid for. - /// This is going to be superseded by key rotation once implemented. - pub maximum_reply_surb_age_ms: u32, + /// Defines maximum number of times the client is going to re-request reply surbs + /// for clearing pending messages before giving up after making no progress. + pub maximum_reply_surbs_rerequests: usize, /// Defines maximum amount of time given reply key is going to be valid for. /// This is going to be superseded by key rotation once implemented. @@ -503,9 +519,6 @@ pub struct ReplySurbsWasm { /// Defines how many mix nodes the reply surb should go through. /// If not set, the default value is going to be used. pub surb_mix_hops: Option, - - /// Specifies if we should reset all the sender tags on startup - pub fresh_sender_tags: bool, } impl Default for ReplySurbsWasm { @@ -530,14 +543,11 @@ impl From for ConfigReplySurbs { maximum_reply_surb_drop_waiting_period: Duration::from_millis( reply_surbs.maximum_reply_surb_drop_waiting_period_ms as u64, ), - maximum_reply_surb_age: Duration::from_millis( - reply_surbs.maximum_reply_surb_age_ms as u64, - ), + maximum_reply_surbs_rerequests: reply_surbs.maximum_reply_surbs_rerequests, maximum_reply_key_age: Duration::from_millis( reply_surbs.maximum_reply_key_age_ms as u64, ), surb_mix_hops: reply_surbs.surb_mix_hops, - fresh_sender_tags: reply_surbs.fresh_sender_tags, } } } @@ -558,10 +568,9 @@ impl From for ReplySurbsWasm { maximum_reply_surb_drop_waiting_period_ms: reply_surbs .maximum_reply_surb_drop_waiting_period .as_millis() as u32, - maximum_reply_surb_age_ms: reply_surbs.maximum_reply_surb_age.as_millis() as u32, + maximum_reply_surbs_rerequests: reply_surbs.maximum_reply_surbs_rerequests, maximum_reply_key_age_ms: reply_surbs.maximum_reply_key_age.as_millis() as u32, surb_mix_hops: reply_surbs.surb_mix_hops, - fresh_sender_tags: reply_surbs.fresh_sender_tags, } } } @@ -589,6 +598,27 @@ impl From for ForgetMeWasm { } } +#[wasm_bindgen(inspectable)] +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize, Default)] +#[serde(deny_unknown_fields)] +pub struct RememberMeWasm { + pub stats: bool, +} + +impl From for RememberMe { + fn from(value: RememberMeWasm) -> Self { + RememberMe::new(value.stats, SessionType::Wasm) + } +} + +impl From for RememberMeWasm { + fn from(value: RememberMe) -> Self { + Self { + stats: value.stats(), + } + } +} + #[wasm_bindgen(inspectable)] #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index eaa492a145..1ee58f2437 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -9,7 +9,7 @@ use super::{ AcknowledgementsWasm, CoverTrafficWasm, DebugWasm, ForgetMeWasm, GatewayConnectionWasm, - ReplySurbsWasm, StatsReportingWasm, TopologyWasm, TrafficWasm, + RememberMeWasm, ReplySurbsWasm, StatsReportingWasm, TopologyWasm, TrafficWasm, }; use crate::config::ConfigDebug; use serde::{Deserialize, Serialize}; @@ -50,6 +50,9 @@ pub struct DebugWasmOverride { #[tsify(optional)] pub forget_me: Option, + + #[tsify(optional)] + pub remember_me: Option, } impl From for DebugWasm { @@ -63,6 +66,7 @@ impl From for DebugWasm { reply_surbs: value.reply_surbs.map(Into::into).unwrap_or_default(), stats_reporting: value.stats_reporting.map(Into::into).unwrap_or_default(), forget_me: value.forget_me.map(Into::into).unwrap_or_default(), + remember_me: value.remember_me.map(Into::into).unwrap_or_default(), } } } @@ -148,6 +152,7 @@ impl From for TrafficWasm { .use_extended_packet_size .unwrap_or(def.use_extended_packet_size), use_outfox: value.use_outfox.unwrap_or(def.use_outfox), + disable_mix_hops: false, // not configured from js config override yet } } } @@ -378,16 +383,16 @@ pub struct ReplySurbsWasmOverride { #[tsify(optional)] pub maximum_reply_surb_drop_waiting_period_ms: Option, - /// Defines maximum amount of time given reply surb is going to be valid for. - /// This is going to be superseded by key rotation once implemented. - #[tsify(optional)] - pub maximum_reply_surb_age_ms: Option, - /// Defines maximum amount of time given reply key is going to be valid for. /// This is going to be superseded by key rotation once implemented. #[tsify(optional)] pub maximum_reply_key_age_ms: Option, + /// Defines maximum number of times the client is going to re-request reply surbs + /// for clearing pending messages before giving up after making no progress. + #[tsify(optional)] + pub maximum_reply_surbs_rerequests: Option, + #[tsify(optional)] pub surb_mix_hops: Option, @@ -424,14 +429,13 @@ impl From for ReplySurbsWasm { maximum_reply_surb_drop_waiting_period_ms: value .maximum_reply_surb_drop_waiting_period_ms .unwrap_or(def.maximum_reply_surb_drop_waiting_period_ms), - maximum_reply_surb_age_ms: value - .maximum_reply_surb_age_ms - .unwrap_or(def.maximum_reply_surb_age_ms), maximum_reply_key_age_ms: value .maximum_reply_key_age_ms .unwrap_or(def.maximum_reply_key_age_ms), surb_mix_hops: value.surb_mix_hops, - fresh_sender_tags: value.fresh_sender_tags, + maximum_reply_surbs_rerequests: value + .maximum_reply_surbs_rerequests + .unwrap_or(def.maximum_reply_surbs_rerequests), } } } @@ -456,6 +460,22 @@ impl From for ForgetMeWasm { } } +#[derive(Tsify, Debug, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct RememberMeWasmOverride { + #[tsify(optional)] + pub stats: Option, +} + +impl From for RememberMeWasm { + fn from(value: RememberMeWasmOverride) -> Self { + RememberMeWasm { + stats: value.stats.unwrap_or_default(), + } + } +} + #[derive(Tsify, Debug, Clone, Serialize, Deserialize)] #[tsify(into_wasm_abi, from_wasm_abi)] #[serde(rename_all = "camelCase")] diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 411c05f1c8..4cb5f06c55 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -29,7 +29,8 @@ use wasm_bindgen_futures::future_to_promise; use wasm_utils::error::PromisableResult; pub use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; -use wasm_utils::console_log; +use nym_topology::provider_trait::ToTopologyMetadata; +use wasm_utils::{console_log, console_warn}; // don't get too excited about the name, under the hood it's just a big fat placeholder // with no disk_persistence @@ -73,14 +74,25 @@ pub async fn current_network_topology_async( let api_client = NymApiClient::new(url); let rewarded_set = api_client.get_current_rewarded_set().await?; - let mixnodes = api_client - .get_all_basic_active_mixing_assigned_nodes() + let mixnodes_res = api_client + .get_all_basic_active_mixing_assigned_nodes_with_metadata() .await?; - let gateways = api_client.get_all_basic_entry_assigned_nodes().await?; + let metadata = mixnodes_res.metadata; + let mixnodes = mixnodes_res.nodes; - let mut topology = NymTopology::new_empty(rewarded_set); - topology.add_skimmed_nodes(&mixnodes); - topology.add_skimmed_nodes(&gateways); + let gateways_res = api_client + .get_all_basic_entry_assigned_nodes_with_metadata() + .await?; + if !gateways_res.metadata.consistency_check(&metadata) { + console_warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata); + return Err(WasmCoreError::UnavailableNetworkTopology); + } + + let gateways = gateways_res.nodes; + + let topology = NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) + .with_skimmed_nodes(&mixnodes) + .with_skimmed_nodes(&gateways); Ok(topology.into()) } diff --git a/common/wasm/utils/src/websocket/mod.rs b/common/wasm/utils/src/websocket/mod.rs index be95a8bf12..902042738d 100644 --- a/common/wasm/utils/src/websocket/mod.rs +++ b/common/wasm/utils/src/websocket/mod.rs @@ -25,10 +25,8 @@ fn map_ws_error(err: WebSocketError) -> WsError { // TODO: are we preserving correct semantics? WebSocketError::ConnectionError => WsError::ConnectionClosed, WebSocketError::ConnectionClose(_event) => WsError::ConnectionClosed, - WebSocketError::MessageSendError(err) => { - WsError::Io(io::Error::new(io::ErrorKind::Other, err.to_string())) - } - _ => WsError::Io(io::Error::new(io::ErrorKind::Other, "new websocket error")), + WebSocketError::MessageSendError(err) => WsError::Io(io::Error::other(err.to_string())), + _ => WsError::Io(io::Error::other("new websocket error")), } } diff --git a/common/wireguard-private-metadata/client/Cargo.toml b/common/wireguard-private-metadata/client/Cargo.toml new file mode 100644 index 0000000000..d7d23ce445 --- /dev/null +++ b/common/wireguard-private-metadata/client/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +tracing = { workspace = true } +nym-http-api-client = { path = "../../http-api-client" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs new file mode 100644 index 0000000000..3f683efe5e --- /dev/null +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use tracing::instrument; + +use nym_http_api_client::{ApiClient, Client, HttpClientError, NO_PARAMS}; + +use nym_wireguard_private_metadata_shared::{ + routes, Version, {ErrorResponse, Request, Response}, +}; + +pub type WireguardMetadataApiClientError = HttpClientError; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait WireguardMetadataApiClient: ApiClient { + #[instrument(level = "debug", skip(self))] + async fn version(&self) -> Result { + let version: u64 = self + .get_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::VERSION], + NO_PARAMS, + ) + .await?; + Ok(version.into()) + } + + #[instrument(level = "debug", skip(self))] + async fn available_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::AVAILABLE], + NO_PARAMS, + request_body, + ) + .await + } + + #[instrument(level = "debug", skip(self, request_body))] + async fn topup_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::TOPUP], + NO_PARAMS, + request_body, + ) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl WireguardMetadataApiClient for Client {} diff --git a/common/wireguard-private-metadata/server/Cargo.toml b/common/wireguard-private-metadata/server/Cargo.toml new file mode 100644 index 0000000000..fa850181f1 --- /dev/null +++ b/common/wireguard-private-metadata/server/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +futures = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tokio-util = { workspace = true } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } + +nym-credentials-interface = { path = "../../credentials-interface" } +nym-credential-verification = { path = "../../credential-verification" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[dev-dependencies] +async-trait = { workspace = true } + + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/server/src/http/mod.rs b/common/wireguard-private-metadata/server/src/http/mod.rs new file mode 100644 index 0000000000..e3d502d8b5 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/mod.rs @@ -0,0 +1,46 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +use nym_wireguard::WgApiWrapper; + +pub(crate) mod openapi; +pub(crate) mod router; +pub(crate) mod state; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Of course, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +#[allow(unused)] +pub struct ShutdownHandles { + axum_shutdown_button: CancellationToken, + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, + /// Wireguard API for kernel interactions + wg_api: Arc, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub fn new( + axum_join_handle: AxumJoinHandle, + wg_api: Arc, + axum_shutdown_button: CancellationToken, + ) -> Self { + Self { + axum_shutdown_button, + axum_join_handle, + wg_api, + } + } +} + +type AxumJoinHandle = JoinHandle>; diff --git a/common/wireguard-private-metadata/server/src/http/openapi.rs b/common/wireguard-private-metadata/server/src/http/openapi.rs new file mode 100644 index 0000000000..5e91266582 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/openapi.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use utoipa::OpenApi; + +use nym_wireguard_private_metadata_shared::{Request, Response}; + +#[derive(OpenApi)] +#[openapi( + info(title = "Nym Wireguard Private Metadata"), + tags(), + components(schemas(Request, Response)) +)] +pub(crate) struct ApiDoc; diff --git a/common/wireguard-private-metadata/server/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs new file mode 100644 index 0000000000..c5936f1f4c --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -0,0 +1,101 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use axum::response::Redirect; +use axum::routing::get; +use axum::Router; +use core::net::SocketAddr; +use nym_http_api_common::middleware::logging::log_request_info; +use tokio::net::TcpListener; +use tokio_util::sync::WaitForCancellationFutureOwned; +use tower_http::cors::CorsLayer; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::openapi::ApiDoc; +use crate::http::state::AppState; +use crate::network::bandwidth_routes; + +/// Wrapper around `axum::Router` which ensures correct [order of layers][order]. +/// Add new routes as if you were working directly with `axum`. +/// +/// Why? Middleware like logger, CORS, TLS which need to handle request before other +/// layers should be added last. Using this builder pattern ensures that. +/// +/// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering +pub struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + /// All routes should be, if possible, added here. Exceptions are e.g. + /// routes which are added conditionally in other places based on some `if`. + pub fn with_default_routes() -> Self { + let default_routes = Router::new() + .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/", get(|| async { Redirect::to("/swagger") })) + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())); + Self { + unfinished_router: default_routes, + } + } + + /// Invoke this as late as possible before constructing HTTP server + /// (after all routes were added). + pub fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + /// Middleware added here intercepts the request before it gets to other routes. + fn finalize_routes(self) -> Router { + self.unfinished_router + .layer(setup_cors()) + .layer(axum::middleware::from_fn(log_request_info)) + } +} + +fn setup_cors() -> CorsLayer { + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} + +pub struct RouterWithState { + pub router: Router, +} + +impl RouterWithState { + pub async fn build_server(self, bind_address: &SocketAddr) -> anyhow::Result { + let listener = tokio::net::TcpListener::bind(bind_address) + .await + .map_err(|err| anyhow!("Couldn't bind to address {} due to {}", bind_address, err))?; + + Ok(ApiHttpServer { + router: self.router, + listener, + }) + } +} + +pub struct ApiHttpServer { + router: Router, + listener: TcpListener, +} + +impl ApiHttpServer { + pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/common/wireguard-private-metadata/server/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs new file mode 100644 index 0000000000..06916bb35f --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -0,0 +1,35 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use nym_credentials_interface::CredentialSpendingData; + +use crate::transceiver::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; + +#[derive(Clone, axum::extract::FromRef)] +pub struct AppState { + transceiver: PeerControllerTransceiver, +} + +impl AppState { + pub fn new(transceiver: PeerControllerTransceiver) -> Self { + Self { transceiver } + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + self.transceiver.query_bandwidth(ip).await + } + + // Top up with a credential and return the afterwards available bandwidth + pub async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result { + self.transceiver + .topup_bandwidth(ip, Box::new(credential)) + .await + } +} diff --git a/common/wireguard-private-metadata/server/src/lib.rs b/common/wireguard-private-metadata/server/src/lib.rs new file mode 100644 index 0000000000..3d772ac1e0 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod http; +mod network; +mod transceiver; + +pub use http::{ + router::{ApiHttpServer, RouterBuilder, RouterWithState}, + state::AppState, + ShutdownHandles, +}; +pub use transceiver::PeerControllerTransceiver; diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs new file mode 100644 index 0000000000..36b22ba32b --- /dev/null +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -0,0 +1,111 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; + +use axum::{ + extract::{ConnectInfo, Query, State}, + Json, Router, +}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_wireguard_private_metadata_shared::{ + interface::{RequestData, ResponseData}, + latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, +}; +use tower_http::compression::CompressionLayer; + +use crate::http::state::AppState; + +pub(crate) fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) +} + +#[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn available_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct( + ResponseData::AvailableBandwidth(available_bandwidth), + version, + ) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn topup_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(credential), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .topup_bandwidth(addr.ip(), *credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} diff --git a/common/wireguard-private-metadata/server/src/transceiver.rs b/common/wireguard-private-metadata/server/src/transceiver.rs new file mode 100644 index 0000000000..cbe77126cf --- /dev/null +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -0,0 +1,307 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use futures::channel::oneshot; +use tokio::sync::mpsc; + +use nym_credential_verification::ClientBandwidth; +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard::peer_controller::PeerControlRequest; +use nym_wireguard_private_metadata_shared::error::MetadataError; + +#[derive(Clone)] +pub struct PeerControllerTransceiver { + request_tx: mpsc::Sender, +} + +impl PeerControllerTransceiver { + pub fn new(request_tx: mpsc::Sender) -> Self { + Self { request_tx } + } + + async fn get_client_bandwidth(&self, ip: IpAddr) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }; + self.request_tx + .send(msg) + .await + .map_err(|_| MetadataError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| MetadataError::NoResponse)? + .map_err(|err| MetadataError::Unsuccessful { + reason: err.to_string(), + }) + } + + pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result { + Ok(self.get_client_bandwidth(ip).await?.available().await) + } + + // Top up with a credential and return the afterwards available bandwidth + pub(crate) async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: Box, + ) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetVerifierByIp { + ip, + credential, + response_tx, + }; + self.request_tx + .send(msg) + .await + .map_err(|_| MetadataError::PeerInteractionStopped)?; + + let mut verifier = response_rx + .await + .map_err(|_| MetadataError::NoResponse)? + .map_err(|err| MetadataError::Unsuccessful { + reason: err.to_string(), + })?; + let available_bandwidth = + verifier + .verify() + .await + .map_err(|err| MetadataError::CredentialVerification { + message: err.to_string(), + })?; + + Ok(available_bandwidth) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use nym_credential_verification::TicketVerifier; + use nym_wireguard::CONTROL_CHANNEL_SIZE; + + use super::*; + + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + #[tokio::test] + async fn get_bandwidth() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let bw = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap(); + assert_eq!(bw, 0); + } + + #[tokio::test] + async fn stop_peer() { + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + drop(request_rx); + let err = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap_err(); + assert_eq!(err, MetadataError::PeerInteractionStopped); + } + + #[tokio::test] + async fn unresponsive_peer() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { + ip: _, + response_tx: _, + } => {} + _ => panic!("Not expected"), + } + }); + + let err = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap_err(); + assert_eq!(err, MetadataError::NoResponse); + } + + #[tokio::test] + async fn unsuccessful_query_bandwidth() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Err(nym_wireguard::error::Error::Internal( + "testing".to_owned(), + ))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let ret = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await; + assert!(ret.is_err()); + } + + #[tokio::test] + async fn topup() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let verifier_bw = 42; + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new(verifier_bw)))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let bw = transceiver + .topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential)) + .await + .unwrap(); + assert_eq!(bw, verifier_bw); + } + + #[tokio::test] + async fn unsuccessful_topup() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Err(nym_wireguard::error::Error::Internal( + "testing".to_owned(), + ))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let ret = transceiver + .topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential)) + .await; + assert!(ret.is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/Cargo.toml b/common/wireguard-private-metadata/shared/Cargo.toml new file mode 100644 index 0000000000..26b3cc6cd6 --- /dev/null +++ b/common/wireguard-private-metadata/shared/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +axum = { workspace = true } +bincode = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true } +thiserror = { workspace = true } +utoipa = { workspace = true } + +nym-credentials-interface = { path = "../../credentials-interface" } + +[features] +testing = [] + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/shared/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs new file mode 100644 index 0000000000..3783462a4d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -0,0 +1,28 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum MetadataError { + #[error("peers can't be interacted with anymore")] + PeerInteractionStopped, + + #[error("no response received")] + NoResponse, + + #[error("query was not successful: {reason}")] + Unsuccessful { reason: String }, + + #[error("Models error: {message}")] + Models { message: String }, + + #[error("Credential verification error: {message}")] + CredentialVerification { message: String }, +} + +impl From for MetadataError { + fn from(value: crate::models::error::Error) -> Self { + Self::Models { + message: value.to_string(), + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs new file mode 100644 index 0000000000..de79157874 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod error; +mod models; +pub mod routes; + +#[cfg(feature = "testing")] +pub use models::v0; +pub use models::{ + error::Error as ModelError, interface, latest, v1, AxumErrorResponse, AxumResult, Construct, + ErrorResponse, Extract, Request, Response, Version, +}; + +fn make_bincode_serializer() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs new file mode 100644 index 0000000000..45dc88617d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -0,0 +1,30 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::Version; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Bincode(#[from] bincode::Error), + + #[error("trying to deserialize from version {source_version:?} into {target_version:?}")] + InvalidVersion { + source_version: Version, + target_version: Version, + }, + + #[error( + "trying to deserialize from query type {source_query_type} query type {target_query_type}" + )] + InvalidQueryType { + source_query_type: String, + target_query_type: String, + }, + + #[error("update not possible from {from:?} to {to:?}")] + UpdateNotPossible { from: Version, to: Version }, + + #[error("downgrade not possible from {from:?} to {to:?}")] + DowngradeNotPossible { from: Version, to: Version }, +} diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs new file mode 100644 index 0000000000..3f4537f170 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use crate::models::v0; +use crate::models::{v1, Construct, Extract, Request, Response, Version}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +impl From for RequestData { + fn from(value: super::latest::interface::RequestData) -> Self { + match value { + super::latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: super::latest::interface::ResponseData) -> Self { + match value { + super::latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct(info: RequestData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + Version::V0 => { + let translate_info = super::latest::interface::RequestData::from(info); + let downgrade_info = v0::interface::RequestData::try_from(translate_info)?; + let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V1 => { + let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + Ok(versioned_request.try_into()?) + } + } + } +} + +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), crate::models::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + + Ok((upgrade_request.into(), version)) + } + super::Version::V1 => { + let versioned_request = v1::VersionedRequest::try_from(self.clone())?; + let (extracted, version) = versioned_request.extract()?; + Ok((extracted.into(), version)) + } + } + } +} + +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let translate_response = super::latest::interface::ResponseData::from(info); + let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?; + let versioned_response = + v0::VersionedResponse::construct(downgrade_response, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + Ok(versioned_response.try_into()?) + } + } + } +} + +impl Extract for Response { + fn extract(&self) -> Result<(ResponseData, Version), super::error::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_response = v0::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + let upgrade_response = super::latest::interface::ResponseData::try_from(response)?; + + Ok((upgrade_response.into(), version)) + } + super::Version::V1 => { + let versioned_response = v1::VersionedResponse::try_from(self.clone())?; + let (extracted, version) = versioned_response.extract()?; + Ok((extracted.into(), version)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs new file mode 100644 index 0000000000..e408d7c5df --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{Display, Formatter}; + +use axum::http::StatusCode; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) mod error; +pub mod interface; +#[cfg(feature = "testing")] +pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing +pub mod v1; + +pub use v1 as latest; + +use crate::models::error::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum Version { + #[cfg(feature = "testing")] + /// only used for testing purposes, don't include it in your matching arms + V0, + V1, +} + +impl From for Version { + fn from(value: u64) -> Self { + #[cfg(feature = "testing")] + let zero_version = Version::V0; + #[cfg(not(feature = "testing"))] + let zero_version = latest::VERSION; + match value { + 0 => zero_version, + 1 => Version::V1, + _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about + } + } +} + +impl From for u64 { + fn from(value: Version) -> Self { + // remember to modify the above match if you're bumping the version + match value { + #[cfg(feature = "testing")] + Version::V0 => 0, + Version::V1 => 1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct Request { + pub version: Version, + pub(crate) inner: Vec, +} + +#[derive(Clone, Serialize, Deserialize, ToSchema)] +pub struct Response { + pub version: Version, + pub(crate) inner: Vec, +} + +pub trait Extract { + fn extract(&self) -> Result<(T, Version), Error>; +} + +pub trait Construct: Sized { + fn construct(info: T, version: Version) -> Result; +} + +pub type AxumResult = Result; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +pub struct ErrorResponse { + pub message: String, +} + +impl Display for ErrorResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.message.fmt(f) + } +} + +pub struct AxumErrorResponse { + message: ErrorResponse, + status: StatusCode, +} + +impl AxumErrorResponse { + pub fn bad_request(msg: impl Display) -> Self { + Self { + message: ErrorResponse { + message: msg.to_string(), + }, + status: StatusCode::BAD_REQUEST, + } + } +} + +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message.to_string()).into_response() + } +} + +mod tests { + #[allow(dead_code)] + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs new file mode 100644 index 0000000000..78dfdec7b5 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs new file mode 100644 index 0000000000..5243e312ee --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse {} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse {}; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs new file mode 100644 index 0000000000..2f8fd2805c --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(_) => Ok(InnerTopUpRequest {}.try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _req = InnerTopUpRequest::try_from(self.clone())?; + Ok((RequestData::TopUpBandwidth(()), VERSION)) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(()) => { + Ok(InnerAvailableBandwidthResponse {}.try_into()?) + } + ResponseData::TopUpBandwidth(()) => Ok(InnerTopUpResponse {}.try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok((ResponseData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _resp = InnerTopUpResponse::try_from(self.clone())?; + Ok((ResponseData::TopUpBandwidth(()), VERSION)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs new file mode 100644 index 0000000000..997fbf6f57 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V0; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs new file mode 100644 index 0000000000..9c333478d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest {} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest {}; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs new file mode 100644 index 0000000000..cd934b6e7e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse {} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse {}; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs new file mode 100644 index 0000000000..78dfdec7b5 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs new file mode 100644 index 0000000000..f5addd609f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs @@ -0,0 +1,90 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs new file mode 100644 index 0000000000..46cafddc7d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use super::super::v0 as previous; + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(credential) => Ok(InnerTopUpRequest { + credential: *credential, + } + .try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let req = InnerTopUpRequest::try_from(self.clone())?; + Ok(( + RequestData::TopUpBandwidth(Box::new(req.credential)), + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(available_bandwidth) => { + Ok(InnerAvailableBandwidthResponse { + available_bandwidth, + } + .try_into()?) + } + ResponseData::TopUpBandwidth(available_bandwidth) => Ok(InnerTopUpResponse { + available_bandwidth, + } + .try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok(( + ResponseData::AvailableBandwidth(resp.available_bandwidth), + VERSION, + )) + } + QueryType::TopupBandwidth => { + let resp = InnerTopUpResponse::try_from(self.clone())?; + Ok(( + ResponseData::TopUpBandwidth(resp.available_bandwidth), + VERSION, + )) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(inner) => { + Ok(Self::AvailableBandwidth(inner)) + } + previous::interface::RequestData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth(inner) => Ok(Self::AvailableBandwidth(inner)), + RequestData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for ResponseData { + type Error = super::Error; + + fn try_from(value: previous::interface::ResponseData) -> Result { + match value { + previous::interface::ResponseData::AvailableBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + previous::interface::ResponseData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth(())), + ResponseData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth(()) + ); + assert!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(())).is_err(), + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(())).is_err() + ); + } + + #[test] + fn request_downgrade() { + assert_eq!( + previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) + .unwrap(), + previous::interface::RequestData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::RequestData::try_from(RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(()) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth(42)).unwrap(), + previous::interface::ResponseData::TopUpBandwidth(()) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs new file mode 100644 index 0000000000..787a74f671 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + + use nym_credentials_interface::CredentialSpendingData; + + use crate::models::tests::CREDENTIAL_BYTES; + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V0; + let future_bw = Request { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedRequest::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn mismatched_response_version() { + let version = Version::V0; + let future_bw = Response { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedResponse::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs new file mode 100644 index 0000000000..871cc127ef --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs @@ -0,0 +1,92 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use nym_credentials_interface::CredentialSpendingData; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: CredentialSpendingData, +} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs new file mode 100644 index 0000000000..08e0ef111f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/routes.rs b/common/wireguard-private-metadata/shared/src/routes.rs new file mode 100644 index 0000000000..bda615fe1c --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -0,0 +1,10 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const V1_API_VERSION: &str = "v1"; + +pub const BANDWIDTH: &str = "bandwidth"; + +pub const VERSION: &str = "version"; +pub const AVAILABLE: &str = "available"; +pub const TOPUP: &str = "topup"; diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml new file mode 100644 index 0000000000..6827c16f60 --- /dev/null +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +nym-credential-verification = { path = "../../credential-verification" } +nym-credentials-interface = { path = "../../credentials-interface" } +nym-http-api-client = { path = "../../http-api-client" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } + +nym-wireguard-private-metadata-client = { path = "../client" } +nym-wireguard-private-metadata-shared = { path = "../shared", features = [ + "testing", +] } +nym-wireguard-private-metadata-server = { path = "../server" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs new file mode 100644 index 0000000000..c1a981e484 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -0,0 +1,217 @@ +#[cfg(test)] +mod v0; + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use nym_credential_verification::{ClientBandwidth, TicketVerifier}; + use nym_credentials_interface::CredentialSpendingData; + use nym_http_api_client::Client; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_server::{ + AppState, PeerControllerTransceiver, RouterBuilder, + }; + use nym_wireguard_private_metadata_shared::{latest, v0, v1, ErrorResponse}; + use tokio::{net::TcpListener, sync::mpsc}; + + pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = RouterBuilder::with_default_routes() + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))) + .router; + + tokio::spawn(async move { + loop { + match request_rx.recv().await { + Some(PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx }) => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + Some(PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + }) => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + None => break, + _ => panic!("Not expected"), + } + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + } + + #[tokio::test] + async fn query_latest_version() { + let client = spawn_server_and_create_client().await; + let version = client.version().await.unwrap(); + assert_eq!(version, latest::VERSION); + } + + #[tokio::test] + async fn query_against_server_v0() { + let client = super::v0::network::test::spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v0::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + v0::TopUpResponse::try_from(response).unwrap(); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + } + + #[tokio::test] + async fn query_against_server_v1() { + let client = spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v1::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 0); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::TopUpResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs new file mode 100644 index 0000000000..77cd2d12fe --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/interface.rs @@ -0,0 +1,150 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_shared::{ + v0 as latest, Construct, Extract, Request, Response, Version, +}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { + match value { + latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: latest::interface::ResponseData) -> Self { + match value { + latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct( + info: RequestData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_info = latest::interface::RequestData::from(info); + let versioned_request = + latest::VersionedRequest::construct(translate_info, latest::VERSION)?; + Ok(versioned_request.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Request { + fn extract( + &self, + ) -> Result<(RequestData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_request = latest::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + Ok((request.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} + +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for Response { + fn construct( + info: ResponseData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_response = latest::interface::ResponseData::from(info); + let versioned_response = + latest::VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Response { + fn extract( + &self, + ) -> Result<(ResponseData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_response = latest::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + Ok((response.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/mod.rs b/common/wireguard-private-metadata/tests/src/v0/mod.rs new file mode 100644 index 0000000000..7338cc5ae2 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod interface; +pub(crate) mod network; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs new file mode 100644 index 0000000000..6a847ecc36 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -0,0 +1,146 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use std::net::SocketAddr; + + use crate::{ + tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, + v0::interface::{RequestData, ResponseData}, + }; + use axum::{extract::Query, Json, Router}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::ErrorResponse; + use nym_wireguard_private_metadata_shared::{ + v0 as latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, + }; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + use nym_wireguard_private_metadata_server::AppState; + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + #[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn available_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::AvailableBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn topup_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::TopUpBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url::<_, ErrorResponse>(addr.to_string(), None).unwrap() + } +} diff --git a/common/wireguard-types/src/config.rs b/common/wireguard-types/src/config.rs index fda6fc70f3..e322904d2e 100644 --- a/common/wireguard-types/src/config.rs +++ b/common/wireguard-types/src/config.rs @@ -17,9 +17,13 @@ pub struct Config { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. - pub announced_port: u16, + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. /// The maximum value for IPv4 is 32 diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index c999861cd6..e98e5fc27d 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -11,11 +11,13 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } chrono = { workspace = true } dashmap = { workspace = true } defguard_wireguard_rs = { workspace = true } +dyn-clone = { workspace = true } futures = { workspace = true } # The latest version on crates.io at the time of writing this (6.0.0) has a # version mismatch with x25519-dalek/curve25519-dalek that is resolved in the @@ -30,10 +32,19 @@ time = { workspace = true } tracing = { workspace = true } nym-authenticator-requests = { path = "../authenticator-requests" } +nym-credentials-interface = { path = "../credentials-interface" } nym-credential-verification = { path = "../credential-verification" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-gateway-storage = { path = "../gateway-storage" } +nym-gateway-requests = { path = "../gateway-requests" } nym-network-defaults = { path = "../network-defaults" } nym-task = { path = "../task" } nym-wireguard-types = { path = "../wireguard-types" } nym-node-metrics = { path = "../../nym-node/nym-node-metrics" } + +[dev-dependencies] +nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] } + +[features] +default = [] +mock = ["nym-gateway-storage/mock"] diff --git a/common/wireguard/src/error.rs b/common/wireguard/src/error.rs index d2fd0e7956..d240889d4a 100644 --- a/common/wireguard/src/error.rs +++ b/common/wireguard/src/error.rs @@ -3,21 +3,23 @@ #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("traffic byte data needs to be increasing")] - InconsistentConsumedBytes, - #[error("{0}")] Defguard(#[from] defguard_wireguard_rs::error::WireguardInterfaceError), #[error("internal {0}")] Internal(String), - #[error("storage should have the requested bandwidht entry")] + #[error("storage should have the requested bandwidth entry")] MissingClientBandwidthEntry, + #[error("kernel should have the requested client entry: {0}")] + MissingClientKernelEntry(String), + #[error("{0}")] GatewayStorage(#[from] nym_gateway_storage::error::GatewayStorageError), #[error("{0}")] SystemTime(#[from] std::time::SystemTimeError), } + +pub type Result = std::result::Result; diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 3858d476bc..9fe3371fde 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -6,28 +6,104 @@ // #![warn(clippy::expect_used)] // #![warn(clippy::unwrap_used)] -use defguard_wireguard_rs::WGApi; +use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, WGApi, WireguardInterfaceApi}; +#[cfg(target_os = "linux")] +use nym_credential_verification::ecash::EcashManager; use nym_crypto::asymmetric::x25519::KeyPair; use nym_wireguard_types::Config; use peer_controller::PeerControlRequest; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; -#[cfg(target_os = "linux")] -use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; - #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; -pub(crate) mod error; +pub mod error; pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; +pub const CONTROL_CHANNEL_SIZE: usize = 256; + pub struct WgApiWrapper { inner: WGApi, } +impl WireguardInterfaceApi for WgApiWrapper { + fn create_interface( + &self, + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.create_interface() + } + + fn assign_address( + &self, + address: &IpAddrMask, + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.assign_address(address) + } + + fn configure_peer_routing( + &self, + peers: &[Peer], + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.configure_peer_routing(peers) + } + + #[cfg(not(target_os = "windows"))] + fn configure_interface( + &self, + config: &defguard_wireguard_rs::InterfaceConfiguration, + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.configure_interface(config) + } + + #[cfg(target_os = "windows")] + fn configure_interface( + &self, + config: &defguard_wireguard_rs::InterfaceConfiguration, + dns: &[std::net::IpAddr], + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.configure_interface(config, dns) + } + + fn remove_interface( + &self, + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.remove_interface() + } + + fn configure_peer( + &self, + peer: &Peer, + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.configure_peer(peer) + } + + fn remove_peer( + &self, + peer_pubkey: &Key, + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.remove_peer(peer_pubkey) + } + + fn read_interface_data( + &self, + ) -> Result< + defguard_wireguard_rs::host::Host, + defguard_wireguard_rs::error::WireguardInterfaceError, + > { + self.inner.read_interface_data() + } + + fn configure_dns( + &self, + dns: &[std::net::IpAddr], + ) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.inner.configure_dns(dns) + } +} + impl WgApiWrapper { pub fn new(wg_api: WGApi) -> Self { WgApiWrapper { inner: wg_api } @@ -38,7 +114,7 @@ impl Drop for WgApiWrapper { fn drop(&mut self) { if let Err(e) = defguard_wireguard_rs::WireguardInterfaceApi::remove_interface(&self.inner) { - log::error!("Could not remove the wireguard interface: {:?}", e); + log::error!("Could not remove the wireguard interface: {e:?}"); } } } @@ -52,7 +128,7 @@ pub struct WireguardGatewayData { impl WireguardGatewayData { pub fn new(config: Config, keypair: Arc) -> (Self, Receiver) { - let (peer_tx, peer_rx) = mpsc::channel(1); + let (peer_tx, peer_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); ( WireguardGatewayData { config, @@ -84,15 +160,16 @@ pub struct WireguardData { /// Start wireguard device #[cfg(target_os = "linux")] pub async fn start_wireguard( - storage: nym_gateway_storage::GatewayStorage, + ecash_manager: Arc, metrics: nym_node_metrics::NymNodeMetrics, - all_peers: Vec, + peers: Vec, task_client: nym_task::TaskClient, wireguard_data: WireguardData, ) -> Result, Box> { use base64::{prelude::BASE64_STANDARD, Engine}; use defguard_wireguard_rs::{InterfaceConfiguration, WireguardInterfaceApi}; use ip_network::IpNetwork; + use nym_credential_verification::ecash::traits::EcashManager; use peer_controller::PeerController; use std::collections::HashMap; use tokio::sync::RwLock; @@ -100,29 +177,19 @@ pub async fn start_wireguard( let ifname = String::from(WG_TUN_BASE_NAME); let wg_api = defguard_wireguard_rs::WGApi::new(ifname.clone(), false)?; - let mut peer_bandwidth_managers = HashMap::with_capacity(all_peers.len()); - let peers = all_peers - .into_iter() - .map(Peer::try_from) - .collect::, _>>()? - .into_iter() - .map(|mut peer| { - // since WGApi doesn't set those values on init, let's set them to 0 - peer.rx_bytes = 0; - peer.tx_bytes = 0; - peer - }) - .collect::>(); + let mut peer_bandwidth_managers = HashMap::with_capacity(peers.len()); + for peer in peers.iter() { - let bandwidth_manager = - PeerController::generate_bandwidth_manager(storage.clone(), &peer.public_key) - .await? - .map(|bw_m| Arc::new(RwLock::new(bw_m))); - // Update storage with *x_bytes set to 0, as in kernel peers we can't set those values - // so we need to restart counting. Hopefully the bandwidth was counted in available_bandwidth - storage - .insert_wireguard_peer(peer, bandwidth_manager.is_some()) - .await?; + let bandwidth_manager = peer_handle::SharedBandwidthStorageManager::new( + Arc::new(RwLock::new( + PeerController::generate_bandwidth_manager( + ecash_manager.storage(), + &peer.public_key, + ) + .await?, + )), + peer.allowed_ips.clone(), + ); peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone())); } @@ -131,7 +198,7 @@ pub async fn start_wireguard( name: ifname.clone(), prvkey: BASE64_STANDARD.encode(wireguard_data.inner.keypair().private_key().to_bytes()), address: wireguard_data.inner.config().private_ipv4.to_string(), - port: wireguard_data.inner.config().announced_port as u32, + port: wireguard_data.inner.config().announced_tunnel_port as u32, peers, mtu: None, }; @@ -174,8 +241,9 @@ pub async fn start_wireguard( let host = wg_api.read_interface_data()?; let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api)); + let mut controller = PeerController::new( - storage, + ecash_manager, metrics, wg_api.clone(), host, diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 19555fd37e..7da4a2336e 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -8,29 +8,32 @@ use defguard_wireguard_rs::{ }; use futures::channel::oneshot; use log::info; -use nym_authenticator_requests::latest::registration::{ - RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY, -}; use nym_credential_verification::{ - bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, - ClientBandwidth, + bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager, + BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, TicketVerifier, }; -use nym_gateway_storage::GatewayStorage; +use nym_credentials_interface::CredentialSpendingData; +use nym_gateway_requests::models::CredentialSpendingRequest; +use nym_gateway_storage::traits::BandwidthGatewayStorage; use nym_node_metrics::NymNodeMetrics; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; -use std::time::{Duration, SystemTime}; use std::{collections::HashMap, sync::Arc}; +use std::{ + net::IpAddr, + time::{Duration, SystemTime}, +}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; -use crate::WgApiWrapper; -use crate::{error::Error, peer_handle::SharedBandwidthStorageManager}; -use crate::{peer_handle::PeerHandle, peer_storage_manager::PeerStorageManager}; +use crate::{ + error::{Error, Result}, + peer_handle::SharedBandwidthStorageManager, +}; +use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; pub enum PeerControlRequest { AddPeer { peer: Peer, - client_id: Option, response_tx: oneshot::Sender, }, RemovePeer { @@ -41,32 +44,34 @@ pub enum PeerControlRequest { key: Key, response_tx: oneshot::Sender, }, - QueryBandwidth { + GetClientBandwidthByKey { key: Key, - response_tx: oneshot::Sender, + response_tx: oneshot::Sender, + }, + GetClientBandwidthByIp { + ip: IpAddr, + response_tx: oneshot::Sender, + }, + GetVerifierByKey { + key: Key, + credential: Box, + response_tx: oneshot::Sender, + }, + GetVerifierByIp { + ip: IpAddr, + credential: Box, + response_tx: oneshot::Sender, }, } -pub struct AddPeerControlResponse { - pub success: bool, -} - -pub struct RemovePeerControlResponse { - pub success: bool, -} - -pub struct QueryPeerControlResponse { - pub success: bool, - pub peer: Option, -} - -pub struct QueryBandwidthControlResponse { - pub success: bool, - pub bandwidth_data: Option, -} +pub type AddPeerControlResponse = Result<()>; +pub type RemovePeerControlResponse = Result<()>; +pub type QueryPeerControlResponse = Result>; +pub type GetClientBandwidthControlResponse = Result; +pub type QueryVerifierControlResponse = Result>; pub struct PeerController { - storage: GatewayStorage, + ecash_verifier: Arc, // we have "all" metrics of a node, but they're behind a single Arc pointer, // so the overhead is minimal @@ -75,21 +80,21 @@ pub struct PeerController { // used to receive commands from individual handles too request_tx: mpsc::Sender, request_rx: mpsc::Receiver, - wg_api: Arc, + wg_api: Arc, host_information: Arc>, - bw_storage_managers: HashMap>, + bw_storage_managers: HashMap, timeout_check_interval: IntervalStream, task_client: nym_task::TaskClient, } impl PeerController { #[allow(clippy::too_many_arguments)] - pub fn new( - storage: GatewayStorage, + pub(crate) fn new( + ecash_verifier: Arc, metrics: NymNodeMetrics, - wg_api: Arc, + wg_api: Arc, initial_host_information: Host, - bw_storage_managers: HashMap, Peer)>, + bw_storage_managers: HashMap, request_tx: mpsc::Sender, request_rx: mpsc::Receiver, task_client: nym_task::TaskClient, @@ -99,23 +104,19 @@ impl PeerController { ); let host_information = Arc::new(RwLock::new(initial_host_information)); for (public_key, (bandwidth_storage_manager, peer)) in bw_storage_managers.iter() { - let peer_storage_manager = PeerStorageManager::new( - storage.clone(), - peer.clone(), - bandwidth_storage_manager.is_some(), - ); + let cached_peer_manager = CachedPeerManager::new(peer); let mut handle = PeerHandle::new( public_key.clone(), host_information.clone(), - peer_storage_manager, + cached_peer_manager, bandwidth_storage_manager.clone(), request_tx.clone(), &task_client, ); + let public_key = public_key.clone(); tokio::spawn(async move { - if let Err(e) = handle.run().await { - log::error!("Peer handle shut down ungracefully - {e}"); - } + handle.run().await; + log::debug!("Peer handle shut down for {public_key}"); }); } let bw_storage_managers = bw_storage_managers @@ -124,7 +125,7 @@ impl PeerController { .collect(); PeerController { - storage, + ecash_verifier, wg_api, host_information, bw_storage_managers, @@ -136,32 +137,14 @@ impl PeerController { } } - // Function that should be used for peer insertion, to handle both storage and kernel interaction - pub async fn add_peer(&self, peer: &Peer, client_id: Option) -> Result<(), Error> { - if client_id.is_none() { - self.storage.insert_wireguard_peer(peer, false).await?; - } - let ret: Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> = - self.wg_api.inner.configure_peer(peer); - if client_id.is_none() && ret.is_err() { - // Try to revert the insertion in storage - if self - .storage - .remove_wireguard_peer(&peer.public_key.to_string()) - .await - .is_err() - { - log::error!("The storage has been corrupted. Wireguard peer {} will persist in storage indefinitely.", peer.public_key); - } - } - Ok(ret?) - } - // Function that should be used for peer removal, to handle both storage and kernel interaction - pub async fn remove_peer(&mut self, key: &Key) -> Result<(), Error> { - self.storage.remove_wireguard_peer(&key.to_string()).await?; + pub async fn remove_peer(&mut self, key: &Key) -> Result<()> { + self.ecash_verifier + .storage() + .remove_wireguard_peer(&key.to_string()) + .await?; self.bw_storage_managers.remove(key); - let ret = self.wg_api.inner.remove_peer(key); + let ret = self.wg_api.remove_peer(key); if ret.is_err() { log::error!("Wireguard peer could not be removed from wireguard kernel module. Process should be restarted so that the interface is reset."); } @@ -169,50 +152,43 @@ impl PeerController { } pub async fn generate_bandwidth_manager( - storage: GatewayStorage, + storage: Box, public_key: &Key, - ) -> Result, Error> { - if let Some(client_id) = storage + ) -> Result { + let client_id = storage .get_wireguard_peer(&public_key.to_string()) .await? .ok_or(Error::MissingClientBandwidthEntry)? - .client_id - { - let bandwidth = storage - .get_available_bandwidth(client_id) - .await? - .ok_or(Error::MissingClientBandwidthEntry)?; - Ok(Some(BandwidthStorageManager::new( - storage, - ClientBandwidth::new(bandwidth.into()), - client_id, - BandwidthFlushingBehaviourConfig::default(), - true, - ))) - } else { - Ok(None) - } + .client_id; + + let bandwidth = storage + .get_available_bandwidth(client_id) + .await? + .ok_or(Error::MissingClientBandwidthEntry)?; + + Ok(BandwidthStorageManager::new( + storage, + ClientBandwidth::new(bandwidth.into()), + client_id, + BandwidthFlushingBehaviourConfig::default(), + true, + )) } - async fn handle_add_request( - &mut self, - peer: &Peer, - client_id: Option, - ) -> Result<(), Error> { - self.add_peer(peer, client_id).await?; - let bandwidth_storage_manager = - Self::generate_bandwidth_manager(self.storage.clone(), &peer.public_key) - .await? - .map(|bw_m| Arc::new(RwLock::new(bw_m))); - let peer_storage_manager = PeerStorageManager::new( - self.storage.clone(), - peer.clone(), - bandwidth_storage_manager.is_some(), + async fn handle_add_request(&mut self, peer: &Peer) -> Result<()> { + self.wg_api.configure_peer(peer)?; + let bandwidth_storage_manager = SharedBandwidthStorageManager::new( + Arc::new(RwLock::new( + Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key) + .await?, + )), + peer.allowed_ips.clone(), ); + let cached_peer_manager = CachedPeerManager::new(peer); let mut handle = PeerHandle::new( peer.public_key.clone(), self.host_information.clone(), - peer_storage_manager, + cached_peer_manager, bandwidth_storage_manager.clone(), self.request_tx.clone(), &self.task_client, @@ -220,56 +196,109 @@ impl PeerController { self.bw_storage_managers .insert(peer.public_key.clone(), bandwidth_storage_manager); // try to immediately update the host information, to eliminate races - if let Ok(host_information) = self.wg_api.inner.read_interface_data() { + if let Ok(host_information) = self.wg_api.read_interface_data() { *self.host_information.write().await = host_information; } + let public_key = peer.public_key.clone(); tokio::spawn(async move { - if let Err(e) = handle.run().await { - log::error!("Peer handle shut down ungracefully - {e}"); - } + handle.run().await; + log::debug!("Peer handle shut down for {public_key}"); }); Ok(()) } - async fn handle_query_peer(&self, key: &Key) -> Result, Error> { + async fn ip_to_key(&self, ip: IpAddr) -> Result> { Ok(self - .storage + .bw_storage_managers + .iter() + .find_map(|(key, bw_manager)| { + bw_manager + .allowed_ips() + .iter() + .find(|ip_mask| ip_mask.ip == ip) + .and(Some(key.clone())) + })) + } + + async fn handle_query_peer_by_key(&self, key: &Key) -> Result> { + Ok(self + .ecash_verifier + .storage() .get_wireguard_peer(&key.to_string()) .await? .map(Peer::try_from) .transpose()?) } - async fn handle_query_bandwidth( - &self, - key: &Key, - ) -> Result, Error> { - let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) else { - return Ok(None); - }; - let available_bandwidth = if let Some(bandwidth_storage_manager) = bandwidth_storage_manager - { - bandwidth_storage_manager - .read() - .await - .available_bandwidth() - .await - } else { - let Some(peer) = self.host_information.read().await.peers.get(key).cloned() else { - // host information not updated yet - return Ok(None); - }; - BANDWIDTH_CAP_PER_DAY.saturating_sub(peer.rx_bytes + peer.tx_bytes) as i64 + async fn handle_get_client_bandwidth_by_key(&self, key: &Key) -> Result { + let bandwidth_storage_manager = self + .bw_storage_managers + .get(key) + .ok_or(Error::MissingClientBandwidthEntry)?; + + Ok(bandwidth_storage_manager + .inner() + .read() + .await + .client_bandwidth()) + } + + async fn handle_get_client_bandwidth_by_ip(&self, ip: IpAddr) -> Result { + let Some(key) = self.ip_to_key(ip).await? else { + return Err(Error::MissingClientKernelEntry(ip.to_string())); }; - Ok(Some(RemainingBandwidthData { - available_bandwidth, - })) + self.handle_get_client_bandwidth_by_key(&key).await + } + + async fn handle_query_verifier_by_key( + &self, + key: &Key, + credential: CredentialSpendingData, + ) -> Result> { + let storage = self.ecash_verifier.storage(); + let client_id = storage + .get_wireguard_peer(&key.to_string()) + .await? + .ok_or(Error::MissingClientBandwidthEntry)? + .client_id; + let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) else { + return Err(Error::MissingClientBandwidthEntry); + }; + let client_bandwidth = bandwidth_storage_manager + .inner() + .read() + .await + .client_bandwidth(); + let verifier = CredentialVerifier::new( + CredentialSpendingRequest::new(credential), + self.ecash_verifier.clone(), + BandwidthStorageManager::new( + storage, + client_bandwidth, + client_id, + BandwidthFlushingBehaviourConfig::default(), + true, + ), + ); + Ok(Box::new(verifier)) + } + + async fn handle_query_verifier_by_ip( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result> { + let Some(key) = self.ip_to_key(ip).await? else { + return Err(Error::MissingClientKernelEntry(ip.to_string())); + }; + + self.handle_query_verifier_by_key(&key, credential).await } async fn update_metrics(&self, new_host: &Host) { let now = SystemTime::now(); - const ACTIVITY_THRESHOLD: Duration = Duration::from_secs(60); + const ACTIVITY_THRESHOLD: Duration = Duration::from_secs(180); let old_host = self.host_information.read().await; @@ -279,26 +308,51 @@ impl PeerController { let mut new_tx = 0; for (peer_key, peer) in new_host.peers.iter() { - // only consider pre-existing peers, - // so that the value would always be increasing - if let Some(prior) = old_host.peers.get(peer_key) { - let delta_rx = peer.rx_bytes.saturating_sub(prior.rx_bytes); - let delta_tx = prior.tx_bytes.saturating_sub(prior.tx_bytes); + match old_host.peers.get(peer_key) { + // only consider pre-existing peers for the purposes of bandwidth accounting, + // so that the value would always be increasing. + Some(prior) => { + // 1. determine bandwidth changes + let delta_rx = peer.rx_bytes.saturating_sub(prior.rx_bytes); + let delta_tx = peer.tx_bytes.saturating_sub(prior.tx_bytes); - new_rx += delta_rx; - new_tx += delta_tx; - } + new_rx += delta_rx; + new_tx += delta_tx; - // if a peer hasn't performed a handshake in last minute, - // I think it's reasonable to assume it's no longer active - let Some(last_handshake) = peer.last_handshake else { - continue; - }; - let Ok(elapsed) = now.duration_since(last_handshake) else { - continue; - }; - if elapsed < ACTIVITY_THRESHOLD { - active_peers += 1; + // 2. attempt to determine if the peer is still active + + // 2.1. if there were bytes sent and received on the link since last it was called, + // the peer is definitely still active + if delta_rx > 0 && delta_tx > 0 { + active_peers += 1; + continue; + } + + // 2.2. otherwise attempt to look at time since last handshake - + // if no handshake occurred in the last 3min, we assume the connection might be dead + let Some(last_handshake) = peer.last_handshake else { + continue; + }; + let Ok(elapsed) = now.duration_since(last_handshake) else { + continue; + }; + if elapsed < ACTIVITY_THRESHOLD { + active_peers += 1; + } + } + None => { + // if it's a brand-new peer, and it hasn't repeated the handshake in the last 3 min, + // we assume the connection might be dead + let Some(last_handshake) = peer.last_handshake else { + continue; + }; + let Ok(elapsed) = now.duration_since(last_handshake) else { + continue; + }; + if elapsed < ACTIVITY_THRESHOLD { + active_peers += 1; + } + } } } @@ -321,7 +375,7 @@ impl PeerController { loop { tokio::select! { _ = self.timeout_check_interval.next() => { - let Ok(host) = self.wg_api.inner.read_interface_data() else { + let Ok(host) = self.wg_api.read_interface_data() else { log::error!("Can't read wireguard kernel data"); continue; }; @@ -335,38 +389,30 @@ impl PeerController { } msg = self.request_rx.recv() => { match msg { - Some(PeerControlRequest::AddPeer { peer, client_id, response_tx }) => { - let ret = self.handle_add_request(&peer, client_id).await; - if ret.is_ok() { - response_tx.send(AddPeerControlResponse { success: true }).ok(); - } else { - response_tx.send(AddPeerControlResponse { success: false }).ok(); - } + Some(PeerControlRequest::AddPeer { peer, response_tx }) => { + response_tx.send(self.handle_add_request(&peer).await).ok(); } Some(PeerControlRequest::RemovePeer { key, response_tx }) => { - let success = self.remove_peer(&key).await.is_ok(); - response_tx.send(RemovePeerControlResponse { success }).ok(); + response_tx.send(self.remove_peer(&key).await).ok(); } Some(PeerControlRequest::QueryPeer { key, response_tx }) => { - let ret = self.handle_query_peer(&key).await; - if let Ok(peer) = ret { - response_tx.send(QueryPeerControlResponse { success: true, peer }).ok(); - } else { - response_tx.send(QueryPeerControlResponse { success: false, peer: None }).ok(); - } + response_tx.send(self.handle_query_peer_by_key(&key).await).ok(); } - Some(PeerControlRequest::QueryBandwidth { key, response_tx }) => { - let ret = self.handle_query_bandwidth(&key).await; - if let Ok(bandwidth_data) = ret { - response_tx.send(QueryBandwidthControlResponse { success: true, bandwidth_data }).ok(); - } else { - response_tx.send(QueryBandwidthControlResponse { success: false, bandwidth_data: None }).ok(); - } + Some(PeerControlRequest::GetClientBandwidthByKey { key, response_tx }) => { + response_tx.send(self.handle_get_client_bandwidth_by_key(&key).await).ok(); + } + Some(PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }) => { + response_tx.send(self.handle_get_client_bandwidth_by_ip(ip).await).ok(); + } + Some(PeerControlRequest::GetVerifierByKey { key, credential, response_tx }) => { + response_tx.send(self.handle_query_verifier_by_key(&key, *credential).await).ok(); + } + Some(PeerControlRequest::GetVerifierByIp { ip, credential, response_tx }) => { + response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok(); } None => { log::trace!("PeerController [main loop]: stopping since channel closed"); break; - } } } @@ -374,3 +420,140 @@ impl PeerController { } } } + +#[cfg(feature = "mock")] +#[derive(Default)] +struct MockWgApi { + peers: std::sync::RwLock>, +} + +#[cfg(feature = "mock")] +impl WireguardInterfaceApi for MockWgApi { + fn create_interface( + &self, + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } + + fn assign_address( + &self, + _address: &defguard_wireguard_rs::net::IpAddrMask, + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } + + fn configure_peer_routing( + &self, + _peers: &[Peer], + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } + + #[cfg(not(target_os = "windows"))] + fn configure_interface( + &self, + _config: &defguard_wireguard_rs::InterfaceConfiguration, + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } + + #[cfg(target_os = "windows")] + fn configure_interface( + &self, + _config: &defguard_wireguard_rs::InterfaceConfiguration, + _dns: &[std::net::IpAddr], + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } + + fn remove_interface( + &self, + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } + + fn configure_peer( + &self, + peer: &Peer, + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.peers + .write() + .unwrap() + .insert(peer.public_key.clone(), peer.clone()); + Ok(()) + } + + fn remove_peer( + &self, + peer_pubkey: &Key, + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + self.peers.write().unwrap().remove(peer_pubkey); + Ok(()) + } + + fn read_interface_data( + &self, + ) -> std::result::Result { + let mut host = Host::default(); + host.peers = self.peers.read().unwrap().clone(); + Ok(host) + } + + fn configure_dns( + &self, + _dns: &[std::net::IpAddr], + ) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> { + todo!() + } +} + +#[cfg(feature = "mock")] +pub fn start_controller( + request_tx: mpsc::Sender, + request_rx: mpsc::Receiver, +) -> ( + Arc>, + nym_task::TaskManager, +) { + use std::sync::Arc; + + let storage = Arc::new(RwLock::new( + nym_gateway_storage::traits::mock::MockGatewayStorage::default(), + )); + let ecash_manager = Arc::new(nym_credential_verification::ecash::MockEcashManager::new( + Box::new(storage.clone()), + )); + let wg_api = Arc::new(MockWgApi::default()); + let task_manager = nym_task::TaskManager::default(); + let mut peer_controller = PeerController::new( + ecash_manager, + Default::default(), + wg_api, + Default::default(), + Default::default(), + request_tx, + request_rx, + task_manager.subscribe(), + ); + tokio::spawn(async move { peer_controller.run().await }); + + (storage, task_manager) +} + +#[cfg(feature = "mock")] +pub async fn stop_controller(mut task_manager: nym_task::TaskManager) { + task_manager.signal_shutdown().unwrap(); + task_manager.wait_for_shutdown().await; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn start_and_stop() { + let (request_tx, request_rx) = mpsc::channel(1); + let (_, task_manager) = start_controller(request_tx.clone(), request_rx); + stop_controller(task_manager).await; + } +} diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 7112972058..9eda055b2e 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -3,40 +3,55 @@ use crate::error::Error; use crate::peer_controller::PeerControlRequest; -use crate::peer_storage_manager::PeerStorageManager; -use defguard_wireguard_rs::host::Peer; -use defguard_wireguard_rs::{host::Host, key::Key}; +use crate::peer_storage_manager::{CachedPeerManager, PeerInformation}; +use defguard_wireguard_rs::{host::Host, key::Key, net::IpAddrMask}; use futures::channel::oneshot; -use nym_authenticator_requests::latest::registration::BANDWIDTH_CAP_PER_DAY; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; -use nym_gateway_storage::models::WireguardPeer; use nym_task::TaskClient; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; use std::sync::Arc; -use std::time::{Duration, SystemTime}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; -pub(crate) type SharedBandwidthStorageManager = Arc>; -const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 30 days +#[derive(Clone)] +pub(crate) struct SharedBandwidthStorageManager { + inner: Arc>, + allowed_ips: Vec, +} + +impl SharedBandwidthStorageManager { + pub(crate) fn new( + inner: Arc>, + allowed_ips: Vec, + ) -> Self { + Self { inner, allowed_ips } + } + + pub(crate) fn inner(&self) -> &RwLock { + &self.inner + } + + pub(crate) fn allowed_ips(&self) -> &[IpAddrMask] { + &self.allowed_ips + } +} pub struct PeerHandle { public_key: Key, host_information: Arc>, - peer_storage_manager: PeerStorageManager, - bandwidth_storage_manager: Option, + cached_peer: CachedPeerManager, + bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, timeout_check_interval: IntervalStream, task_client: TaskClient, - startup_timestamp: SystemTime, } impl PeerHandle { - pub fn new( + pub(crate) fn new( public_key: Key, host_information: Arc>, - peer_storage_manager: PeerStorageManager, - bandwidth_storage_manager: Option, + cached_peer: CachedPeerManager, + bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, task_client: &TaskClient, ) -> Self { @@ -48,12 +63,11 @@ impl PeerHandle { PeerHandle { public_key, host_information, - peer_storage_manager, + cached_peer, bandwidth_storage_manager, request_tx, timeout_check_interval, task_client, - startup_timestamp: SystemTime::now(), } } @@ -69,96 +83,134 @@ impl PeerHandle { let success = response_rx .await .map_err(|_| Error::Internal("peer controller didn't respond".to_string()))? - .success; + .inspect_err(|err| tracing::error!("Could not remove peer: {err:?}")) + .is_ok(); Ok(success) } - async fn active_peer( - &mut self, - storage_peer: &WireguardPeer, - kernel_peer: &Peer, - ) -> Result { - if let Some(bandwidth_manager) = &self.bandwidth_storage_manager { - let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes) - .checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64) - .ok_or(Error::InconsistentConsumedBytes)? - .try_into() - .map_err(|_| Error::InconsistentConsumedBytes)?; - if spent_bandwidth > 0 { - self.peer_storage_manager.update_trx(kernel_peer); - if bandwidth_manager - .write() - .await - .try_use_bandwidth(spent_bandwidth) - .await - .is_err() - { - let success = self.remove_peer().await?; - self.peer_storage_manager.remove_peer(); - return Ok(!success); - } - } - } else { - if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER { - log::debug!( - "Peer {} has been present for 30 days, removing it", - self.public_key + fn compute_spent_bandwidth( + kernel_peer: PeerInformation, + cached_peer: PeerInformation, + ) -> Option { + let kernel_total = kernel_peer + .rx_bytes + .checked_add(kernel_peer.tx_bytes) + .or_else(|| { + tracing::error!( + "Overflow on kernel adding bytes: {} + {}", + kernel_peer.rx_bytes, + kernel_peer.tx_bytes ); - let success = self.remove_peer().await?; - return Ok(!success); - } - let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes; - if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY { - log::debug!( - "Peer {} doesn't have bandwidth anymore, removing it", - self.public_key + None + })?; + let cached_total = cached_peer + .rx_bytes + .checked_add(cached_peer.tx_bytes) + .or_else(|| { + tracing::error!( + "Overflow on cached adding bytes: {} + {}", + cached_peer.rx_bytes, + cached_peer.tx_bytes ); - let success = self.remove_peer().await?; - return Ok(!success); - } + None + })?; + + kernel_total.checked_sub(cached_total).or_else(|| { + tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); + None + }) + } + + async fn active_peer(&mut self, kernel_peer: PeerInformation) -> Result { + let Some(cached_peer) = self.cached_peer.get_peer() else { + log::debug!( + "Peer {:?} not in storage anymore, shutting down handle", + self.public_key + ); + return Ok(false); + }; + + let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, cached_peer) + .unwrap_or_default() + .try_into() + .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) + .unwrap_or_default(); + + self.cached_peer.update(kernel_peer); + + if spent_bandwidth > 0 + && self + .bandwidth_storage_manager + .inner() + .write() + .await + .try_use_bandwidth(spent_bandwidth) + .await + .is_err() + { + tracing::debug!( + "Peer {} is out of bandwidth, removing it", + self.public_key.to_string() + ); + let success = self.remove_peer().await?; + self.cached_peer.remove_peer(); + return Ok(!success); } Ok(true) } - pub async fn run(&mut self) -> Result<(), Error> { + async fn continue_checking(&mut self) -> Result { + let kernel_peer = self + .host_information + .read() + .await + .peers + .get(&self.public_key) + .ok_or(Error::MissingClientKernelEntry(self.public_key.to_string()))? + .into(); + if !self.active_peer(kernel_peer).await? { + log::debug!( + "Peer {:?} is not active anymore, shutting down handle", + self.public_key + ); + Ok(false) + } else { + Ok(true) + } + } + + pub async fn run(&mut self) { while !self.task_client.is_shutdown() { tokio::select! { _ = self.timeout_check_interval.next() => { - let Some(kernel_peer) = self - .host_information - .read() - .await - .peers - .get(&self.public_key) - .cloned() else { - // the host information hasn't beed updated yet - continue; - }; - let Some(storage_peer) = self.peer_storage_manager.get_peer() else { - log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key); - return Ok(()); - }; - if !self.active_peer(&storage_peer, &kernel_peer).await? { - log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key); - return Ok(()); - } else { - // Update storage values - self.peer_storage_manager.sync_storage_peer().await?; + match self.continue_checking().await { + Ok(true) => continue, + Ok(false) => return, + Err(err) => { + match self.remove_peer().await { + Ok(true) => { + tracing::debug!("Removed peer due to error {err}"); + return; + } + _ => { + tracing::warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); + continue; + } + } + }, } } _ = self.task_client.recv() => { log::trace!("PeerHandle: Received shutdown"); - if let Some(bandwidth_manager) = &self.bandwidth_storage_manager { - if let Err(e) = bandwidth_manager.write().await.sync_storage_bandwidth().await { - log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); - } + if let Err(e) = self.bandwidth_storage_manager.inner().write().await.sync_storage_bandwidth().await { + log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); } + log::trace!("PeerHandle: Finished shutdown"); } } } - Ok(()) } } diff --git a/common/wireguard/src/peer_storage_manager.rs b/common/wireguard/src/peer_storage_manager.rs index c2567ce83a..1675cf6b2f 100644 --- a/common/wireguard/src/peer_storage_manager.rs +++ b/common/wireguard/src/peer_storage_manager.rs @@ -1,12 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::Error; use defguard_wireguard_rs::host::Peer; -use nym_gateway_storage::models::WireguardPeer; -use nym_gateway_storage::GatewayStorage; use std::time::Duration; -use time::OffsetDateTime; const DEFAULT_PEER_MAX_FLUSHING_RATE: Duration = Duration::from_secs(60 * 60 * 24); // 24h const DEFAULT_PEER_MAX_DELTA_FLUSHING_AMOUNT: u64 = 512 * 1024 * 1024; // 512MB @@ -29,110 +25,50 @@ impl Default for PeerFlushingBehaviourConfig { } } -pub struct PeerStorageManager { - pub(crate) storage: GatewayStorage, +pub struct CachedPeerManager { pub(crate) peer_information: Option, - pub(crate) cfg: PeerFlushingBehaviourConfig, - pub(crate) with_client_id: bool, } -impl PeerStorageManager { - pub(crate) fn new(storage: GatewayStorage, peer: Peer, with_client_id: bool) -> Self { - let peer_information = Some(PeerInformation::new(peer)); +impl CachedPeerManager { + pub(crate) fn new(peer: &Peer) -> Self { Self { - storage, - peer_information, - cfg: PeerFlushingBehaviourConfig::default(), - with_client_id, + peer_information: Some(peer.into()), } } - pub(crate) fn get_peer(&self) -> Option { + pub(crate) fn get_peer(&self) -> Option { self.peer_information - .as_ref() - .map(|p| p.peer.clone().into()) } pub(crate) fn remove_peer(&mut self) { self.peer_information = None; } - pub(crate) fn update_trx(&mut self, kernel_peer: &Peer) { + pub(crate) fn update(&mut self, kernel_peer: PeerInformation) { if let Some(peer_information) = self.peer_information.as_mut() { - peer_information.update_trx_bytes(kernel_peer.tx_bytes, kernel_peer.rx_bytes); + peer_information.update_trx_bytes(kernel_peer); } } - - pub(crate) async fn sync_storage_peer(&mut self) -> Result<(), Error> { - let Some(peer_information) = self.peer_information.as_mut() else { - return Ok(()); - }; - if !peer_information.should_sync(self.cfg) { - return Ok(()); - } - if self - .storage - .get_wireguard_peer(&peer_information.peer().public_key.to_string()) - .await? - .is_none() - { - self.peer_information = None; - return Ok(()); - } - self.storage - .insert_wireguard_peer(peer_information.peer(), self.with_client_id) - .await?; - - peer_information.resync_peer_with_storage(); - - Ok(()) - } } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub(crate) struct PeerInformation { - pub(crate) peer: Peer, - pub(crate) last_synced: OffsetDateTime, + pub(crate) tx_bytes: u64, + pub(crate) rx_bytes: u64, +} - pub(crate) bytes_delta_since_sync: u64, +impl From<&Peer> for PeerInformation { + fn from(value: &Peer) -> Self { + Self { + tx_bytes: value.tx_bytes, + rx_bytes: value.rx_bytes, + } + } } impl PeerInformation { - pub fn new(peer: Peer) -> PeerInformation { - PeerInformation { - peer, - last_synced: OffsetDateTime::now_utc(), - bytes_delta_since_sync: 0, - } - } - - pub(crate) fn should_sync(&self, cfg: PeerFlushingBehaviourConfig) -> bool { - if self.bytes_delta_since_sync >= cfg.peer_max_delta_flushing_amount { - return true; - } - - if self.last_synced + cfg.peer_max_flushing_rate < OffsetDateTime::now_utc() - && self.bytes_delta_since_sync != 0 - { - return true; - } - - false - } - - pub(crate) fn peer(&self) -> &Peer { - &self.peer - } - - pub(crate) fn update_trx_bytes(&mut self, tx_bytes: u64, rx_bytes: u64) { - self.bytes_delta_since_sync += tx_bytes.saturating_sub(self.peer.tx_bytes) - + rx_bytes.saturating_sub(self.peer.rx_bytes); - self.peer.tx_bytes = tx_bytes; - self.peer.rx_bytes = rx_bytes; - } - - pub(crate) fn resync_peer_with_storage(&mut self) { - self.bytes_delta_since_sync = 0; - self.last_synced = OffsetDateTime::now_utc(); + pub(crate) fn update_trx_bytes(&mut self, peer: PeerInformation) { + self.tx_bytes = peer.tx_bytes; + self.rx_bytes = peer.rx_bytes; } } diff --git a/common/zulip-client/Cargo.toml b/common/zulip-client/Cargo.toml new file mode 100644 index 0000000000..acfc84a540 --- /dev/null +++ b/common/zulip-client/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "zulip-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +thiserror = { workspace = true } + +itertools = { workspace = true } +url = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +zeroize = { workspace = true } + +nym-bin-common = { path = "../bin-common" } +nym-http-api-client = { path = "../http-api-client" } +reqwest = { workspace = true } +tracing = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/common/zulip-client/src/client.rs b/common/zulip-client/src/client.rs new file mode 100644 index 0000000000..60554e5c25 --- /dev/null +++ b/common/zulip-client/src/client.rs @@ -0,0 +1,165 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +//! An incomplete Zulip API Client +//! +//! Currently, it serves a single purpose: to send a message to a server, +//! however, it could very easily be extended with additional functionalities. +//! +//! ## Sending Direct Message +//! +//! ```rust +//! # use zulip_client::{Client, ZulipClientError}; +//! # use zulip_client::message::DirectMessage; +//! # async fn try_send() -> Result<(), ZulipClientError> { +//! let api_key = "your-api-key"; +//! let email = "associated-email-address"; +//! let server = "https://server-address.com"; +//! let client = Client::builder(email, api_key, server)?.build()?; +//! // send to userid 12 +//! client.send_message((12u32, "hello world")).await?; +//! // more concrete typing +//! client.send_message(DirectMessage::new(12, "hello world2")).await?; +//! # Ok(()) +//! # } +//! ``` + +use crate::error::ZulipClientError; +use crate::message::{DirectMessage, SendMessageResponse, SendableMessage, StreamMessage}; +use nym_bin_common::bin_info; +use nym_http_api_client::UserAgent; +use reqwest::{header, Method, RequestBuilder}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tracing::trace; +use url::Url; +use zeroize::Zeroizing; + +#[derive(Serialize, Deserialize)] +pub struct ClientConfig { + pub user_email: String, + pub api_key: String, + // TODO: introduce validation + pub user_agent: Option, + pub server_url: Url, +} + +pub struct Client { + server_url: Url, + + api_key: Zeroizing, + user_email: String, + + inner_client: reqwest::Client, +} + +fn default_user_agent() -> String { + UserAgent::from(bin_info!()).to_string() +} + +impl Client { + const MESSAGES_ENDPOINT: &'static str = "/api/v1/messages"; + + pub fn builder( + user_email: impl Into, + api_key: impl Into, + server_url: impl Into, + ) -> Result { + ClientBuilder::new(user_email, api_key, server_url) + } + + pub fn new(config: ClientConfig) -> Result { + let builder = ClientBuilder::new(config.user_email, config.api_key, config.server_url)?; + match config.user_agent { + Some(user_agent) => builder.user_agent(user_agent).build(), + None => builder.build(), + } + } + + pub async fn send_message( + &self, + msg: impl Into, + ) -> Result { + let url = format!("{}{}", self.server_url, Self::MESSAGES_ENDPOINT); + + self.build_request(Method::POST, Self::MESSAGES_ENDPOINT) + .form(&msg.into()) + .send() + .await + .map_err(|source| ZulipClientError::RequestSendingFailure { source, url })? + .json() + .await + .map_err(|source| ZulipClientError::RequestDecodeFailure { source }) + } + + pub async fn send_direct_message( + &self, + msg: impl Into, + ) -> Result { + self.send_message(msg.into()).await + } + + pub async fn send_channel_message( + &self, + msg: impl Into, + ) -> Result { + self.send_message(msg.into()).await + } + + fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder { + let url = format!("{}{endpoint}", self.server_url); + trace!("posting to {url}"); + + self.inner_client + .request(method, url) + .basic_auth(&self.user_email, Some(self.api_key.to_string())) + .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") + } +} + +pub struct ClientBuilder { + api_key: Zeroizing, + user_email: String, + server_url: Url, + user_agent: Option, +} + +impl ClientBuilder { + pub fn new( + user_email: impl Into, + api_key: impl Into, + server_url: impl Into, + ) -> Result { + let server_url = server_url.into(); + let parsed_url = + Url::from_str(&server_url).map_err(|source| ZulipClientError::MalformedServerUrl { + raw: server_url, + source, + })?; + Ok(ClientBuilder { + api_key: Zeroizing::new(api_key.into()), + user_email: user_email.into(), + server_url: parsed_url, + user_agent: None, + }) + } + + #[must_use] + pub fn user_agent(mut self, user_agent: impl Into) -> Self { + self.user_agent = Some(user_agent.into()); + self + } + + pub fn build(self) -> Result { + let user_agent = self.user_agent.unwrap_or_else(default_user_agent); + Ok(Client { + api_key: self.api_key, + server_url: self.server_url, + user_email: self.user_email, + inner_client: reqwest::ClientBuilder::new() + .user_agent(user_agent) + .build() + .map_err(|source| ZulipClientError::ClientBuildFailure { source })?, + }) + } +} diff --git a/common/zulip-client/src/error.rs b/common/zulip-client/src/error.rs new file mode 100644 index 0000000000..8556833855 --- /dev/null +++ b/common/zulip-client/src/error.rs @@ -0,0 +1,22 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ZulipClientError { + #[error("failed to send request to {url}: {source}")] + RequestSendingFailure { url: String, source: reqwest::Error }, + + #[error("failed to decode received response: {source}")] + RequestDecodeFailure { source: reqwest::Error }, + + #[error("failed to build internal client: {source}")] + ClientBuildFailure { source: reqwest::Error }, + + #[error("provided url ({raw}) is malformed: {source}")] + MalformedServerUrl { + raw: String, + source: url::ParseError, + }, +} diff --git a/common/zulip-client/src/lib.rs b/common/zulip-client/src/lib.rs new file mode 100644 index 0000000000..a7c25f7c52 --- /dev/null +++ b/common/zulip-client/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod client; +pub mod error; +pub mod message; + +pub type Id = u32; + +pub use client::{Client, ClientBuilder}; +pub use error::ZulipClientError; diff --git a/common/zulip-client/src/message/mod.rs b/common/zulip-client/src/message/mod.rs new file mode 100644 index 0000000000..2877c724ee --- /dev/null +++ b/common/zulip-client/src/message/mod.rs @@ -0,0 +1,267 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::message::to::{ToChannel, ToDirect}; +use serde::{Deserialize, Serialize}; + +pub mod to; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "result")] +#[serde(rename_all = "snake_case")] +pub enum SendMessageResponse { + Success { + id: i64, + automatic_new_visibility_policy: Option, + msg: String, + }, + Error { + code: String, + msg: String, + stream: Option, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum SendableMessageContent { + // old name: 'private' + Direct { + // internally this is a list + to: String, + content: String, + }, + // alternative name: 'channel' + Stream { + to: String, + topic: Option, + content: String, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub struct SendableMessage { + #[serde(flatten)] + content: SendableMessageContent, + + /// For clients supporting local echo, the event queue ID for the client. + /// If passed, `local_id` is required. If the message is successfully sent, + /// the server will include `local_id` in the message event that the client with this `queue_id` + /// will receive notifying it of the new message via `GET /events`. + /// This lets the client know unambiguously that it should replace the locally echoed message, + /// rather than adding this new message + /// (which would be correct if the user had sent the new message from another device). + /// example: "fb67bf8a-c031-47cc-84cf-ed80accacda8" + queue_id: Option, + + /// For clients supporting local echo, a unique string-format identifier chosen freely by the client; + /// the server will pass it back to the client without inspecting it, as described in the `queue_id` description. + /// example: "100.01" + local_id: Option, + + /// Whether the message should be initially marked read by its sender. + /// If unspecified, the server uses a heuristic based on the client name. + read_by_sender: bool, +} + +impl SendableMessage { + pub fn new(content: impl Into) -> Self { + SendableMessage { + content: content.into(), + queue_id: None, + local_id: None, + read_by_sender: false, + } + } + + #[must_use] + pub fn with_queue(mut self, queue_id: impl Into, local_id: impl Into) -> Self { + self.queue_id = Some(queue_id.into()); + self.local_id = Some(local_id.into()); + self + } + + #[must_use] + pub fn read_by_sender(mut self, read_by_sender: bool) -> Self { + self.read_by_sender = read_by_sender; + self + } +} + +pub type PrivateMessage = DirectMessage; + +pub struct DirectMessage { + to: String, + content: String, +} + +impl DirectMessage { + pub fn new(to: impl Into, content: impl Into) -> Self { + DirectMessage { + to: to.into().to_string(), + content: content.into(), + } + } +} + +pub type ChannelMessage = StreamMessage; +pub struct StreamMessage { + to: String, + topic: Option, + content: String, +} + +impl StreamMessage { + pub fn new( + to: impl Into, + content: impl Into, + topic: impl IntoMaybeTopic, + ) -> Self { + StreamMessage { + to: to.into().to_string(), + topic: topic.into_maybe_topic(), + content: content.into(), + } + } + + pub fn no_topic(to: impl Into, content: impl Into) -> Self { + Self::new(to, content, None::) + } + + #[must_use] + pub fn with_topic(mut self, topic: impl Into) -> Self { + self.topic = Some(topic.into()); + self + } +} + +impl From for SendableMessage { + fn from(content: SendableMessageContent) -> Self { + SendableMessage::new(content) + } +} + +impl From for SendableMessage { + fn from(msg: DirectMessage) -> Self { + SendableMessageContent::from(msg).into() + } +} + +impl From for SendableMessageContent { + fn from(msg: DirectMessage) -> Self { + SendableMessageContent::Direct { + to: msg.to, + content: msg.content, + } + } +} + +impl From<(T, S)> for DirectMessage +where + T: Into, + S: Into, +{ + fn from((to, content): (T, S)) -> Self { + DirectMessage::new(to, content) + } +} + +impl From<(T, S)> for SendableMessage +where + T: Into, + S: Into, +{ + fn from((to, content): (T, S)) -> Self { + DirectMessage::new(to, content).into() + } +} + +impl From for SendableMessage { + fn from(msg: StreamMessage) -> Self { + SendableMessageContent::from(msg).into() + } +} + +impl From for SendableMessageContent { + fn from(msg: StreamMessage) -> Self { + SendableMessageContent::Stream { + to: msg.to, + topic: msg.topic, + content: msg.content, + } + } +} + +impl From<(T, S, U)> for StreamMessage +where + T: Into, + S: Into, + U: IntoMaybeTopic, +{ + fn from((to, content, topic): (T, S, U)) -> Self { + StreamMessage::new(to, content, topic) + } +} + +impl From<(T, S)> for StreamMessage +where + T: Into, + S: Into, +{ + fn from((to, content): (T, S)) -> Self { + StreamMessage::no_topic(to, content) + } +} + +impl From<(T, S, U)> for SendableMessage +where + T: Into, + S: Into, + U: IntoMaybeTopic, +{ + fn from(inner: (T, S, U)) -> Self { + StreamMessage::from(inner).into() + } +} + +pub trait IntoMaybeTopic { + fn into_maybe_topic(self) -> Option; +} + +impl IntoMaybeTopic for &Option +where + S: Into + Clone, +{ + fn into_maybe_topic(self) -> Option { + self.clone().map(|s| s.into()) + } +} + +impl IntoMaybeTopic for Option +where + S: Into, +{ + fn into_maybe_topic(self) -> Option { + self.map(Into::into) + } +} + +impl IntoMaybeTopic for String { + fn into_maybe_topic(self) -> Option { + Some(self) + } +} + +impl IntoMaybeTopic for &String { + fn into_maybe_topic(self) -> Option { + Some(self.clone()) + } +} + +impl IntoMaybeTopic for &str { + fn into_maybe_topic(self) -> Option { + Some(self.to_string()) + } +} diff --git a/common/zulip-client/src/message/to.rs b/common/zulip-client/src/message/to.rs new file mode 100644 index 0000000000..0bafc3fe11 --- /dev/null +++ b/common/zulip-client/src/message/to.rs @@ -0,0 +1,128 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::Id; +use itertools::Itertools; +use std::fmt::Display; + +// from the docs: +// For channel messages, either the name or integer ID of the channel. +// For direct messages, either a list containing integer user IDs +// or a list containing string Zulip API email addresses. +pub enum ToDirect { + ByIds(Vec), + ByNames(Vec), +} + +impl Display for ToDirect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ToDirect::ByIds(ids) => write!(f, "[{}]", ids.iter().join(",")), + ToDirect::ByNames(names) => { + write!(f, "[{}]", names.join(",")) + } + } + } +} + +impl From> for ToDirect { + fn from(names: Vec) -> Self { + ToDirect::ByNames(names) + } +} + +impl From<&[String]> for ToDirect { + fn from(names: &[String]) -> Self { + names.to_vec().into() + } +} + +impl From<&[&str]> for ToDirect { + fn from(names: &[&str]) -> Self { + names + .iter() + .map(|s| s.to_string()) + .collect::>() + .into() + } +} + +impl From<&[&str; N]> for ToDirect { + fn from(names: &[&str; N]) -> Self { + names.as_slice().into() + } +} + +impl From> for ToDirect { + fn from(names: Vec<&str>) -> Self { + names.as_slice().into() + } +} + +impl From for ToDirect { + fn from(name: String) -> Self { + ToDirect::ByNames(vec![name]) + } +} + +impl From<&str> for ToDirect { + fn from(name: &str) -> Self { + name.to_string().into() + } +} + +impl From for ToDirect { + fn from(id: Id) -> Self { + ToDirect::ByIds(vec![id]) + } +} + +impl From<&[Id]> for ToDirect { + fn from(ids: &[Id]) -> Self { + ids.to_vec().into() + } +} + +impl From<&[Id; N]> for ToDirect { + fn from(ids: &[Id; N]) -> Self { + ids.as_slice().into() + } +} + +impl From> for ToDirect { + fn from(ids: Vec) -> Self { + ToDirect::ByIds(ids) + } +} + +pub enum ToChannel { + ByName(String), + ById(Id), +} + +impl Display for ToChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ToChannel::ByName(name) => name.fmt(f), + ToChannel::ById(id) => id.fmt(f), + } + } +} + +impl From for ToChannel { + fn from(name: String) -> Self { + ToChannel::ByName(name) + } +} + +impl From<&str> for ToChannel { + fn from(name: &str) -> Self { + name.to_string().into() + } +} + +impl From for ToChannel { + fn from(id: Id) -> Self { + ToChannel::ById(id) + } +} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index f246379774..67097b0840 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "ark-bls12-381" @@ -266,6 +266,20 @@ dependencies = [ "thiserror 1.0.64", ] +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -730,7 +744,7 @@ version = "0.1.0" dependencies = [ "cosmwasm-std", "quote", - "syn 1.0.109", + "syn 2.0.98", ] [[package]] @@ -1077,6 +1091,7 @@ dependencies = [ name = "nym-coconut-dkg" version = "0.1.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-controllers", @@ -1119,6 +1134,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common-testing" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "rand", + "rand_chacha", + "serde", +] + [[package]] name = "nym-crypto" version = "0.4.0" @@ -1193,7 +1221,6 @@ version = "1.5.1" dependencies = [ "anyhow", "bs58", - "cosmwasm-derive", "cosmwasm-schema", "cosmwasm-std", "cw-controllers", @@ -1201,15 +1228,15 @@ dependencies = [ "cw2", "easy-addr", "nym-contracts-common", + "nym-contracts-common-testing", "nym-crypto", + "nym-mixnet-contract", "nym-mixnet-contract-common", "nym-vesting-contract-common", "rand", "rand_chacha", "semver", "serde", - "thiserror 2.0.12", - "time", ] [[package]] @@ -1227,7 +1254,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -1252,7 +1278,7 @@ dependencies = [ name = "nym-network-defaults" version = "0.1.0" dependencies = [ - "cargo_metadata", + "cargo_metadata 0.19.2", "regex", ] @@ -1262,6 +1288,66 @@ version = "0.3.0" dependencies = [ "pem", "tracing", + "zeroize", +] + +[[package]] +name = "nym-performance-contract" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw2", + "nym-contracts-common", + "nym-contracts-common-testing", + "nym-crypto", + "nym-mixnet-contract", + "nym-mixnet-contract-common", + "nym-performance-contract-common", + "serde", +] + +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "nym-pool-contract" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw2", + "nym-contracts-common", + "nym-contracts-common-testing", + "nym-pool-contract-common", +] + +[[package]] +name = "nym-pool-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "schemars", + "serde", + "thiserror 2.0.12", ] [[package]] @@ -1991,7 +2077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" dependencies = [ "anyhow", - "cargo_metadata", + "cargo_metadata 0.18.1", "cfg-if", "regex", "rustc_version", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index ce2f7df73b..ec3239772c 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -5,9 +5,11 @@ members = [ "ecash", "mixnet", "mixnet-vesting-integration-tests", + "nym-pool", "multisig/cw3-flex-multisig", "multisig/cw4-group", "vesting", + "performance", ] [workspace.package] @@ -46,9 +48,21 @@ cw3-fixed-multisig = "=2.0.0" cw4 = "=2.0.0" cw20 = "=2.0.0" cw20-base = "2.0.0" +rand = "0.8.5" +rand_chacha = "0.3.1" semver = "1.0.21" serde = "1.0.196" sylvia = "1.3.3" schemars = "0.8.16" thiserror = "2.0.11" + +[workspace.lints.clippy] +unwrap_used = "deny" +expect_used = "deny" +todo = "deny" +dbg_macro = "deny" +exit = "deny" +panic = "deny" +unimplemented = "deny" +unreachable = "deny" diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml index e65539ea41..e77171d8b8 100644 --- a/contracts/coconut-dkg/Cargo.toml +++ b/contracts/coconut-dkg/Cargo.toml @@ -29,6 +29,7 @@ cw4 = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" } cw-multi-test = { workspace = true } cw4-group = { path = "../multisig/cw4-group" } diff --git a/contracts/coconut-dkg/schema/nym-coconut-dkg.json b/contracts/coconut-dkg/schema/nym-coconut-dkg.json index 456631e501..677dadd012 100644 --- a/contracts/coconut-dkg/schema/nym-coconut-dkg.json +++ b/contracts/coconut-dkg/schema/nym-coconut-dkg.json @@ -361,6 +361,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_state_at_height" + ], + "properties": { + "get_epoch_state_at_height": { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -460,6 +483,80 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_dealers_addresses" + ], + "properties": { + "get_epoch_dealers_addresses": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_epoch_dealers" + ], + "properties": { + "get_epoch_dealers": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1858,6 +1955,375 @@ } } }, + "get_epoch_dealers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedDealerResponse", + "type": "object", + "required": [ + "dealers", + "per_page" + ], + "properties": { + "dealers": { + "type": "array", + "items": { + "$ref": "#/definitions/DealerDetails" + } + }, + "per_page": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "DealerDetails": { + "type": "object", + "required": [ + "address", + "announce_address", + "assigned_index", + "bte_public_key_with_proof", + "ed25519_identity" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "announce_address": { + "type": "string" + }, + "assigned_index": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "bte_public_key_with_proof": { + "type": "string" + }, + "ed25519_identity": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "get_epoch_dealers_addresses": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedDealerAddressesResponse", + "type": "object", + "required": [ + "dealers" + ], + "properties": { + "dealers": { + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "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" + } + } + }, + "get_epoch_state_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Epoch", + "anyOf": [ + { + "$ref": "#/definitions/Epoch" + }, + { + "type": "null" + } + ], + "definitions": { + "Epoch": { + "type": "object", + "required": [ + "epoch_id", + "state", + "state_progress", + "time_configuration" + ], + "properties": { + "deadline": { + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "state": { + "$ref": "#/definitions/EpochState" + }, + "state_progress": { + "$ref": "#/definitions/StateProgress" + }, + "time_configuration": { + "$ref": "#/definitions/TimeConfiguration" + } + }, + "additionalProperties": false + }, + "EpochState": { + "oneOf": [ + { + "type": "string", + "enum": [ + "waiting_initialisation", + "in_progress" + ] + }, + { + "type": "object", + "required": [ + "public_key_submission" + ], + "properties": { + "public_key_submission": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "dealing_exchange" + ], + "properties": { + "dealing_exchange": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "verification_key_submission" + ], + "properties": { + "verification_key_submission": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "verification_key_validation" + ], + "properties": { + "verification_key_validation": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "verification_key_finalization" + ], + "properties": { + "verification_key_finalization": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "StateProgress": { + "type": "object", + "required": [ + "registered_dealers", + "registered_resharing_dealers", + "submitted_dealings", + "submitted_key_shares", + "verified_keys" + ], + "properties": { + "registered_dealers": { + "description": "Counts the number of dealers that have registered in this epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "registered_resharing_dealers": { + "description": "Counts the number of resharing dealers that have registered in this epoch. This field is only populated during a resharing exchange. It is always <= registered_dealers.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "submitted_dealings": { + "description": "Counts the number of fully received dealings (i.e. full chunks) from all the allowed dealers.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "submitted_key_shares": { + "description": "Counts the number of submitted verification key shared from the dealers.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "verified_keys": { + "description": "Counts the number of verified key shares.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "TimeConfiguration": { + "type": "object", + "required": [ + "dealing_exchange_time_secs", + "in_progress_time_secs", + "public_key_submission_time_secs", + "verification_key_finalization_time_secs", + "verification_key_submission_time_secs", + "verification_key_validation_time_secs" + ], + "properties": { + "dealing_exchange_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "in_progress_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "public_key_submission_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "verification_key_finalization_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "verification_key_submission_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "verification_key_validation_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, "get_epoch_threshold": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "uint64", diff --git a/contracts/coconut-dkg/schema/raw/query.json b/contracts/coconut-dkg/schema/raw/query.json index 386632565c..65b9e038e2 100644 --- a/contracts/coconut-dkg/schema/raw/query.json +++ b/contracts/coconut-dkg/schema/raw/query.json @@ -28,6 +28,29 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_state_at_height" + ], + "properties": { + "get_epoch_state_at_height": { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -127,6 +150,80 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_epoch_dealers_addresses" + ], + "properties": { + "get_epoch_dealers_addresses": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_epoch_dealers" + ], + "properties": { + "get_epoch_dealers": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/coconut-dkg/schema/raw/response_to_get_epoch_dealers.json b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_dealers.json new file mode 100644 index 0000000000..12648de260 --- /dev/null +++ b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_dealers.json @@ -0,0 +1,70 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedDealerResponse", + "type": "object", + "required": [ + "dealers", + "per_page" + ], + "properties": { + "dealers": { + "type": "array", + "items": { + "$ref": "#/definitions/DealerDetails" + } + }, + "per_page": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "DealerDetails": { + "type": "object", + "required": [ + "address", + "announce_address", + "assigned_index", + "bte_public_key_with_proof", + "ed25519_identity" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "announce_address": { + "type": "string" + }, + "assigned_index": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "bte_public_key_with_proof": { + "type": "string" + }, + "ed25519_identity": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/coconut-dkg/schema/raw/response_to_get_epoch_dealers_addresses.json b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_dealers_addresses.json new file mode 100644 index 0000000000..529d250a0c --- /dev/null +++ b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_dealers_addresses.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedDealerAddressesResponse", + "type": "object", + "required": [ + "dealers" + ], + "properties": { + "dealers": { + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "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" + } + } +} diff --git a/contracts/coconut-dkg/schema/raw/response_to_get_epoch_state_at_height.json b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_state_at_height.json new file mode 100644 index 0000000000..8f6fb68afb --- /dev/null +++ b/contracts/coconut-dkg/schema/raw/response_to_get_epoch_state_at_height.json @@ -0,0 +1,265 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Epoch", + "anyOf": [ + { + "$ref": "#/definitions/Epoch" + }, + { + "type": "null" + } + ], + "definitions": { + "Epoch": { + "type": "object", + "required": [ + "epoch_id", + "state", + "state_progress", + "time_configuration" + ], + "properties": { + "deadline": { + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "epoch_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "state": { + "$ref": "#/definitions/EpochState" + }, + "state_progress": { + "$ref": "#/definitions/StateProgress" + }, + "time_configuration": { + "$ref": "#/definitions/TimeConfiguration" + } + }, + "additionalProperties": false + }, + "EpochState": { + "oneOf": [ + { + "type": "string", + "enum": [ + "waiting_initialisation", + "in_progress" + ] + }, + { + "type": "object", + "required": [ + "public_key_submission" + ], + "properties": { + "public_key_submission": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "dealing_exchange" + ], + "properties": { + "dealing_exchange": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "verification_key_submission" + ], + "properties": { + "verification_key_submission": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "verification_key_validation" + ], + "properties": { + "verification_key_validation": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "verification_key_finalization" + ], + "properties": { + "verification_key_finalization": { + "type": "object", + "required": [ + "resharing" + ], + "properties": { + "resharing": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "StateProgress": { + "type": "object", + "required": [ + "registered_dealers", + "registered_resharing_dealers", + "submitted_dealings", + "submitted_key_shares", + "verified_keys" + ], + "properties": { + "registered_dealers": { + "description": "Counts the number of dealers that have registered in this epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "registered_resharing_dealers": { + "description": "Counts the number of resharing dealers that have registered in this epoch. This field is only populated during a resharing exchange. It is always <= registered_dealers.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "submitted_dealings": { + "description": "Counts the number of fully received dealings (i.e. full chunks) from all the allowed dealers.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "submitted_key_shares": { + "description": "Counts the number of submitted verification key shared from the dealers.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "verified_keys": { + "description": "Counts the number of verified key shares.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "TimeConfiguration": { + "type": "object", + "required": [ + "dealing_exchange_time_secs", + "in_progress_time_secs", + "public_key_submission_time_secs", + "verification_key_finalization_time_secs", + "verification_key_submission_time_secs", + "verification_key_validation_time_secs" + ], + "properties": { + "dealing_exchange_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "in_progress_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "public_key_submission_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "verification_key_finalization_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "verification_key_submission_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "verification_key_validation_time_secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index 1324abf277..bbd649cea2 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -3,6 +3,7 @@ use crate::dealers::queries::{ query_current_dealers_paged, query_dealer_details, query_dealers_indices_paged, + query_epoch_dealers_addresses_paged, query_epoch_dealers_paged, query_registered_dealer_details, }; use crate::dealers::transactions::try_add_dealer; @@ -13,13 +14,14 @@ use crate::dealings::queries::{ use crate::dealings::transactions::{try_commit_dealings_chunk, try_submit_dealings_metadata}; use crate::epoch_state::queries::{ query_can_advance_state, query_current_epoch, query_current_epoch_threshold, - query_epoch_threshold, + query_epoch_at_height, query_epoch_threshold, }; -use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; +use crate::epoch_state::storage::save_epoch; use crate::epoch_state::transactions::{ try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing, }; use crate::error::ContractError; +use crate::queued_migrations::introduce_historical_epochs; use crate::state::queries::query_state; use crate::state::storage::{DKG_ADMIN, MULTISIG, STATE}; use crate::verification_key_shares::queries::{query_vk_share, query_vk_shares_paged}; @@ -67,8 +69,9 @@ pub fn instantiate( }; STATE.save(deps.storage, &state)?; - CURRENT_EPOCH.save( + save_epoch( deps.storage, + env.block.height, &Epoch::new( EpochState::WaitingInitialisation, 0, @@ -100,6 +103,7 @@ pub fn execute( resharing, } => try_add_dealer( deps, + env, info, bte_key_with_proof, identity_key, @@ -118,7 +122,7 @@ pub fn execute( try_commit_verification_key_share(deps, env, info, share, resharing) } ExecuteMsg::VerifyVerificationKeyShare { owner, resharing } => { - try_verify_verification_key_share(deps, info, owner, resharing) + try_verify_verification_key_share(deps, env, info, owner, resharing) } ExecuteMsg::AdvanceEpochState {} => try_advance_epoch_state(deps, env), ExecuteMsg::TriggerReset {} => try_trigger_reset(deps, env, info), @@ -131,6 +135,9 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result to_json_binary(&query_state(deps.storage)?)?, QueryMsg::GetCurrentEpochState {} => to_json_binary(&query_current_epoch(deps.storage)?)?, + QueryMsg::GetEpochStateAtHeight { height } => { + to_json_binary(&query_epoch_at_height(deps.storage, height)?)? + } QueryMsg::CanAdvanceState {} => { to_json_binary(&query_can_advance_state(deps.storage, env)?)? } @@ -151,6 +158,26 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_json_binary(&query_dealer_details(deps, dealer_address)?)? } + QueryMsg::GetEpochDealersAddresses { + epoch_id, + limit, + start_after, + } => to_json_binary(&query_epoch_dealers_addresses_paged( + deps, + epoch_id, + start_after, + limit, + )?)?, + QueryMsg::GetEpochDealers { + epoch_id, + limit, + start_after, + } => to_json_binary(&query_epoch_dealers_paged( + deps, + epoch_id, + start_after, + limit, + )?)?, QueryMsg::GetCurrentDealers { limit, start_after } => { to_json_binary(&query_current_dealers_paged(deps, start_after, limit)?)? } @@ -221,16 +248,11 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result, _env: Env, _msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // MAINNET MIGRATION ASSERTION - let epoch = CURRENT_EPOCH.load(deps.storage)?; - assert_eq!(0, epoch.epoch_id); - - let threshold = THRESHOLD.load(deps.storage)?; - EPOCH_THRESHOLDS.save(deps.storage, 0, &threshold)?; + introduce_historical_epochs(deps, env)?; Ok(Response::new()) } @@ -334,7 +356,7 @@ mod tests { let api = MockApi::default(); const MEMBER_SIZE: usize = 100; let members: [Addr; MEMBER_SIZE] = - std::array::from_fn(|idx| api.addr_make(&format!("member{}", idx))); + std::array::from_fn(|idx| api.addr_make(&format!("member{idx}"))); let mut app = AppBuilder::new().build(|router, _, storage| { router diff --git a/contracts/coconut-dkg/src/dealers/queries.rs b/contracts/coconut-dkg/src/dealers/queries.rs index b092d8c140..4a309405f7 100644 --- a/contracts/coconut-dkg/src/dealers/queries.rs +++ b/contracts/coconut-dkg/src/dealers/queries.rs @@ -5,12 +5,12 @@ use crate::dealers::storage::{ self, get_dealer_details, get_dealer_index, get_registration_details, DEALERS_INDICES, EPOCH_DEALERS_MAP, }; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::load_current_epoch; use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; use nym_coconut_dkg_common::dealer::{ - DealerDetailsResponse, DealerType, PagedDealerIndexResponse, PagedDealerResponse, - RegisteredDealerDetails, + DealerDetailsResponse, DealerType, PagedDealerAddressesResponse, PagedDealerIndexResponse, + PagedDealerResponse, RegisteredDealerDetails, }; use nym_coconut_dkg_common::types::{DealerDetails, EpochId}; @@ -23,7 +23,7 @@ pub fn query_registered_dealer_details( let epoch_id = match epoch_id { Some(epoch_id) => epoch_id, - None => CURRENT_EPOCH.load(deps.storage)?.epoch_id, + None => load_current_epoch(deps.storage)?.epoch_id, }; Ok(RegisteredDealerDetails { @@ -36,7 +36,7 @@ pub fn query_dealer_details( dealer_address: String, ) -> StdResult { let addr = deps.api.addr_validate(&dealer_address)?; - let current_epoch_id = CURRENT_EPOCH.load(deps.storage)?.epoch_id; + let current_epoch_id = load_current_epoch(deps.storage)?.epoch_id; // if the address has registration data for the current epoch, it means it's an active dealer if let Ok(dealer_details) = get_dealer_details(deps.storage, &addr, current_epoch_id) { @@ -82,8 +82,37 @@ pub fn query_dealers_indices_paged( Ok(PagedDealerIndexResponse::new(dealers, start_next_after)) } -pub fn query_current_dealers_paged( +pub fn query_epoch_dealers_addresses_paged( deps: Deps<'_>, + epoch_id: EpochId, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(storage::DEALERS_ADDRESSES_PAGE_DEFAULT_LIMIT) + .min(storage::DEALERS_ADDRESSES_PAGE_MAX_LIMIT) as usize; + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let dealers = EPOCH_DEALERS_MAP + .prefix(epoch_id) + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + let start_next_after = dealers.last().cloned(); + + Ok(PagedDealerAddressesResponse { + dealers, + start_next_after, + }) +} + +pub fn query_epoch_dealers_paged( + deps: Deps<'_>, + epoch_id: EpochId, start_after: Option, limit: Option, ) -> StdResult { @@ -96,10 +125,8 @@ pub fn query_current_dealers_paged( let start = addr.as_ref().map(Bound::exclusive); - let current_epoch_id = CURRENT_EPOCH.load(deps.storage)?.epoch_id; - let dealers = EPOCH_DEALERS_MAP - .prefix(current_epoch_id) + .prefix(epoch_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|res| { @@ -107,7 +134,7 @@ pub fn query_current_dealers_paged( // SAFETY: if we have DealerRegistrationDetails saved, it means we MUST also have its node index // otherwise some serious invariants have been broken in the contract, and we're in trouble #[allow(clippy::expect_used)] - let assigned_index = get_dealer_index(deps.storage, &address, current_epoch_id) + let assigned_index = get_dealer_index(deps.storage, &address, epoch_id) .expect("could not retrieve dealer index for a registered dealer"); DealerDetails { @@ -125,6 +152,15 @@ pub fn query_current_dealers_paged( Ok(PagedDealerResponse::new(dealers, limit, start_next_after)) } +pub fn query_current_dealers_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let current_epoch_id = load_current_epoch(deps.storage)?.epoch_id; + query_epoch_dealers_paged(deps, current_epoch_id, start_after, limit) +} + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -158,112 +194,330 @@ pub(crate) mod tests { } } - #[test] - fn dealers_empty_on_init() { - let deps = init_contract(); + #[cfg(test)] + mod current_epoch_dealers { + use super::*; - let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap(); - assert_eq!(0, page1.dealers.len() as u32); + #[test] + fn dealers_empty_on_init() { + let deps = init_contract(); + + let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap(); + assert_eq!(0, page1.dealers.len() as u32); + } + + #[test] + fn dealers_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let limit = 2; + + fill_dealers(&mut deps, 0, 1000); + + let page1 = + query_current_dealers_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.dealers.len() as u32); + + remove_dealers(&mut deps, 0, 1000); + } + + #[test] + fn dealers_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + + fill_dealers(&mut deps, 0, 1000); + + // query without explicitly setting a limit + let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32); + + remove_dealers(&mut deps, 0, 1000); + } + + #[test] + fn dealers_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT; + + fill_dealers(&mut deps, 0, 1000); + + let page1 = query_current_dealers_paged(deps.as_ref(), None, Option::from(crazy_limit)) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DEALERS_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.dealers.len() as u32); + + remove_dealers(&mut deps, 0, 1000); + } + + #[test] + fn dealers_pagination_works() { + let mut deps = init_contract(); + + let per_page = 2; + + fill_dealers(&mut deps, 0, 1); + let page1 = + query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.dealers.len()); + remove_dealers(&mut deps, 0, 1); + + fill_dealers(&mut deps, 0, 2); + // page1 should have 2 results on it + let page1 = + query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.dealers.len()); + remove_dealers(&mut deps, 0, 2); + + fill_dealers(&mut deps, 0, 3); + // page1 still has 2 results + let page1 = + query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.dealers.len()); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_current_dealers_paged( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.dealers.len()); + remove_dealers(&mut deps, 0, 3); + + fill_dealers(&mut deps, 0, 4); + let page1 = + query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let start_after = page1.start_next_after.unwrap(); + let page2 = query_current_dealers_paged( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.dealers.len()); + remove_dealers(&mut deps, 0, 4); + } + } + + #[cfg(test)] + mod epoch_dealers { + use super::*; + + #[test] + fn dealers_empty_on_init() { + let deps = init_contract(); + + // check few epochs + for epoch_id in 0..10 { + let page1 = query_epoch_dealers_paged(deps.as_ref(), epoch_id, None, None).unwrap(); + assert_eq!(0, page1.dealers.len() as u32); + } + } + + #[test] + fn theres_no_ovewriting_between_epochs() { + let mut deps = init_contract(); + + fill_dealers(&mut deps, 1, 1000); + + let page1 = query_epoch_dealers_paged(deps.as_ref(), 1, None, None).unwrap(); + assert!(!page1.dealers.is_empty()); + + // nothing for other epochs + let another_epoch = query_epoch_dealers_paged(deps.as_ref(), 2, None, None).unwrap(); + assert!(another_epoch.dealers.is_empty()); + + let another_epoch = query_epoch_dealers_paged(deps.as_ref(), 42, None, None).unwrap(); + assert!(another_epoch.dealers.is_empty()); + } + + #[test] + fn dealers_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let limit = 2; + + fill_dealers(&mut deps, 0, 1000); + + let page1 = + query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.dealers.len() as u32); + } + + #[test] + fn dealers_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + + fill_dealers(&mut deps, 0, 1000); + + // query without explicitly setting a limit + let page1 = query_epoch_dealers_paged(deps.as_ref(), 0, None, None).unwrap(); + + assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32); + } + + #[test] + fn dealers_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT; + + fill_dealers(&mut deps, 0, 1000); + + let page1 = + query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(crazy_limit)) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DEALERS_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.dealers.len() as u32); + } + + #[test] + fn dealers_pagination_works() { + let mut deps = init_contract(); + + let per_page = 2; + + fill_dealers(&mut deps, 0, 1); + let page1 = + query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.dealers.len()); + remove_dealers(&mut deps, 0, 1); + + fill_dealers(&mut deps, 0, 2); + // page1 should have 2 results on it + let page1 = + query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.dealers.len()); + remove_dealers(&mut deps, 0, 2); + + fill_dealers(&mut deps, 0, 3); + // page1 still has 2 results + let page1 = + query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.dealers.len()); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_epoch_dealers_paged( + deps.as_ref(), + 0, + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.dealers.len()); + remove_dealers(&mut deps, 0, 3); + + fill_dealers(&mut deps, 0, 4); + let page1 = + query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap(); + let start_after = page1.start_next_after.unwrap(); + let page2 = query_epoch_dealers_paged( + deps.as_ref(), + 0, + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.dealers.len()); + } } #[test] - fn dealers_paged_retrieval_obeys_limits() { - let mut deps = init_contract(); - let limit = 2; - - fill_dealers(&mut deps, 0, 1000); - - let page1 = query_current_dealers_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); - assert_eq!(limit, page1.dealers.len() as u32); - - remove_dealers(&mut deps, 0, 1000); - } - - #[test] - fn dealers_paged_retrieval_has_default_limit() { + fn epoch_dealers_addresses() { let mut deps = init_contract(); - fill_dealers(&mut deps, 0, 1000); + let mut fixtures = Vec::new(); + for i in 0..100 { + let mut dealer_details = dealer_details_fixture(&deps.api, i); + dealer_details.address = deps.api.addr_make(&format!("dummy-dealer-{i}")); + fixtures.push(dealer_details); + } - // query without explicitly setting a limit - let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap(); + // initially empty for all epochs + for epoch_id in 0..10 { + let page1 = + query_epoch_dealers_addresses_paged(deps.as_ref(), epoch_id, None, None).unwrap(); + assert_eq!(0, page1.dealers.len() as u32); + } - assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32); + // epoch0: dealers 0,1,2,3 + // epoch1: dealers 4,5,6 + // epoch2: dealers: 1,4,6 (some overlap) + // epoch3: dealer 7 + // epoch4: dealers 0..100 (to check limits) + insert_dealer(deps.as_mut(), 0, &fixtures[0]); + insert_dealer(deps.as_mut(), 0, &fixtures[1]); + insert_dealer(deps.as_mut(), 0, &fixtures[2]); + insert_dealer(deps.as_mut(), 0, &fixtures[3]); - remove_dealers(&mut deps, 0, 1000); - } + insert_dealer(deps.as_mut(), 1, &fixtures[4]); + insert_dealer(deps.as_mut(), 1, &fixtures[5]); + insert_dealer(deps.as_mut(), 1, &fixtures[6]); - #[test] - fn dealers_paged_retrieval_has_max_limit() { - let mut deps = init_contract(); + insert_dealer(deps.as_mut(), 2, &fixtures[1]); + insert_dealer(deps.as_mut(), 2, &fixtures[4]); + insert_dealer(deps.as_mut(), 2, &fixtures[6]); - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT; + insert_dealer(deps.as_mut(), 3, &fixtures[7]); - fill_dealers(&mut deps, 0, 1000); + for fixture in &fixtures { + insert_dealer(deps.as_mut(), 4, fixture); + } - let page1 = - query_current_dealers_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); + let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 0, None, None).unwrap(); + assert_eq!(4, res.dealers.len() as u32); + for fixture in &fixtures[0..=3] { + assert!(res.dealers.contains(&fixture.address)) + } - // we default to a decent sized upper bound instead - let expected_limit = DEALERS_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.dealers.len() as u32); + let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 1, None, None).unwrap(); + assert_eq!(3, res.dealers.len() as u32); + for fixture in &fixtures[4..=6] { + assert!(res.dealers.contains(&fixture.address)) + } - remove_dealers(&mut deps, 0, 1000); - } + let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 2, None, None).unwrap(); + assert_eq!(3, res.dealers.len() as u32); + for fixture in &[ + fixtures[1].clone(), + fixtures[4].clone(), + fixtures[6].clone(), + ] { + assert!(res.dealers.contains(&fixture.address)) + } - #[test] - fn dealers_pagination_works() { - let mut deps = init_contract(); + let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 3, None, None).unwrap(); + assert_eq!(vec![fixtures[7].address.clone()], res.dealers); - let per_page = 2; + let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 4, None, None).unwrap(); + assert_eq!( + storage::DEALERS_ADDRESSES_PAGE_DEFAULT_LIMIT, + res.dealers.len() as u32 + ); - fill_dealers(&mut deps, 0, 1); - let page1 = - query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.dealers.len()); - remove_dealers(&mut deps, 0, 1); - - fill_dealers(&mut deps, 0, 2); - // page1 should have 2 results on it - let page1 = - query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.dealers.len()); - remove_dealers(&mut deps, 0, 2); - - fill_dealers(&mut deps, 0, 3); - // page1 still has 2 results - let page1 = - query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.dealers.len()); - - // retrieving the next page should start after the last key on this page - let start_after = page1.start_next_after.unwrap(); - let page2 = query_current_dealers_paged( - deps.as_ref(), - Option::from(start_after.to_string()), - Option::from(per_page), - ) - .unwrap(); - - assert_eq!(1, page2.dealers.len()); - remove_dealers(&mut deps, 0, 3); - - fill_dealers(&mut deps, 0, 4); - let page1 = - query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - let start_after = page1.start_next_after.unwrap(); - let page2 = query_current_dealers_paged( - deps.as_ref(), - Option::from(start_after.to_string()), - Option::from(per_page), - ) - .unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.dealers.len()); - remove_dealers(&mut deps, 0, 4); + let res = + query_epoch_dealers_addresses_paged(deps.as_ref(), 4, None, Some(1000000)).unwrap(); + assert_eq!( + storage::DEALERS_ADDRESSES_PAGE_MAX_LIMIT, + res.dealers.len() as u32 + ); } } diff --git a/contracts/coconut-dkg/src/dealers/storage.rs b/contracts/coconut-dkg/src/dealers/storage.rs index e713940175..e3edcc95a7 100644 --- a/contracts/coconut-dkg/src/dealers/storage.rs +++ b/contracts/coconut-dkg/src/dealers/storage.rs @@ -13,6 +13,9 @@ pub(crate) const DEALER_INDICES_PAGE_DEFAULT_LIMIT: u32 = 40; pub(crate) const DEALERS_PAGE_MAX_LIMIT: u32 = 25; pub(crate) const DEALERS_PAGE_DEFAULT_LIMIT: u32 = 10; +pub(crate) const DEALERS_ADDRESSES_PAGE_MAX_LIMIT: u32 = 50; +pub(crate) const DEALERS_ADDRESSES_PAGE_DEFAULT_LIMIT: u32 = 25; + pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); pub(crate) const DEALERS_INDICES: Map = Map::new("dealer_index"); diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index a7fee91108..ccb6d57c29 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -4,12 +4,12 @@ use crate::dealers::storage::{ get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer, }; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::{load_current_epoch, save_epoch}; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::storage::STATE; use crate::Dealer; -use cosmwasm_std::{Deps, DepsMut, MessageInfo, Response, StdResult}; +use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response}; use nym_coconut_dkg_common::dealer::DealerRegistrationDetails; use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; @@ -28,13 +28,14 @@ fn ensure_group_member(deps: Deps, dealer: Dealer) -> Result<(), ContractError> // for a recurring dealer just let it refresh the keys without having to do all the storage operations pub fn try_add_dealer( deps: DepsMut<'_>, + env: Env, info: MessageInfo, bte_key_with_proof: EncodedBTEPublicKeyWithProof, identity_key: String, announce_address: String, resharing: bool, ) -> Result { - let epoch = CURRENT_EPOCH.load(deps.storage)?; + let epoch = load_current_epoch(deps.storage)?; check_epoch_state(deps.storage, EpochState::PublicKeySubmission { resharing })?; // make sure this potential dealer actually belong to the group @@ -68,16 +69,16 @@ pub fn try_add_dealer( ); // increment the number of registered dealers - CURRENT_EPOCH.update(deps.storage, |epoch| -> StdResult<_> { - let mut updated_epoch = epoch; + { + let current_epoch = load_current_epoch(deps.storage)?; + let mut updated_epoch = current_epoch; updated_epoch.state_progress.registered_dealers += 1; if is_resharing_dealer { updated_epoch.state_progress.registered_resharing_dealers += 1; } - - Ok(updated_epoch) - })?; + save_epoch(deps.storage, env.block.height, &updated_epoch)?; + } Ok(Response::new().add_attribute("node_index", node_index.to_string())) } @@ -115,10 +116,11 @@ pub(crate) mod tests { .plus_seconds(TimeConfiguration::default().public_key_submission_time_secs); add_fixture_dealer(deps.as_mut()); - try_advance_epoch_state(deps.as_mut(), env).unwrap(); + try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let ret = try_add_dealer( deps.as_mut(), + env, info, bte_key_with_proof, identity, diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index 73107c352f..186754c3a8 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -5,7 +5,7 @@ use crate::dealers::storage::ensure_dealer; use crate::dealings::storage::{ metadata_exists, must_read_metadata, store_metadata, StoredDealing, }; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::{load_current_epoch, save_epoch}; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::storage::STATE; @@ -42,7 +42,7 @@ pub fn try_submit_dealings_metadata( chunks: Vec, resharing: bool, ) -> Result { - let epoch = CURRENT_EPOCH.load(deps.storage)?; + let epoch = load_current_epoch(deps.storage)?; let state = STATE.load(deps.storage)?; ensure_permission(deps.storage, &info.sender, epoch.epoch_id, resharing)?; @@ -137,7 +137,7 @@ pub fn try_commit_dealings_chunk( // note: checking permissions is implicit as if the metadata exists, // the sender must have been allowed to submit it - let mut epoch = CURRENT_EPOCH.load(deps.storage)?; + let mut epoch = load_current_epoch(deps.storage)?; // read meta let mut metadata = must_read_metadata( @@ -197,7 +197,7 @@ pub fn try_commit_dealings_chunk( // there won't be a lot of them if metadata.is_complete() { epoch.state_progress.submitted_dealings += 1; - CURRENT_EPOCH.save(deps.storage, &epoch)?; + save_epoch(deps.storage, env.block.height, &epoch)?; } Ok(Response::new()) @@ -309,12 +309,10 @@ pub(crate) mod tests { ); // same index, but next epoch - CURRENT_EPOCH - .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { - epoch.epoch_id += 1; - Ok(epoch) - }) - .unwrap(); + let mut epoch = load_current_epoch(&deps.storage).unwrap(); + epoch.epoch_id += 1; + save_epoch(deps.as_mut().storage, epoch.epoch_id, &epoch).unwrap(); + re_register_dealer(deps.as_mut(), &info.sender); try_submit_dealings_metadata( diff --git a/contracts/coconut-dkg/src/epoch_state/queries.rs b/contracts/coconut-dkg/src/epoch_state/queries.rs index 15ba780aec..f107d2e59c 100644 --- a/contracts/coconut-dkg/src/epoch_state/queries.rs +++ b/contracts/coconut-dkg/src/epoch_state/queries.rs @@ -1,17 +1,19 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; +use crate::epoch_state::storage::{ + load_current_epoch, EPOCH_THRESHOLDS, HISTORICAL_EPOCH, THRESHOLD, +}; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; -use cosmwasm_std::{Env, Storage}; +use cosmwasm_std::{Env, StdResult, Storage}; use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, StateAdvanceResponse}; pub(crate) fn query_can_advance_state( storage: &dyn Storage, env: Env, ) -> Result { - let epoch = CURRENT_EPOCH.load(storage)?; + let epoch = load_current_epoch(storage)?; if epoch.state == EpochState::WaitingInitialisation { return Ok(StateAdvanceResponse::default()); @@ -34,9 +36,14 @@ pub(crate) fn query_can_advance_state( } pub(crate) fn query_current_epoch(storage: &dyn Storage) -> Result { - CURRENT_EPOCH - .load(storage) - .map_err(|_| ContractError::EpochNotInitialised) + load_current_epoch(storage).map_err(|_| ContractError::EpochNotInitialised) +} + +pub(crate) fn query_epoch_at_height( + storage: &dyn Storage, + height: u64, +) -> StdResult> { + HISTORICAL_EPOCH.may_load_at_height(storage, height) } pub(crate) fn query_current_epoch_threshold( diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs index faed18e431..4318c8cf7b 100644 --- a/contracts/coconut-dkg/src/epoch_state/storage.rs +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -1,11 +1,225 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::{StdResult, Storage}; +use cw_storage_plus::{Item, Map, SnapshotItem, Strategy}; use nym_coconut_dkg_common::types::{Epoch, EpochId}; +#[deprecated] +// leave old values in storage for backwards compatibility, but make sure everything in the contract +// uses the new reference pub(crate) const CURRENT_EPOCH: Item = Item::new("current_epoch"); +pub const HISTORICAL_EPOCH: SnapshotItem = SnapshotItem::new( + "historical_epoch", + "historical_epoch__checkpoints", + "historical_epoch__changelog", + Strategy::EveryBlock, +); pub const THRESHOLD: Item = Item::new("threshold"); pub const EPOCH_THRESHOLDS: Map = Map::new("epoch_thresholds"); + +#[allow(deprecated)] +pub fn save_epoch(storage: &mut dyn Storage, height: u64, epoch: &Epoch) -> StdResult<()> { + CURRENT_EPOCH.save(storage, epoch)?; + HISTORICAL_EPOCH.save(storage, epoch, height) +} + +#[allow(deprecated)] +pub fn load_current_epoch(storage: &dyn Storage) -> StdResult { + #[cfg(debug_assertions)] + { + let current = CURRENT_EPOCH.load(storage); + let historical = HISTORICAL_EPOCH.load(storage); + debug_assert_eq!(current, historical); + } + HISTORICAL_EPOCH.load(storage) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::epoch_state::transactions::{try_advance_epoch_state, try_initiate_dkg}; + use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS}; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::{Addr, Env}; + use nym_coconut_dkg_common::types::EpochState; + use std::ops::{Deref, DerefMut}; + + #[test] + fn full_dkg_correctly_updates_historical_epoch() -> anyhow::Result<()> { + struct EnvWrapper { + env: Env, + } + + impl EnvWrapper { + fn next_block(&mut self) { + self.env.block.height += 1; + self.env.block.time = self.env.block.time.plus_seconds(5); + } + + fn height(&self) -> u64 { + self.block.height + } + } + + impl Deref for EnvWrapper { + type Target = Env; + fn deref(&self) -> &Self::Target { + &self.env + } + } + + impl DerefMut for EnvWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.env + } + } + + let mut empty_deps = mock_dependencies(); + + // before contract is initialised, there's nothing saved + assert!(HISTORICAL_EPOCH + .may_load(empty_deps.as_mut().storage)? + .is_none()); + + let mut deps = init_contract(); + let mut env = EnvWrapper { env: mock_env() }; + + let init_height = env.height(); + // after init it has initial state + assert_eq!(HISTORICAL_EPOCH.load(deps.as_mut().storage)?.epoch_id, 0); + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::WaitingInitialisation + ); + + env.next_block(); + let pub_key_submission_height = env.height(); + try_initiate_dkg( + deps.as_mut(), + (*env).clone(), + message_info(&Addr::unchecked(ADMIN_ADDRESS), &[]), + )?; + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::PublicKeySubmission { resharing: false } + ); + + env.block.time = env.block.time.plus_seconds(100000); + env.next_block(); + let dealing_exchange_height = env.height(); + try_advance_epoch_state(deps.as_mut(), (*env).clone())?; + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::DealingExchange { resharing: false } + ); + + env.block.time = env.block.time.plus_seconds(100000); + env.next_block(); + let verification_key_submission_height = env.height(); + try_advance_epoch_state(deps.as_mut(), (*env).clone())?; + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::VerificationKeySubmission { resharing: false } + ); + + env.block.time = env.block.time.plus_seconds(100000); + env.next_block(); + let verification_key_validation_height = env.height(); + try_advance_epoch_state(deps.as_mut(), (*env).clone())?; + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::VerificationKeyValidation { resharing: false } + ); + + env.block.time = env.block.time.plus_seconds(100000); + env.next_block(); + let verification_key_finalization_height = env.height(); + try_advance_epoch_state(deps.as_mut(), (*env).clone())?; + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::VerificationKeyFinalization { resharing: false } + ); + + env.block.time = env.block.time.plus_seconds(100000); + env.next_block(); + let in_progress_height = env.height(); + try_advance_epoch_state(deps.as_mut(), (*env).clone())?; + assert_eq!( + HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state, + EpochState::InProgress {} + ); + + // check old data + assert!(HISTORICAL_EPOCH + .may_load_at_height(deps.as_mut().storage, init_height - 1)? + .is_none()); + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height(deps.as_mut().storage, init_height + 1)? + .unwrap() + .state, + EpochState::WaitingInitialisation + ); + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height(deps.as_mut().storage, pub_key_submission_height + 1)? + .unwrap() + .state, + EpochState::PublicKeySubmission { resharing: false } + ); + + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height(deps.as_mut().storage, dealing_exchange_height + 1)? + .unwrap() + .state, + EpochState::DealingExchange { resharing: false } + ); + + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height( + deps.as_mut().storage, + verification_key_submission_height + 1 + )? + .unwrap() + .state, + EpochState::VerificationKeySubmission { resharing: false } + ); + + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height( + deps.as_mut().storage, + verification_key_validation_height + 1 + )? + .unwrap() + .state, + EpochState::VerificationKeyValidation { resharing: false } + ); + + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height( + deps.as_mut().storage, + verification_key_finalization_height + 1 + )? + .unwrap() + .state, + EpochState::VerificationKeyFinalization { resharing: false } + ); + + assert_eq!( + HISTORICAL_EPOCH + .may_load_at_height(deps.as_mut().storage, in_progress_height + 1)? + .unwrap() + .state, + EpochState::InProgress + ); + + Ok(()) + } +} diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs index cfabdc700d..156459f24c 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD}; +use crate::epoch_state::storage::{load_current_epoch, save_epoch, EPOCH_THRESHOLDS, THRESHOLD}; use crate::epoch_state::transactions::reset_dkg_state; use crate::epoch_state::utils::check_state_completion; use crate::error::ContractError; @@ -39,7 +39,7 @@ fn ensure_can_advance_state( pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result { // TODO: the only case where this can retrigger itself is when insufficient number of parties completed it, i.e. we don't have threshold - let current_epoch = CURRENT_EPOCH.load(deps.storage)?; + let current_epoch = load_current_epoch(deps.storage)?; // checks whether the given phase has either completed or reached its deadline ensure_can_advance_state(deps.as_ref(), &env, ¤t_epoch)?; @@ -82,7 +82,7 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result, env: Env) -> Result(storage: &mut dyn Storage, env: &Env, action: A) + where + A: Fn(Epoch) -> Epoch, + { + let current = load_current_epoch(storage).unwrap(); + let updated = action(current); + save_epoch(storage, env.block.height, &updated).unwrap(); + } + #[test] fn short_circuit_advance_state() { fn epoch_in_state(state: EpochState, env: &Env) -> Epoch { Epoch::new(state, 0, Default::default(), env.block.time) } - fn set_epoch(storage: &mut dyn Storage, epoch: Epoch) { - CURRENT_EPOCH.save(storage, &epoch).unwrap(); + fn set_epoch(storage: &mut dyn Storage, env: &Env, epoch: Epoch) { + save_epoch(storage, env.block.height, &epoch).unwrap(); } let mut deps = init_contract(); @@ -114,18 +124,18 @@ mod tests { // it's never possible to short-circuit `WaitingInitialisation` let epoch = epoch_in_state(EpochState::WaitingInitialisation, &env); - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); // neither PublicKeySubmission (in either resharing or non-resharing) let epoch = epoch_in_state(EpochState::PublicKeySubmission { resharing: false }, &env); - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); let epoch = epoch_in_state(EpochState::PublicKeySubmission { resharing: true }, &env); - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -138,7 +148,7 @@ mod tests { // no dealings let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: false }, &env); epoch.state_progress.registered_dealers = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -146,7 +156,7 @@ mod tests { let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: false }, &env); epoch.state_progress.registered_dealers = 5; epoch.state_progress.submitted_dealings = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -154,7 +164,7 @@ mod tests { let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: false }, &env); epoch.state_progress.registered_dealers = 5; epoch.state_progress.submitted_dealings = key_size * 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_ok()); check_epoch_state( @@ -167,7 +177,7 @@ mod tests { let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: true }, &env); epoch.state_progress.registered_dealers = 5; epoch.state_progress.registered_resharing_dealers = 4; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -176,7 +186,7 @@ mod tests { epoch.state_progress.registered_dealers = 5; epoch.state_progress.registered_resharing_dealers = 4; epoch.state_progress.submitted_dealings = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -185,7 +195,7 @@ mod tests { epoch.state_progress.registered_dealers = 5; epoch.state_progress.registered_resharing_dealers = 4; epoch.state_progress.submitted_dealings = key_size * 4; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_ok()); check_epoch_state( @@ -200,7 +210,7 @@ mod tests { &env, ); epoch.state_progress.registered_dealers = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -209,7 +219,7 @@ mod tests { &env, ); epoch.state_progress.registered_dealers = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -219,7 +229,7 @@ mod tests { ); epoch.state_progress.registered_dealers = 5; epoch.state_progress.submitted_key_shares = 4; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -229,7 +239,7 @@ mod tests { ); epoch.state_progress.registered_dealers = 5; epoch.state_progress.submitted_key_shares = 4; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -239,7 +249,7 @@ mod tests { ); epoch.state_progress.registered_dealers = 5; epoch.state_progress.submitted_key_shares = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_ok()); check_epoch_state( @@ -254,7 +264,7 @@ mod tests { ); epoch.state_progress.registered_dealers = 5; epoch.state_progress.submitted_key_shares = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_ok()); check_epoch_state( @@ -268,7 +278,7 @@ mod tests { EpochState::VerificationKeyValidation { resharing: false }, &env, ); - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -276,7 +286,7 @@ mod tests { EpochState::VerificationKeyValidation { resharing: true }, &env, ); - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -286,7 +296,7 @@ mod tests { &env, ); epoch.state_progress.submitted_key_shares = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -295,7 +305,7 @@ mod tests { &env, ); epoch.state_progress.submitted_key_shares = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -305,7 +315,7 @@ mod tests { ); epoch.state_progress.submitted_key_shares = 5; epoch.state_progress.verified_keys = 4; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -315,7 +325,7 @@ mod tests { ); epoch.state_progress.submitted_key_shares = 5; epoch.state_progress.verified_keys = 4; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); @@ -325,7 +335,7 @@ mod tests { ); epoch.state_progress.submitted_key_shares = 5; epoch.state_progress.verified_keys = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_ok()); check_epoch_state(deps.as_ref().storage, EpochState::InProgress).unwrap(); @@ -336,14 +346,14 @@ mod tests { ); epoch.state_progress.submitted_key_shares = 5; epoch.state_progress.verified_keys = 5; - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_ok()); check_epoch_state(deps.as_ref().storage, EpochState::InProgress).unwrap(); // it's never possible to short-circuit `InProgress` let epoch = epoch_in_state(EpochState::InProgress, &env); - set_epoch(deps.as_mut().storage, epoch); + set_epoch(deps.as_mut().storage, &env, epoch); let res = try_advance_epoch_state(deps.as_mut(), env.clone()); assert!(res.is_err()); } @@ -366,7 +376,7 @@ mod tests { ) .unwrap(); - let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let epoch = load_current_epoch(deps.as_mut().storage).unwrap(); assert_eq!( epoch.state, EpochState::PublicKeySubmission { resharing: false } @@ -390,19 +400,16 @@ mod tests { env.block.time = env.block.time.plus_seconds(1); // add some dealers to prevent short-circuiting - CURRENT_EPOCH - .update(deps.as_mut().storage, |mut e| -> StdResult<_> { - e.state_progress.registered_dealers = 42; - Ok(e) - }) - .unwrap(); - + update_epoch(deps.as_mut().storage, &env, |mut e| { + e.state_progress.registered_dealers = 42; + e + }); env.block.time = env .block .time .plus_seconds(epoch.time_configuration.public_key_submission_time_secs); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let epoch = load_current_epoch(deps.as_mut().storage).unwrap(); assert_eq!( epoch.state, EpochState::DealingExchange { resharing: false } @@ -425,7 +432,7 @@ mod tests { env.block.time = env.block.time.plus_seconds(3); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let epoch = load_current_epoch(deps.as_mut().storage).unwrap(); assert_eq!( epoch.state, EpochState::VerificationKeySubmission { resharing: false } @@ -452,7 +459,7 @@ mod tests { env.block.time = env.block.time.plus_seconds(3); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let epoch = load_current_epoch(deps.as_mut().storage).unwrap(); assert_eq!( epoch.state, EpochState::VerificationKeyValidation { resharing: false } @@ -478,16 +485,13 @@ mod tests { ); // add some key shares to prevent short-circuiting - CURRENT_EPOCH - .update(deps.as_mut().storage, |mut e| -> StdResult<_> { - e.state_progress.submitted_key_shares = 42; - Ok(e) - }) - .unwrap(); - + update_epoch(deps.as_mut().storage, &env, |mut e| { + e.state_progress.submitted_key_shares = 42; + e + }); env.block.time = env.block.time.plus_seconds(3); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let epoch = load_current_epoch(deps.as_mut().storage).unwrap(); assert_eq!( epoch.state, EpochState::VerificationKeyFinalization { resharing: false } @@ -512,16 +516,14 @@ mod tests { ); // add some finalized keys to prevent reset - CURRENT_EPOCH - .update(deps.as_mut().storage, |mut e| -> StdResult<_> { - e.state_progress.verified_keys = 42; - Ok(e) - }) - .unwrap(); + update_epoch(deps.as_mut().storage, &env, |mut e| { + e.state_progress.verified_keys = 42; + e + }); env.block.time = env.block.time.plus_seconds(1); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let epoch = load_current_epoch(deps.as_mut().storage).unwrap(); assert_eq!(epoch.state, EpochState::InProgress); assert_eq!( epoch.deadline.unwrap(), @@ -547,9 +549,9 @@ mod tests { // Group hasn't changed, so we remain in the same epoch, with updated finish timestamp env.block.time = env.block.time.plus_seconds(100); - let prev_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let prev_epoch = load_current_epoch(deps.as_mut().storage).unwrap(); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let curr_epoch = load_current_epoch(deps.as_mut().storage).unwrap(); let mut expected_epoch = Epoch::new( EpochState::InProgress, prev_epoch.epoch_id, @@ -570,11 +572,11 @@ mod tests { // fewer than the threshold epoch.state_progress.verified_keys = 41; - CURRENT_EPOCH.save(deps.as_mut().storage, &epoch).unwrap(); + save_epoch(deps.as_mut().storage, env.block.height, &epoch).unwrap(); env.block.time = env.block.time.plus_seconds(5000000); try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let curr_epoch = load_current_epoch(deps.as_mut().storage).unwrap(); let expected_epoch = Epoch::new( EpochState::PublicKeySubmission { resharing: false }, epoch.epoch_id + 1, @@ -598,12 +600,10 @@ mod tests { assert!(THRESHOLD.may_load(deps.as_mut().storage).unwrap().is_none()); - CURRENT_EPOCH - .update(deps.as_mut().storage, |mut e| -> StdResult<_> { - e.state_progress.registered_dealers = 100; - Ok(e) - }) - .unwrap(); + update_epoch(deps.as_mut().storage, &env, |mut e| { + e.state_progress.registered_dealers = 100; + e + }); env.block.time = env .block diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/mod.rs b/contracts/coconut-dkg/src/epoch_state/transactions/mod.rs index 73d548cdeb..e206fcb379 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/mod.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{load_current_epoch, save_epoch, THRESHOLD}; use crate::error::ContractError; use crate::state::storage::DKG_ADMIN; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage}; @@ -29,7 +29,7 @@ pub(crate) fn try_initiate_dkg( // only the admin is allowed to kick start the process DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let epoch = CURRENT_EPOCH.load(deps.storage)?; + let epoch = load_current_epoch(deps.storage)?; if !matches!(epoch.state, EpochState::WaitingInitialisation) { return Err(ContractError::AlreadyInitialised); } @@ -37,7 +37,7 @@ pub(crate) fn try_initiate_dkg( // the first exchange won't involve resharing let initial_state = EpochState::PublicKeySubmission { resharing: false }; let initial_epoch = Epoch::new(initial_state, 0, epoch.time_configuration, env.block.time); - CURRENT_EPOCH.save(deps.storage, &initial_epoch)?; + save_epoch(deps.storage, env.block.height, &initial_epoch)?; Ok(Response::default()) } @@ -49,7 +49,7 @@ pub(crate) fn try_trigger_reset( ) -> Result { // only the admin is allowed to trigger DKG reset DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let current_epoch = CURRENT_EPOCH.load(deps.storage)?; + let current_epoch = load_current_epoch(deps.storage)?; // only allow reset when the DKG exchange isn't in progress if !current_epoch.state.is_in_progress() { @@ -57,7 +57,7 @@ pub(crate) fn try_trigger_reset( } let next_epoch = current_epoch.next_reset(env.block.time); - CURRENT_EPOCH.save(deps.storage, &next_epoch)?; + save_epoch(deps.storage, env.block.height, &next_epoch)?; reset_dkg_state(deps.storage)?; @@ -71,7 +71,7 @@ pub(crate) fn try_trigger_resharing( ) -> Result { // only the admin is allowed to trigger DKG resharing DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let current_epoch = CURRENT_EPOCH.load(deps.storage)?; + let current_epoch = load_current_epoch(deps.storage)?; // only allow resharing when the DKG exchange isn't in progress if !current_epoch.state.is_in_progress() { @@ -79,7 +79,7 @@ pub(crate) fn try_trigger_resharing( } let next_epoch = current_epoch.next_resharing(env.block.time); - CURRENT_EPOCH.save(deps.storage, &next_epoch)?; + save_epoch(deps.storage, env.block.height, &next_epoch)?; reset_dkg_state(deps.storage)?; @@ -89,6 +89,7 @@ pub(crate) fn try_trigger_resharing( #[cfg(test)] pub(crate) mod tests { use super::*; + use crate::epoch_state::storage::load_current_epoch; use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS}; use cosmwasm_std::testing::{message_info, mock_env}; use cosmwasm_std::Addr; @@ -99,7 +100,7 @@ pub(crate) mod tests { let mut deps = init_contract(); let env = mock_env(); - let initial_epoch_info = CURRENT_EPOCH.load(&deps.storage).unwrap(); + let initial_epoch_info = load_current_epoch(&deps.storage).unwrap(); assert!(initial_epoch_info.deadline.is_none()); let not_admin = deps.api.addr_make("not an admin"); @@ -125,7 +126,7 @@ pub(crate) mod tests { assert_eq!(ContractError::AlreadyInitialised, res); // sets the correct epoch data - let epoch = CURRENT_EPOCH.load(&deps.storage).unwrap(); + let epoch = load_current_epoch(&deps.storage).unwrap(); assert_eq!(epoch.epoch_id, 0); assert_eq!( epoch.state, diff --git a/contracts/coconut-dkg/src/epoch_state/utils.rs b/contracts/coconut-dkg/src/epoch_state/utils.rs index c468e2af8b..31ce9def13 100644 --- a/contracts/coconut-dkg/src/epoch_state/utils.rs +++ b/contracts/coconut-dkg/src/epoch_state/utils.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::load_current_epoch; use crate::error::ContractError; use crate::state::storage::STATE; use cosmwasm_std::Storage; @@ -52,7 +52,7 @@ pub(crate) fn check_epoch_state( storage: &dyn Storage, against: EpochState, ) -> Result<(), ContractError> { - let epoch_state = CURRENT_EPOCH.load(storage)?.state; + let epoch_state = load_current_epoch(storage)?.state; if epoch_state != against { Err(ContractError::IncorrectEpochState { current_state: epoch_state.to_string(), @@ -66,6 +66,7 @@ pub(crate) fn check_epoch_state( #[cfg(test)] pub(crate) mod test { use super::*; + use crate::epoch_state::storage::save_epoch; use crate::support::tests::helpers::init_contract; use cosmwasm_std::testing::mock_env; use cosmwasm_std::Timestamp; @@ -210,12 +211,12 @@ pub(crate) mod test { let env = mock_env(); for fixed_state in EpochState::first().all_until(EpochState::InProgress) { - CURRENT_EPOCH - .save( - deps.as_mut().storage, - &Epoch::new(fixed_state, 0, TimeConfiguration::default(), env.block.time), - ) - .unwrap(); + save_epoch( + deps.as_mut().storage, + env.block.height, + &Epoch::new(fixed_state, 0, TimeConfiguration::default(), env.block.time), + ) + .unwrap(); for against_state in EpochState::first().all_until(EpochState::InProgress) { let ret = check_epoch_state(deps.as_mut().storage, against_state); if fixed_state == against_state { diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index c867fec2be..45f626fe87 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -10,6 +10,9 @@ use thiserror::Error; /// Custom errors for contract failure conditions. #[derive(Error, Debug, PartialEq)] pub enum ContractError { + #[error("could not perform contract migration: {comment}")] + FailedMigration { comment: String }, + #[error(transparent)] Std(#[from] StdError), diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index ca3cf4898a..729ba3cede 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -11,6 +11,7 @@ mod dealers; mod dealings; mod epoch_state; pub mod error; +mod queued_migrations; mod state; mod support; mod verification_key_shares; diff --git a/contracts/coconut-dkg/src/queued_migrations.rs b/contracts/coconut-dkg/src/queued_migrations.rs new file mode 100644 index 0000000000..54c149a518 --- /dev/null +++ b/contracts/coconut-dkg/src/queued_migrations.rs @@ -0,0 +1,21 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::epoch_state::storage::HISTORICAL_EPOCH; +use crate::error::ContractError; +use cosmwasm_std::{DepsMut, Env}; + +pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> { + if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() { + return Err(ContractError::FailedMigration { + comment: "this migration has already been run before".to_string(), + }); + } + + #[allow(deprecated)] + let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?; + // we won't have information on intermediate states prior to now, but that's not the end of the world + HISTORICAL_EPOCH.save(deps.storage, ¤t, env.block.height)?; + + Ok(()) +} diff --git a/contracts/coconut-dkg/src/support/tests/fixtures.rs b/contracts/coconut-dkg/src/support/tests/fixtures.rs index 04dde90358..87a59af53b 100644 --- a/contracts/coconut-dkg/src/support/tests/fixtures.rs +++ b/contracts/coconut-dkg/src/support/tests/fixtures.rs @@ -12,8 +12,8 @@ pub const TEST_MIX_DENOM: &str = "unym"; pub fn vk_share_fixture(owner: &str, index: u64) -> ContractVKShare { ContractVKShare { - share: format!("share{}", index), - announce_address: format!("localhost:{}", index), + share: format!("share{index}"), + announce_address: format!("localhost:{index}"), node_index: index, owner: Addr::unchecked(owner), epoch_id: index, @@ -43,7 +43,7 @@ pub fn dealing_metadata_fixture() -> Vec { pub fn dealer_details_fixture(api: &MockApi, assigned_index: u64) -> DealerDetails { DealerDetails { - address: api.addr_make(&format!("owner{}", assigned_index)), + address: api.addr_make(&format!("owner{assigned_index}")), bte_public_key_with_proof: "".to_string(), ed25519_identity: "".to_string(), announce_address: "".to_string(), diff --git a/contracts/coconut-dkg/src/support/tests/helpers.rs b/contracts/coconut-dkg/src/support/tests/helpers.rs index d567888577..5ef8eea22f 100644 --- a/contracts/coconut-dkg/src/support/tests/helpers.rs +++ b/contracts/coconut-dkg/src/support/tests/helpers.rs @@ -3,7 +3,7 @@ use crate::contract::instantiate; use crate::dealers::storage::{DEALERS_INDICES, EPOCH_DEALERS_MAP}; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::load_current_epoch; use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env, MockApi, MockQuerier}; use cosmwasm_std::{ from_json, to_json_binary, Addr, ContractResult, DepsMut, Empty, MemoryStorage, OwnedDeps, @@ -27,7 +27,7 @@ pub const MULTISIG_CONTRACT: &str = addr!("multisig contract address"); pub(crate) static GROUP_MEMBERS: Mutex> = Mutex::new(Vec::new()); pub fn re_register_dealer(deps: DepsMut, dealer: &Addr) { - let epoch_id = CURRENT_EPOCH.load(deps.storage).unwrap().epoch_id; + let epoch_id = load_current_epoch(deps.storage).unwrap().epoch_id; let previous = epoch_id - 1; let details = EPOCH_DEALERS_MAP .load(deps.storage, (previous, dealer)) @@ -38,7 +38,7 @@ pub fn re_register_dealer(deps: DepsMut, dealer: &Addr) { } pub fn add_current_dealer(deps: DepsMut<'_>, details: &DealerDetails) { - let epoch_id = CURRENT_EPOCH.load(deps.storage).unwrap().epoch_id; + let epoch_id = load_current_epoch(deps.storage).unwrap().epoch_id; insert_dealer(deps, epoch_id, details) } diff --git a/contracts/coconut-dkg/src/verification_key_shares/queries.rs b/contracts/coconut-dkg/src/verification_key_shares/queries.rs index e0254eebdd..f337f07a22 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/queries.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/queries.rs @@ -99,7 +99,7 @@ pub(crate) mod tests { let mut deps = init_contract(); let limit = 2; for n in 0..1000 { - let owner = format!("owner{}", n); + let owner = format!("owner{n}"); let vk_share = vk_share_fixture(&owner, 0); let sender = Addr::unchecked(owner); vk_shares() @@ -115,7 +115,7 @@ pub(crate) mod tests { fn vk_shares_paged_retrieval_has_default_limit() { let mut deps = init_contract(); for n in 0..1000 { - let owner = format!("owner{}", n); + let owner = format!("owner{n}"); let vk_share = vk_share_fixture(&owner, 0); let sender = Addr::unchecked(owner); vk_shares() @@ -136,7 +136,7 @@ pub(crate) mod tests { fn vk_shares_paged_retrieval_has_max_limit() { let mut deps = init_contract(); for n in 0..1000 { - let owner = format!("owner{}", n); + let owner = format!("owner{n}"); let vk_share = vk_share_fixture(&owner, 0); let sender = Addr::unchecked(owner); vk_shares() diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs index cff0c88232..5c01b12b1a 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -3,7 +3,7 @@ use crate::constants::BLOCK_TIME_FOR_VERIFICATION_SECS; use crate::dealers::storage::get_dealer_details; -use crate::epoch_state::storage::CURRENT_EPOCH; +use crate::epoch_state::storage::{load_current_epoch, save_epoch}; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::storage::{MULTISIG, STATE}; @@ -25,7 +25,7 @@ pub fn try_commit_verification_key_share( deps.storage, EpochState::VerificationKeySubmission { resharing }, )?; - let mut epoch = CURRENT_EPOCH.load(deps.storage)?; + let mut epoch = load_current_epoch(deps.storage)?; let epoch_id = epoch.epoch_id; let details = get_dealer_details(deps.storage, &info.sender, epoch_id)?; @@ -43,7 +43,7 @@ pub fn try_commit_verification_key_share( node_index: details.assigned_index, announce_address: details.announce_address, owner: info.sender.clone(), - epoch_id: CURRENT_EPOCH.load(deps.storage)?.epoch_id, + epoch_id: load_current_epoch(deps.storage)?.epoch_id, verified: false, }; vk_shares().save(deps.storage, (&info.sender, epoch_id), &data)?; @@ -60,13 +60,14 @@ pub fn try_commit_verification_key_share( )?; epoch.state_progress.submitted_key_shares += 1; - CURRENT_EPOCH.save(deps.storage, &epoch)?; + save_epoch(deps.storage, env.block.height, &epoch)?; Ok(Response::new().add_message(msg)) } pub fn try_verify_verification_key_share( deps: DepsMut<'_>, + env: Env, info: MessageInfo, owner: String, resharing: bool, @@ -77,7 +78,7 @@ pub fn try_verify_verification_key_share( deps.storage, EpochState::VerificationKeyFinalization { resharing }, )?; - let mut epoch = CURRENT_EPOCH.load(deps.storage)?; + let mut epoch = load_current_epoch(deps.storage)?; let epoch_id = epoch.epoch_id; MULTISIG.assert_admin(deps.as_ref(), &info.sender)?; @@ -93,7 +94,7 @@ pub fn try_verify_verification_key_share( })?; epoch.state_progress.verified_keys += 1; - CURRENT_EPOCH.save(deps.storage, &epoch)?; + save_epoch(deps.storage, env.block.height, &epoch)?; Ok(Response::default()) } @@ -259,9 +260,14 @@ mod tests { let owner = deps.api.addr_make("owner").to_string(); let multisig_info = message_info(&Addr::unchecked(MULTISIG_CONTRACT), &[]); - let ret = - try_verify_verification_key_share(deps.as_mut(), info.clone(), owner.clone(), false) - .unwrap_err(); + let ret = try_verify_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + owner.clone(), + false, + ) + .unwrap_err(); assert_eq!( ret, ContractError::IncorrectEpochState { @@ -291,15 +297,26 @@ mod tests { .block .time .plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs); - try_advance_epoch_state(deps.as_mut(), env).unwrap(); + try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - let ret = try_verify_verification_key_share(deps.as_mut(), info, owner.clone(), false) - .unwrap_err(); + let ret = try_verify_verification_key_share( + deps.as_mut(), + env.clone(), + info, + owner.clone(), + false, + ) + .unwrap_err(); assert_eq!(ret, ContractError::Admin(AdminError::NotAdmin {})); - let ret = - try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone(), false) - .unwrap_err(); + let ret = try_verify_verification_key_share( + deps.as_mut(), + env.clone(), + multisig_info, + owner.clone(), + false, + ) + .unwrap_err(); assert_eq!( ret, ContractError::NoCommitForOwner { @@ -356,9 +373,15 @@ mod tests { .block .time .plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs); - try_advance_epoch_state(deps.as_mut(), env).unwrap(); + try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.to_string(), false) - .unwrap(); + try_verify_verification_key_share( + deps.as_mut(), + env, + multisig_info, + owner.to_string(), + false, + ) + .unwrap(); } } diff --git a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs index 3b857d6dba..2df1a87e35 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs @@ -34,5 +34,6 @@ pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg { version_score_params: Default::default(), profit_margin: Default::default(), interval_operating_cost: Default::default(), + key_validity_in_epochs: None, } } diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 917ad82e88..ae2ad99d36 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -26,14 +26,13 @@ name = "mixnet_contract" crate-type = ["cdylib", "rlib"] [dependencies] -mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.6.0" } -vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" } -nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" } +mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" } +vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" } +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true } cosmwasm-schema = { workspace = true, optional = true } cosmwasm-std = { workspace = true } - -cosmwasm-derive = { workspace = true } cw-controllers = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } @@ -41,17 +40,24 @@ cw-storage-plus = { workspace = true } bs58 = { workspace = true } serde = { workspace = true, default-features = false, features = ["derive"] } semver = { workspace = true } -thiserror = { workspace = true } -time = { version = "0.3", features = ["macros"] } + [dev-dependencies] anyhow.workspace = true -rand_chacha = "0.3" -rand = "0.8.5" +rand_chacha = { workspace = true } +rand = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" } +# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick) +nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } + [features] default = [] contract-testing = ["mixnet-contract-common/contract-testing"] +testable-mixnet-contract = ["nym-contracts-common-testing"] schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"] + +[lints] +workspace = true \ No newline at end of file diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 3b80f44e3a..cf36563949 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -41,6 +41,15 @@ } ] }, + "key_validity_in_epochs": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "profit_margin": { "default": { "maximum": "1", @@ -3440,6 +3449,34 @@ } }, "additionalProperties": false + }, + { + "description": "Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)", + "type": "object", + "required": [ + "get_key_rotation_state" + ], + "properties": { + "get_key_rotation_state": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the current key rotation id", + "type": "object", + "required": [ + "get_key_rotation_id" + ], + "properties": { + "get_key_rotation_id": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -5116,6 +5153,46 @@ } } }, + "get_key_rotation_id": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "KeyRotationIdResponse", + "type": "object", + "required": [ + "rotation_id" + ], + "properties": { + "rotation_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "get_key_rotation_state": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "KeyRotationState", + "type": "object", + "required": [ + "initial_epoch_id", + "validity_epochs" + ], + "properties": { + "initial_epoch_id": { + "description": "Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts). It is used for determining when rotation is meant to advance.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "validity_epochs": { + "description": "Defines how long each key rotation is valid for (in terms of epochs)", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "get_mix_node_bonds": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PagedMixnodeBondsResponse", diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index 0eca5410be..b17e1ae6bf 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -37,6 +37,15 @@ } ] }, + "key_validity_in_epochs": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "profit_margin": { "default": { "maximum": "1", diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index fdf631ebce..6ec5fdf579 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -1486,6 +1486,34 @@ } }, "additionalProperties": false + }, + { + "description": "Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)", + "type": "object", + "required": [ + "get_key_rotation_state" + ], + "properties": { + "get_key_rotation_state": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the current key rotation id", + "type": "object", + "required": [ + "get_key_rotation_id" + ], + "properties": { + "get_key_rotation_id": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/mixnet/schema/raw/response_to_get_key_rotation_id.json b/contracts/mixnet/schema/raw/response_to_get_key_rotation_id.json new file mode 100644 index 0000000000..f0bdf2fbd1 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_key_rotation_id.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "KeyRotationIdResponse", + "type": "object", + "required": [ + "rotation_id" + ], + "properties": { + "rotation_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false +} diff --git a/contracts/mixnet/schema/raw/response_to_get_key_rotation_state.json b/contracts/mixnet/schema/raw/response_to_get_key_rotation_state.json new file mode 100644 index 0000000000..efd32685f8 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_key_rotation_state.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "KeyRotationState", + "type": "object", + "required": [ + "initial_epoch_id", + "validity_epochs" + ], + "properties": { + "initial_epoch_id": { + "description": "Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts). It is used for determining when rotation is meant to advance.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "validity_epochs": { + "description": "Defines how long each key rotation is valid for (in terms of epochs)", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false +} diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 50cf7c2541..2bcb2ba4ea 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -78,6 +78,7 @@ pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles"; pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata"; pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles"; +pub const KEY_ROTATION_STATE_KEY: &str = "key_rot_state"; pub const NODE_ID_COUNTER_KEY: &str = "nic"; pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc"; pub const MIXNODES_PK_NAMESPACE: &str = "mnn"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index db9e3f44f4..0703e6dc23 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -5,6 +5,7 @@ use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::nodes::storage as nymnodes_storage; +use crate::queued_migrations::introduce_key_rotation_id; use crate::rewards::storage::RewardingStorage; use cosmwasm_std::{ entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, @@ -82,6 +83,11 @@ pub fn instantiate( }); } + let key_rotation_validity = msg.key_validity_in_epochs(); + if key_rotation_validity < InstantiateMsg::MIN_KEY_ROTATION_VALIDITY { + return Err(MixnetContractError::TooShortRotationInterval); + } + let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?; let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?; let state = default_initial_state( @@ -109,7 +115,7 @@ pub fn instantiate( msg.current_nym_node_version, )?; RewardingStorage::new().initialise(deps.storage, reward_params)?; - nymnodes_storage::initialise_storage(deps.storage)?; + nymnodes_storage::initialise_storage(deps.storage, key_rotation_validity)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; set_build_information!(deps.storage)?; @@ -598,6 +604,14 @@ pub fn query( QueryMsg::GetSigningNonce { address } => to_json_binary( &crate::signing::queries::query_current_signing_nonce(deps, address)?, ), + + // sphinx key rotation-related + QueryMsg::GetKeyRotationState {} => { + to_json_binary(&crate::nodes::queries::query_key_rotation_state(deps)?) + } + QueryMsg::GetKeyRotationId {} => { + to_json_binary(&crate::nodes::queries::query_key_rotation_id(deps)?) + } }; Ok(query_res?) @@ -605,18 +619,18 @@ pub fn query( #[entry_point] pub fn migrate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false); - // - // if !skip_state_updates { - // - // } + let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false); + + if !skip_state_updates { + introduce_key_rotation_id(deps.branch())?; + } // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh @@ -635,7 +649,7 @@ pub fn migrate( mod tests { use super::*; use crate::rewards::storage as rewards_storage; - use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + use cosmwasm_std::testing::{message_info, mock_env}; use cosmwasm_std::{Decimal, Uint128}; use mixnet_contract_common::reward_params::{ IntervalRewardParams, RewardedSetParams, RewardingParams, @@ -643,6 +657,7 @@ mod tests { use mixnet_contract_common::{ InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange, }; + use nym_contracts_common_testing::mock_dependencies; use std::time::Duration; #[test] @@ -681,6 +696,7 @@ mod tests { minimum: "1000".parse().unwrap(), maximum: "10000".parse().unwrap(), }, + key_validity_in_epochs: None, }; let sender = message_info(&deps.api.addr_make("sender"), &[]); diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 8e1450421d..559f6e7ee9 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -22,3 +22,6 @@ mod support; #[cfg(feature = "contract-testing")] mod testing; mod vesting_migration; + +#[cfg(feature = "testable-mixnet-contract")] +pub mod testable_mixnet_contract; diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 0175e4a279..8d1202df71 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -130,20 +130,22 @@ pub mod tests { use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{message_info, MockApi}; + use cosmwasm_std::testing::message_info; use cosmwasm_std::{Coin, Uint128}; use cw_controllers::AdminError::NotAdmin; use mixnet_contract_common::OperatorsParamsUpdate; + use nym_contracts_common_testing::mock_api; #[test] fn update_contract_rewarding_validator_address() { let mut deps = test_helpers::init_contract(); + let mock_api = mock_api(); let info = message_info(&deps.api.addr_make("not-the-creator"), &[]); let res = try_update_rewarding_validator_address( deps.as_mut(), info, - MockApi::default().addr_make("not-the-creator").to_string(), + mock_api.addr_make("not-the-creator").to_string(), ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); @@ -151,14 +153,14 @@ pub mod tests { let res = try_update_rewarding_validator_address( deps.as_mut(), info, - MockApi::default().addr_make("new-good-address").to_string(), + mock_api.addr_make("new-good-address").to_string(), ); assert_eq!( res, Ok( Response::default().add_event(new_rewarding_validator_address_update_event( - MockApi::default().addr_make("rewarder"), - MockApi::default().addr_make("new-good-address") + mock_api.addr_make("rewarder"), + mock_api.addr_make("new-good-address") )) ) ); @@ -166,7 +168,7 @@ pub mod tests { let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap(); assert_eq!( state.rewarding_validator_address, - MockApi::default().addr_make("new-good-address") + mock_api.addr_make("new-good-address") ); assert_eq!( diff --git a/contracts/mixnet/src/nodes/queries.rs b/contracts/mixnet/src/nodes/queries.rs index 6283d21d75..02bc5276fc 100644 --- a/contracts/mixnet/src/nodes/queries.rs +++ b/contracts/mixnet/src/nodes/queries.rs @@ -6,6 +6,7 @@ use crate::constants::{ NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT, }; +use crate::interval::storage as interval_storage; use crate::nodes::helpers::{ attach_nym_node_details, get_node_details_by_id, get_node_details_by_identity, get_node_details_by_owner, @@ -21,7 +22,9 @@ use mixnet_contract_common::nym_node::{ PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, StakeSaturationResponse, UnbondedNodeResponse, }; -use mixnet_contract_common::{NodeId, NymNodeBond, NymNodeDetails}; +use mixnet_contract_common::{ + KeyRotationIdResponse, KeyRotationState, NodeId, NymNodeBond, NymNodeDetails, +}; use nym_contracts_common::IdentityKey; pub(crate) fn query_nymnode_bonds_paged( @@ -257,3 +260,14 @@ pub fn query_stake_saturation( uncapped_saturation: Some(node_rewarding.uncapped_bond_saturation(&rewarding_params)), }) } + +pub fn query_key_rotation_state(deps: Deps<'_>) -> StdResult { + storage::KEY_ROTATION_STATE.load(deps.storage) +} + +pub fn query_key_rotation_id(deps: Deps<'_>) -> StdResult { + let interval = interval_storage::current_interval(deps.storage)?; + let rotation_state = storage::KEY_ROTATION_STATE.load(deps.storage)?; + let rotation_id = rotation_state.key_rotation_id(interval.current_epoch_absolute_id()); + Ok(KeyRotationIdResponse { rotation_id }) +} diff --git a/contracts/mixnet/src/nodes/storage/helpers.rs b/contracts/mixnet/src/nodes/storage/helpers.rs index 169a0d42bc..7c8ae9dc6e 100644 --- a/contracts/mixnet/src/nodes/storage/helpers.rs +++ b/contracts/mixnet/src/nodes/storage/helpers.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; -use crate::nodes::storage::{nym_nodes, NYMNODE_ID_COUNTER}; +use crate::nodes::storage::{nym_nodes, KEY_ROTATION_STATE, NYMNODE_ID_COUNTER}; use cosmwasm_std::{StdResult, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; -use mixnet_contract_common::{EpochId, NodeId, NymNodeBond, RoleAssignment}; +use mixnet_contract_common::{EpochId, KeyRotationState, NodeId, NymNodeBond, RoleAssignment}; use serde::{Deserialize, Serialize}; #[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -103,7 +103,10 @@ pub(crate) fn next_nymnode_id_counter(store: &mut dyn Storage) -> StdResult Result<(), MixnetContractError> { +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + key_rotation_validity: u32, +) -> Result<(), MixnetContractError> { let active_bucket = RoleStorageBucket::default(); let inactive_bucket = active_bucket.other(); @@ -124,6 +127,15 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet ROLES_METADATA.save(storage, active_bucket as u8, &Default::default())?; ROLES_METADATA.save(storage, inactive_bucket as u8, &Default::default())?; + // since we're initialising fresh storage, the current epoch_id is 0 + KEY_ROTATION_STATE.save( + storage, + &KeyRotationState { + validity_epochs: key_rotation_validity, + initial_epoch_id: 0, + }, + )?; + Ok(()) } diff --git a/contracts/mixnet/src/nodes/storage/mod.rs b/contracts/mixnet/src/nodes/storage/mod.rs index 41e7aea033..fd45ee613a 100644 --- a/contracts/mixnet/src/nodes/storage/mod.rs +++ b/contracts/mixnet/src/nodes/storage/mod.rs @@ -2,23 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, NYMNODE_IDENTITY_IDX_NAMESPACE, - NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, NYMNODE_REWARDED_SET_METADATA_NAMESPACE, - NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, PENDING_NYMNODE_CHANGES_NAMESPACE, - UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, - UNBONDED_NYMNODE_PK_NAMESPACE, + KEY_ROTATION_STATE_KEY, NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, + NYMNODE_IDENTITY_IDX_NAMESPACE, NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, + NYMNODE_REWARDED_SET_METADATA_NAMESPACE, NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, + PENDING_NYMNODE_CHANGES_NAMESPACE, UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, + UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, UNBONDED_NYMNODE_PK_NAMESPACE, }; use crate::nodes::storage::helpers::RoleStorageBucket; use cosmwasm_std::Addr; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; use mixnet_contract_common::nym_node::{NymNodeBond, RewardedSetMetadata, Role, UnbondedNymNode}; -use mixnet_contract_common::{NodeId, PendingNodeChanges}; +use mixnet_contract_common::{KeyRotationState, NodeId, PendingNodeChanges}; use nym_contracts_common::IdentityKey; pub(crate) mod helpers; pub(crate) use helpers::*; +/// Item recording the current state of the key rotation setup +pub const KEY_ROTATION_STATE: Item = Item::new(KEY_ROTATION_STATE_KEY); + // IMPORTANT NOTE: we're using the same storage key as we had for MIXNODE_ID_COUNTER, // so that we could start from the old values pub const NYMNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index a0273196be..4f9690a87e 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,2 +1,21 @@ -// Copyright 2022-2024 - Nym Technologies SA +// Copyright 2022-2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage as interval_storage; +use crate::nodes::storage as nymnodes_storage; +use cosmwasm_std::DepsMut; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::KeyRotationState; + +pub fn introduce_key_rotation_id(deps: DepsMut) -> Result<(), MixnetContractError> { + let current_epoch_id = + interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id(); + nymnodes_storage::KEY_ROTATION_STATE.save( + deps.storage, + &KeyRotationState { + validity_epochs: 24, + initial_epoch_id: current_epoch_id, + }, + )?; + Ok(()) +} diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index f07a9c64a1..7f0b18077e 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -313,6 +313,8 @@ pub(crate) fn try_update_rewarding_params( } } +#[allow(clippy::panic)] +#[allow(clippy::unreachable)] #[cfg(test)] pub mod tests { use super::*; diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 61974e0da3..29aafaa92e 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -1,6 +1,11 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +// fine in test code +#![allow(clippy::panic)] +#![allow(clippy::unreachable)] +#![allow(clippy::unimplemented)] + #[cfg(test)] pub mod fixtures; pub(crate) mod legacy; @@ -46,11 +51,11 @@ pub mod test_helpers { use crate::support::helpers::ensure_no_existing_bond; use crate::support::tests; use crate::support::tests::fixtures::{ - good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM, + good_gateway_pledge, good_mixnode_pledge, good_node_plegge, }; use crate::support::tests::{legacy, test_helpers}; + use crate::testable_mixnet_contract::MixnetContract; use cosmwasm_std::testing::message_info; - use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; @@ -69,22 +74,24 @@ pub mod test_helpers { use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; use mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor, + NodeRewardingParameters, Performance, RewardingParams, WorkFactor, }; use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode; use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, - Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg, - Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, - NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, - OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment, - SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, + Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond, + MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload, + NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange, + RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, }; + use nym_contracts_common_testing::TestableNymContract; + use nym_contracts_common_testing::{mock_api, mock_dependencies}; use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519::KeyPair; use rand::distributions::WeightedIndex; @@ -95,13 +102,12 @@ pub mod test_helpers { use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; - use std::time::Duration; pub(crate) fn sorted_addresses(n: usize) -> Vec { let mut rng = test_rng(); let mut addrs = Vec::with_capacity(n); for i in 0..n { - addrs.push(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64()))); + addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64()))); } addrs.sort(); addrs @@ -1815,45 +1821,9 @@ pub mod test_helpers { SignableGatewayBondingMsg::new(nonce, content) } - fn intial_rewarded_set_params() -> RewardedSetParams { - RewardedSetParams { - entry_gateways: 50, - exit_gateways: 70, - mixnodes: 120, - standby: 50, - } - } - - fn initial_rewarding_params() -> InitialRewardingParams { - let reward_pool = 250_000_000_000_000u128; - let staking_supply = 100_000_000_000_000u128; - - InitialRewardingParams { - initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) - initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M - staking_supply_scale_factor: Percent::hundred(), - sybil_resistance: Percent::from_percentage_value(30).unwrap(), - active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), - interval_pool_emission: Percent::from_percentage_value(2).unwrap(), - rewarded_set_params: intial_rewarded_set_params(), - } - } - pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - rewarding_validator_address: deps.api.addr_make("rewarder").to_string(), - vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(), - rewarding_denom: TEST_COIN_DENOM.to_string(), - epochs_in_interval: 720, - epoch_duration: Duration::from_secs(60 * 60), - initial_rewarding_params: initial_rewarding_params(), - current_nym_node_version: "1.1.10".to_string(), - version_score_weights: Default::default(), - version_score_params: Default::default(), - profit_margin: Default::default(), - interval_operating_cost: Default::default(), - }; + let msg = MixnetContract::base_init_msg(); let env = mock_env(); let info = sender("creator"); instantiate(deps.as_mut(), env, info, msg).unwrap(); diff --git a/contracts/mixnet/src/testable_mixnet_contract.rs b/contracts/mixnet/src/testable_mixnet_contract.rs new file mode 100644 index 0000000000..498e121222 --- /dev/null +++ b/contracts/mixnet/src/testable_mixnet_contract.rs @@ -0,0 +1,89 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// fine in test code +#![allow(clippy::unwrap_used)] + +use crate::contract::{execute, instantiate, migrate, query}; +use cosmwasm_std::Decimal; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::reward_params::RewardedSetParams; +use mixnet_contract_common::{ + ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg, +}; +use nym_contracts_common::Percent; +use nym_contracts_common_testing::{ + mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM, +}; +use std::time::Duration; + +pub use nym_contracts_common_testing::TestableNymContract; + +pub struct MixnetContract; + +fn initial_rewarded_set_params() -> RewardedSetParams { + RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 50, + } +} + +fn initial_rewarding_params() -> InitialRewardingParams { + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + + InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_params: initial_rewarded_set_params(), + } +} + +impl TestableNymContract for MixnetContract { + const NAME: &'static str = "mixnet-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = MixnetContractError; + + fn instantiate() -> ContractFn { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + let deps = mock_dependencies(); + InstantiateMsg { + rewarding_validator_address: deps.api.addr_make("rewarder").to_string(), + vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(), + rewarding_denom: TEST_DENOM.to_string(), + epochs_in_interval: 720, + epoch_duration: Duration::from_secs(60 * 60), + initial_rewarding_params: initial_rewarding_params(), + current_nym_node_version: "1.1.10".to_string(), + version_score_weights: Default::default(), + version_score_params: Default::default(), + profit_margin: Default::default(), + interval_operating_cost: Default::default(), + key_validity_in_epochs: None, + } + } +} diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index 152c4adb87..683b7a6102 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -212,6 +212,7 @@ pub(crate) fn try_migrate_vested_delegation( )?)) } +#[allow(clippy::panic)] #[cfg(test)] mod tests { use super::*; diff --git a/contracts/nym-pool/.cargo/config b/contracts/nym-pool/.cargo/config new file mode 100644 index 0000000000..2fb2c1afdb --- /dev/null +++ b/contracts/nym-pool/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema --features=schema-gen" \ No newline at end of file diff --git a/contracts/nym-pool/Cargo.toml b/contracts/nym-pool/Cargo.toml new file mode 100644 index 0000000000..5bd54bb3bc --- /dev/null +++ b/contracts/nym-pool/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "nym-pool-contract" +version = "0.1.0" +edition = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[[bin]] +name = "schema" +required-features = ["schema-gen"] + +[lib] +name = "nym_pool_contract" +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-controllers = { workspace = true } + +cosmwasm-schema = { workspace = true, optional = true } + +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-pool-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-pool-contract" } + +[dev-dependencies] +anyhow = { workspace = true } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } + +[features] +schema-gen = ["nym-pool-contract-common/schema", "cosmwasm-schema"] diff --git a/contracts/nym-pool/Makefile b/contracts/nym-pool/Makefile new file mode 100644 index 0000000000..086fa71ad3 --- /dev/null +++ b/contracts/nym-pool/Makefile @@ -0,0 +1,5 @@ +wasm: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +generate-schema: + cargo schema diff --git a/contracts/nym-pool/schema/nym-pool-contract.json b/contracts/nym-pool/schema/nym-pool-contract.json new file mode 100644 index 0000000000..838f9a83cb --- /dev/null +++ b/contracts/nym-pool/schema/nym-pool-contract.json @@ -0,0 +1,1957 @@ +{ + "contract_name": "nym-pool-contract", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "grants", + "pool_denomination" + ], + "properties": { + "grants": { + "description": "Initial map of grants to be created at instantiation", + "type": "object", + "additionalProperties": false + }, + "pool_denomination": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "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" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + }, + "update_granter_set": { + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to grant new allowance to the specified grantee", + "type": "object", + "required": [ + "grant_allowance" + ], + "properties": { + "grant_allowance": { + "type": "object", + "required": [ + "allowance", + "grantee" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Allowance" + }, + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to revoke previously granted allowance", + "type": "object", + "required": [ + "revoke_allowance" + ], + "properties": { + "revoke_allowance": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to use allowance", + "type": "object", + "required": [ + "use_allowance" + ], + "properties": { + "use_allowance": { + "type": "object", + "required": [ + "recipients" + ], + "properties": { + "recipients": { + "type": "array", + "items": { + "$ref": "#/definitions/TransferRecipient" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to withdraw the specified amount into the grantee's account", + "type": "object", + "required": [ + "withdraw_allowance" + ], + "properties": { + "withdraw_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to lock part of existing allowance for future use", + "type": "object", + "required": [ + "lock_allowance" + ], + "properties": { + "lock_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to unlock previously locked allowance", + "type": "object", + "required": [ + "unlock_allowance" + ], + "properties": { + "unlock_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to use part of the locked allowance", + "type": "object", + "required": [ + "use_locked_allowance" + ], + "properties": { + "use_locked_allowance": { + "type": "object", + "required": [ + "recipients" + ], + "properties": { + "recipients": { + "type": "array", + "items": { + "$ref": "#/definitions/TransferRecipient" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to withdraw the specified amount of locked tokens into the grantee's account", + "type": "object", + "required": [ + "withdraw_locked_allowance" + ], + "properties": { + "withdraw_locked_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to add a new account to the permitted set of grant granters", + "type": "object", + "required": [ + "add_new_granter" + ], + "properties": { + "add_new_granter": { + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Revoke the provided account from the permitted set of granters", + "type": "object", + "required": [ + "revoke_granter" + ], + "properties": { + "revoke_granter": { + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to remove expired grant from the storage and unlock (if any) locked tokens", + "type": "object", + "required": [ + "remove_expired_grant" + ], + "properties": { + "remove_expired_grant": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "additionalProperties": false + }, + "TransferRecipient": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "recipient": { + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_available_tokens" + ], + "properties": { + "get_available_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_total_locked_tokens" + ], + "properties": { + "get_total_locked_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_locked_tokens" + ], + "properties": { + "get_locked_tokens": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_grant" + ], + "properties": { + "get_grant": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_granter" + ], + "properties": { + "get_granter": { + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_locked_tokens_paged" + ], + "properties": { + "get_locked_tokens_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_granters_paged" + ], + "properties": { + "get_granters_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_grants_paged" + ], + "properties": { + "get_grants_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "get_available_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AvailableTokensResponse", + "type": "object", + "required": [ + "available" + ], + "properties": { + "available": { + "$ref": "#/definitions/Coin" + } + }, + "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_grant": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GrantResponse", + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grant": { + "anyOf": [ + { + "$ref": "#/definitions/GrantInformation" + }, + { + "type": "null" + } + ] + }, + "grantee": { + "$ref": "#/definitions/Addr" + } + }, + "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" + }, + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "additionalProperties": false + }, + "Grant": { + "type": "object", + "required": [ + "allowance", + "granted_at_height", + "grantee", + "granter" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Allowance" + }, + "granted_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "grantee": { + "$ref": "#/definitions/Addr" + }, + "granter": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "GrantInformation": { + "type": "object", + "required": [ + "expired", + "grant" + ], + "properties": { + "expired": { + "type": "boolean" + }, + "grant": { + "$ref": "#/definitions/Grant" + } + }, + "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_granter": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GranterResponse", + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "$ref": "#/definitions/Addr" + }, + "information": { + "anyOf": [ + { + "$ref": "#/definitions/GranterInformation" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "GranterInformation": { + "type": "object", + "required": [ + "created_at_height", + "created_by" + ], + "properties": { + "created_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "created_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } + }, + "get_granters_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GrantersPagedResponse", + "type": "object", + "required": [ + "granters" + ], + "properties": { + "granters": { + "type": "array", + "items": { + "$ref": "#/definitions/GranterDetails" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "GranterDetails": { + "type": "object", + "required": [ + "granter", + "information" + ], + "properties": { + "granter": { + "$ref": "#/definitions/Addr" + }, + "information": { + "$ref": "#/definitions/GranterInformation" + } + }, + "additionalProperties": false + }, + "GranterInformation": { + "type": "object", + "required": [ + "created_at_height", + "created_by" + ], + "properties": { + "created_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "created_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } + }, + "get_grants_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GrantsPagedResponse", + "type": "object", + "required": [ + "grants" + ], + "properties": { + "grants": { + "type": "array", + "items": { + "$ref": "#/definitions/GrantInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "additionalProperties": false + }, + "Grant": { + "type": "object", + "required": [ + "allowance", + "granted_at_height", + "grantee", + "granter" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Allowance" + }, + "granted_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "grantee": { + "$ref": "#/definitions/Addr" + }, + "granter": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "GrantInformation": { + "type": "object", + "required": [ + "expired", + "grant" + ], + "properties": { + "expired": { + "type": "boolean" + }, + "grant": { + "$ref": "#/definitions/Grant" + } + }, + "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_locked_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LockedTokensResponse", + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "$ref": "#/definitions/Addr" + }, + "locked": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "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" + } + } + }, + "get_locked_tokens_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LockedTokensPagedResponse", + "type": "object", + "required": [ + "locked" + ], + "properties": { + "locked": { + "type": "array", + "items": { + "$ref": "#/definitions/LockedTokens" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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 + }, + "LockedTokens": { + "type": "object", + "required": [ + "grantee", + "locked" + ], + "properties": { + "grantee": { + "$ref": "#/definitions/Addr" + }, + "locked": { + "$ref": "#/definitions/Coin" + } + }, + "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_total_locked_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalLockedTokensResponse", + "type": "object", + "required": [ + "locked" + ], + "properties": { + "locked": { + "$ref": "#/definitions/Coin" + } + }, + "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" + } + } + } + } +} diff --git a/contracts/nym-pool/schema/raw/execute.json b/contracts/nym-pool/schema/raw/execute.json new file mode 100644 index 0000000000..905bca197f --- /dev/null +++ b/contracts/nym-pool/schema/raw/execute.json @@ -0,0 +1,544 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + }, + "update_granter_set": { + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to grant new allowance to the specified grantee", + "type": "object", + "required": [ + "grant_allowance" + ], + "properties": { + "grant_allowance": { + "type": "object", + "required": [ + "allowance", + "grantee" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Allowance" + }, + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to revoke previously granted allowance", + "type": "object", + "required": [ + "revoke_allowance" + ], + "properties": { + "revoke_allowance": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to use allowance", + "type": "object", + "required": [ + "use_allowance" + ], + "properties": { + "use_allowance": { + "type": "object", + "required": [ + "recipients" + ], + "properties": { + "recipients": { + "type": "array", + "items": { + "$ref": "#/definitions/TransferRecipient" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to withdraw the specified amount into the grantee's account", + "type": "object", + "required": [ + "withdraw_allowance" + ], + "properties": { + "withdraw_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to lock part of existing allowance for future use", + "type": "object", + "required": [ + "lock_allowance" + ], + "properties": { + "lock_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to unlock previously locked allowance", + "type": "object", + "required": [ + "unlock_allowance" + ], + "properties": { + "unlock_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to use part of the locked allowance", + "type": "object", + "required": [ + "use_locked_allowance" + ], + "properties": { + "use_locked_allowance": { + "type": "object", + "required": [ + "recipients" + ], + "properties": { + "recipients": { + "type": "array", + "items": { + "$ref": "#/definitions/TransferRecipient" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to withdraw the specified amount of locked tokens into the grantee's account", + "type": "object", + "required": [ + "withdraw_locked_allowance" + ], + "properties": { + "withdraw_locked_allowance": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to add a new account to the permitted set of grant granters", + "type": "object", + "required": [ + "add_new_granter" + ], + "properties": { + "add_new_granter": { + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Revoke the provided account from the permitted set of granters", + "type": "object", + "required": [ + "revoke_granter" + ], + "properties": { + "revoke_granter": { + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to remove expired grant from the storage and unlock (if any) locked tokens", + "type": "object", + "required": [ + "remove_expired_grant" + ], + "properties": { + "remove_expired_grant": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "additionalProperties": false + }, + "TransferRecipient": { + "type": "object", + "required": [ + "amount", + "recipient" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "recipient": { + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/instantiate.json b/contracts/nym-pool/schema/raw/instantiate.json new file mode 100644 index 0000000000..f2942f99c6 --- /dev/null +++ b/contracts/nym-pool/schema/raw/instantiate.json @@ -0,0 +1,262 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "grants", + "pool_denomination" + ], + "properties": { + "grants": { + "description": "Initial map of grants to be created at instantiation", + "type": "object", + "additionalProperties": false + }, + "pool_denomination": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/migrate.json b/contracts/nym-pool/schema/raw/migrate.json new file mode 100644 index 0000000000..7fbe8c5708 --- /dev/null +++ b/contracts/nym-pool/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/nym-pool/schema/raw/query.json b/contracts/nym-pool/schema/raw/query.json new file mode 100644 index 0000000000..1af2075d3b --- /dev/null +++ b/contracts/nym-pool/schema/raw/query.json @@ -0,0 +1,201 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_available_tokens" + ], + "properties": { + "get_available_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_total_locked_tokens" + ], + "properties": { + "get_total_locked_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_locked_tokens" + ], + "properties": { + "get_locked_tokens": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_grant" + ], + "properties": { + "get_grant": { + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_granter" + ], + "properties": { + "get_granter": { + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_locked_tokens_paged" + ], + "properties": { + "get_locked_tokens_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_granters_paged" + ], + "properties": { + "get_granters_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_grants_paged" + ], + "properties": { + "get_grants_paged": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/nym-pool/schema/raw/response_to_admin.json b/contracts/nym-pool/schema/raw/response_to_admin.json new file mode 100644 index 0000000000..c73969ab04 --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_available_tokens.json b/contracts/nym-pool/schema/raw/response_to_get_available_tokens.json new file mode 100644 index 0000000000..cd495cbe0c --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_available_tokens.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AvailableTokensResponse", + "type": "object", + "required": [ + "available" + ], + "properties": { + "available": { + "$ref": "#/definitions/Coin" + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_grant.json b/contracts/nym-pool/schema/raw/response_to_get_grant.json new file mode 100644 index 0000000000..08e4cf0ffe --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_grant.json @@ -0,0 +1,312 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GrantResponse", + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grant": { + "anyOf": [ + { + "$ref": "#/definitions/GrantInformation" + }, + { + "type": "null" + } + ] + }, + "grantee": { + "$ref": "#/definitions/Addr" + } + }, + "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" + }, + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "additionalProperties": false + }, + "Grant": { + "type": "object", + "required": [ + "allowance", + "granted_at_height", + "grantee", + "granter" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Allowance" + }, + "granted_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "grantee": { + "$ref": "#/definitions/Addr" + }, + "granter": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "GrantInformation": { + "type": "object", + "required": [ + "expired", + "grant" + ], + "properties": { + "expired": { + "type": "boolean" + }, + "grant": { + "$ref": "#/definitions/Grant" + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_granter.json b/contracts/nym-pool/schema/raw/response_to_get_granter.json new file mode 100644 index 0000000000..89a704a666 --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_granter.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GranterResponse", + "type": "object", + "required": [ + "granter" + ], + "properties": { + "granter": { + "$ref": "#/definitions/Addr" + }, + "information": { + "anyOf": [ + { + "$ref": "#/definitions/GranterInformation" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "GranterInformation": { + "type": "object", + "required": [ + "created_at_height", + "created_by" + ], + "properties": { + "created_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "created_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_granters_paged.json b/contracts/nym-pool/schema/raw/response_to_get_granters_paged.json new file mode 100644 index 0000000000..402cac8338 --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_granters_paged.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GrantersPagedResponse", + "type": "object", + "required": [ + "granters" + ], + "properties": { + "granters": { + "type": "array", + "items": { + "$ref": "#/definitions/GranterDetails" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "GranterDetails": { + "type": "object", + "required": [ + "granter", + "information" + ], + "properties": { + "granter": { + "$ref": "#/definitions/Addr" + }, + "information": { + "$ref": "#/definitions/GranterInformation" + } + }, + "additionalProperties": false + }, + "GranterInformation": { + "type": "object", + "required": [ + "created_at_height", + "created_by" + ], + "properties": { + "created_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "created_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_grants_paged.json b/contracts/nym-pool/schema/raw/response_to_get_grants_paged.json new file mode 100644 index 0000000000..6e703edb69 --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_grants_paged.json @@ -0,0 +1,311 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GrantsPagedResponse", + "type": "object", + "required": [ + "grants" + ], + "properties": { + "grants": { + "type": "array", + "items": { + "$ref": "#/definitions/GrantInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "Allowance": { + "oneOf": [ + { + "type": "object", + "required": [ + "basic" + ], + "properties": { + "basic": { + "$ref": "#/definitions/BasicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "classic_periodic" + ], + "properties": { + "classic_periodic": { + "$ref": "#/definitions/ClassicPeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cumulative_periodic" + ], + "properties": { + "cumulative_periodic": { + "$ref": "#/definitions/CumulativePeriodicAllowance" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delayed" + ], + "properties": { + "delayed": { + "$ref": "#/definitions/DelayedAllowance" + } + }, + "additionalProperties": false + } + ] + }, + "BasicAllowance": { + "description": "BasicAllowance is an allowance with a one-time grant of coins that optionally expires. The grantee can use up to SpendLimit to cover fees.", + "type": "object", + "properties": { + "expiration_unix_timestamp": { + "description": "expiration specifies an optional time when this allowance expires", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "spend_limit": { + "description": "spend_limit specifies the maximum amount of coins that can be spent by this allowance and will be updated as coins are spent. If it is empty, there is no spend limit and any amount of coins can be spent.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "ClassicPeriodicAllowance": { + "description": "ClassicPeriodicAllowance extends BasicAllowance to allow for both a maximum cap, as well as a limit per time period.", + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_spend_limit" + ], + "properties": { + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "period_can_spend": { + "description": "period_can_spend is the number of coins left to be spent before the period_reset time", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which period_spend_limit coins can be spent before that allowance is reset", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_reset_unix_timestamp": { + "description": "period_reset is the time at which this period resets and a new one begins, it is calculated from the start time of the first transaction after the last period ended", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_spend_limit": { + "description": "period_spend_limit specifies the maximum number of coins that can be spent in the period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "CumulativePeriodicAllowance": { + "type": "object", + "required": [ + "basic", + "period_duration_secs", + "period_grant" + ], + "properties": { + "accumulation_limit": { + "description": "accumulation_limit is the maximum value the grants and accumulate to", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + }, + "last_grant_applied_unix_timestamp": { + "description": "last_grant_applied is the time at which last transaction associated with this allowance has been sent and `spendable` value has been adjusted", + "default": 0, + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_duration_secs": { + "description": "period_duration_secs specifies the time duration in which spendable coins can be spent before that allowance is incremented", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "period_grant": { + "description": "period_grant specifies the maximum number of coins that is granted per period", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "spendable": { + "description": "spendable is the number of coins left to be spent before additional grant is applied", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "DelayedAllowance": { + "description": "Create a grant to allow somebody to withdraw from the pool only after the specified time. For example, we could create a grant for mixnet rewarding/testing/etc However, if the required work has not been completed, the grant could be revoked before it's withdrawn", + "type": "object", + "required": [ + "available_at_unix_timestamp", + "basic" + ], + "properties": { + "available_at_unix_timestamp": { + "description": "available_at specifies when this allowance is going to become usable", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "basic": { + "description": "basic specifies a struct of `BasicAllowance`", + "allOf": [ + { + "$ref": "#/definitions/BasicAllowance" + } + ] + } + }, + "additionalProperties": false + }, + "Grant": { + "type": "object", + "required": [ + "allowance", + "granted_at_height", + "grantee", + "granter" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Allowance" + }, + "granted_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "grantee": { + "$ref": "#/definitions/Addr" + }, + "granter": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "GrantInformation": { + "type": "object", + "required": [ + "expired", + "grant" + ], + "properties": { + "expired": { + "type": "boolean" + }, + "grant": { + "$ref": "#/definitions/Grant" + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_locked_tokens.json b/contracts/nym-pool/schema/raw/response_to_get_locked_tokens.json new file mode 100644 index 0000000000..9f0d8aec6b --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_locked_tokens.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LockedTokensResponse", + "type": "object", + "required": [ + "grantee" + ], + "properties": { + "grantee": { + "$ref": "#/definitions/Addr" + }, + "locked": { + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_locked_tokens_paged.json b/contracts/nym-pool/schema/raw/response_to_get_locked_tokens_paged.json new file mode 100644 index 0000000000..24c8268d44 --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_locked_tokens_paged.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LockedTokensPagedResponse", + "type": "object", + "required": [ + "locked" + ], + "properties": { + "locked": { + "type": "array", + "items": { + "$ref": "#/definitions/LockedTokens" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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 + }, + "LockedTokens": { + "type": "object", + "required": [ + "grantee", + "locked" + ], + "properties": { + "grantee": { + "$ref": "#/definitions/Addr" + }, + "locked": { + "$ref": "#/definitions/Coin" + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/schema/raw/response_to_get_total_locked_tokens.json b/contracts/nym-pool/schema/raw/response_to_get_total_locked_tokens.json new file mode 100644 index 0000000000..5d423ece3e --- /dev/null +++ b/contracts/nym-pool/schema/raw/response_to_get_total_locked_tokens.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalLockedTokensResponse", + "type": "object", + "required": [ + "locked" + ], + "properties": { + "locked": { + "$ref": "#/definitions/Coin" + } + }, + "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" + } + } +} diff --git a/contracts/nym-pool/src/bin/schema.rs b/contracts/nym-pool/src/bin/schema.rs new file mode 100644 index 0000000000..cf6a1b03dc --- /dev/null +++ b/contracts/nym-pool/src/bin/schema.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::write_api; +use nym_pool_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/nym-pool/src/contract.rs b/contracts/nym-pool/src/contract.rs new file mode 100644 index 0000000000..476aaad5f7 --- /dev/null +++ b/contracts/nym-pool/src/contract.rs @@ -0,0 +1,327 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::queries::{ + query_admin, query_available_tokens, query_grant, query_granter, query_granters_paged, + query_grants_paged, query_locked_tokens, query_locked_tokens_paged, query_total_locked_tokens, +}; +use crate::storage::NYM_POOL_STORAGE; +use crate::transactions::{ + try_add_new_granter, try_grant_allowance, try_lock_allowance, try_remove_expired, + try_revoke_grant, try_revoke_granter, try_unlock_allowance, try_update_contract_admin, + try_use_allowance, try_use_locked_allowance, try_withdraw_allowance, + try_withdraw_locked_allowance, +}; +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, +}; +use nym_contracts_common::set_build_information; +use nym_pool_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NymPoolContractError, QueryMsg, +}; + +const CONTRACT_NAME: &str = "crate:nym-pool-contract"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(deps.storage)?; + + NYM_POOL_STORAGE.initialise(deps, env, info.sender, &msg.pool_denomination, msg.grants)?; + + Ok(Response::default()) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateAdmin { + admin, + update_granter_set, + } => try_update_contract_admin(deps, env, info, admin, update_granter_set), + ExecuteMsg::GrantAllowance { grantee, allowance } => { + try_grant_allowance(deps, env, info, grantee, *allowance) + } + ExecuteMsg::RevokeAllowance { grantee } => try_revoke_grant(deps, env, info, grantee), + ExecuteMsg::UseAllowance { recipients } => try_use_allowance(deps, env, info, recipients), + ExecuteMsg::WithdrawAllowance { amount } => try_withdraw_allowance(deps, env, info, amount), + ExecuteMsg::LockAllowance { amount } => try_lock_allowance(deps, env, info, amount), + ExecuteMsg::UnlockAllowance { amount } => try_unlock_allowance(deps, env, info, amount), + ExecuteMsg::UseLockedAllowance { recipients } => { + try_use_locked_allowance(deps, env, info, recipients) + } + ExecuteMsg::WithdrawLockedAllowance { amount } => { + try_withdraw_locked_allowance(deps, env, info, amount) + } + ExecuteMsg::AddNewGranter { granter } => try_add_new_granter(deps, env, info, granter), + ExecuteMsg::RevokeGranter { granter } => try_revoke_granter(deps, env, info, granter), + ExecuteMsg::RemoveExpiredGrant { grantee } => try_remove_expired(deps, env, info, grantee), + } +} + +#[entry_point] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?), + QueryMsg::GetAvailableTokens {} => Ok(to_json_binary(&query_available_tokens(deps, env)?)?), + QueryMsg::GetTotalLockedTokens {} => Ok(to_json_binary(&query_total_locked_tokens(deps)?)?), + QueryMsg::GetLockedTokens { grantee } => { + Ok(to_json_binary(&query_locked_tokens(deps, grantee)?)?) + } + QueryMsg::GetLockedTokensPaged { limit, start_after } => Ok(to_json_binary( + &query_locked_tokens_paged(deps, limit, start_after)?, + )?), + QueryMsg::GetGrant { grantee } => Ok(to_json_binary(&query_grant(deps, env, grantee)?)?), + QueryMsg::GetGranter { granter } => Ok(to_json_binary(&query_granter(deps, granter)?)?), + QueryMsg::GetGrantersPaged { limit, start_after } => Ok(to_json_binary( + &query_granters_paged(deps, limit, start_after)?, + )?), + QueryMsg::GetGrantsPaged { limit, start_after } => Ok(to_json_binary( + &query_grants_paged(deps, env, limit, start_after)?, + )?), + } +} + +#[entry_point] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result { + set_build_information!(deps.storage)?; + cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod contract_instantiaton { + use super::*; + use crate::storage::NYM_POOL_STORAGE; + use crate::testing::TEST_DENOM; + use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env}; + + #[test] + fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> { + let mut deps = mock_dependencies(); + let env = mock_env(); + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants: Default::default(), + }; + + let some_sender = deps.api.addr_make("some_sender"); + instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[]), + init_msg, + )?; + + NYM_POOL_STORAGE + .contract_admin + .assert_admin(deps.as_ref(), &some_sender)?; + + Ok(()) + } + + #[test] + fn sets_the_pool_denomination() -> anyhow::Result<()> { + let mut deps = mock_dependencies(); + let env = mock_env(); + let init_msg = InstantiateMsg { + pool_denomination: "some_denom".to_string(), + grants: Default::default(), + }; + + let some_sender = deps.api.addr_make("some_sender"); + instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[]), + init_msg, + )?; + + assert_eq!( + NYM_POOL_STORAGE + .pool_denomination + .load(deps.as_ref().storage)?, + "some_denom" + ); + + Ok(()) + } + + #[test] + fn adds_sender_to_set_of_initial_granters() -> anyhow::Result<()> { + let mut deps = mock_dependencies(); + let env = mock_env(); + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants: Default::default(), + }; + + let some_sender = deps.api.addr_make("some_sender"); + instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[]), + init_msg, + )?; + + let granter = query_granter(deps.as_ref(), some_sender.to_string())?; + assert!(granter.information.is_some()); + + Ok(()) + } + + #[cfg(test)] + mod setting_initial_grants { + use super::*; + use cosmwasm_std::{coin, Order, Storage}; + use nym_contracts_common_testing::deps_with_balance; + use nym_pool_contract_common::{Allowance, BasicAllowance, Grant, GranteeAddress}; + use std::collections::HashMap; + + fn all_grants(storage: &dyn Storage) -> HashMap { + NYM_POOL_STORAGE + .grants + .range(storage, None, None, Order::Ascending) + .collect::, _>>() + .unwrap() + } + + #[test] + fn with_empty_map() -> anyhow::Result<()> { + let mut deps = mock_dependencies(); + let env = mock_env(); + let grants = HashMap::new(); + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants, + }; + + let some_sender = deps.api.addr_make("some_sender"); + instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[]), + init_msg, + )?; + + assert!(all_grants(&deps.storage).is_empty()); + Ok(()) + } + + #[test] + fn with_insufficient_tokens() -> anyhow::Result<()> { + // limited grant + let mut deps = mock_dependencies(); + let env = mock_env(); + let mut grants = HashMap::new(); + grants.insert( + deps.api.addr_make("grantee1").to_string(), + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(100, TEST_DENOM)), + expiration_unix_timestamp: None, + }), + ); + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants, + }; + + let some_sender = deps.api.addr_make("some_sender"); + let res = instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[]), + init_msg, + ); + assert!(res.is_err()); + + // unlimited grant + let mut deps = mock_dependencies(); + let env = mock_env(); + let mut grants = HashMap::new(); + grants.insert( + deps.api.addr_make("grantee1").to_string(), + Allowance::Basic(BasicAllowance::unlimited()), + ); + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants, + }; + + let some_sender = deps.api.addr_make("some_sender"); + let res = instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[]), + init_msg, + ); + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn with_valid_request() -> anyhow::Result<()> { + let env = mock_env(); + let mut deps = deps_with_balance(&env); + let mut grants = HashMap::new(); + grants.insert( + deps.api.addr_make("grantee1").to_string(), + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(100, TEST_DENOM)), + expiration_unix_timestamp: None, + }), + ); + grants.insert( + deps.api.addr_make("grantee2").to_string(), + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(200, TEST_DENOM)), + expiration_unix_timestamp: None, + }), + ); + grants.insert( + deps.api.addr_make("grantee3").to_string(), + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(300, TEST_DENOM)), + expiration_unix_timestamp: None, + }), + ); + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants, + }; + + let some_sender = deps.api.addr_make("some_sender"); + instantiate( + deps.as_mut(), + env, + message_info(&some_sender, &[coin(600, TEST_DENOM)]), + init_msg, + )?; + + assert_eq!(all_grants(&deps.storage).len(), 3); + Ok(()) + } + } + } +} diff --git a/contracts/nym-pool/src/helpers.rs b/contracts/nym-pool/src/helpers.rs new file mode 100644 index 0000000000..129c9089e6 --- /dev/null +++ b/contracts/nym-pool/src/helpers.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::NYM_POOL_STORAGE; +use cosmwasm_std::{Coin, Storage}; +use nym_pool_contract_common::NymPoolContractError; + +pub fn validate_usage_coin(storage: &dyn Storage, coin: &Coin) -> Result<(), NymPoolContractError> { + let denom = NYM_POOL_STORAGE.pool_denomination.load(storage)?; + + if coin.amount.is_zero() { + return Err(NymPoolContractError::EmptyUsageRequest); + } + + if coin.denom != denom { + return Err(NymPoolContractError::InvalidDenom { + expected: denom, + got: coin.denom.to_string(), + }); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::NymPoolStorage; + use crate::testing::init_contract_tester; + use cosmwasm_std::coin; + use nym_contracts_common_testing::ContractOpts; + + #[test] + fn validating_coin_usage() -> anyhow::Result<()> { + let test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let denom = storage.pool_denomination.load(test.storage())?; + + // amount has to be non-zero + assert_eq!( + validate_usage_coin(test.storage(), &coin(0, &denom)).unwrap_err(), + NymPoolContractError::EmptyUsageRequest + ); + + // denom has to match the value set in the storage + assert_eq!( + validate_usage_coin(test.storage(), &coin(1000, "bad-denom")).unwrap_err(), + NymPoolContractError::InvalidDenom { + expected: denom.to_string(), + got: "bad-denom".to_string(), + } + ); + + assert!(validate_usage_coin(test.storage(), &coin(1000, denom)).is_ok()); + + Ok(()) + } +} diff --git a/contracts/nym-pool/src/lib.rs b/contracts/nym-pool/src/lib.rs new file mode 100644 index 0000000000..a1aca86201 --- /dev/null +++ b/contracts/nym-pool/src/lib.rs @@ -0,0 +1,17 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +pub mod contract; +pub mod queued_migrations; +pub mod storage; + +mod helpers; +mod queries; +#[cfg(test)] +pub mod testing; +mod transactions; diff --git a/contracts/nym-pool/src/queries.rs b/contracts/nym-pool/src/queries.rs new file mode 100644 index 0000000000..2d8dcc57a1 --- /dev/null +++ b/contracts/nym-pool/src/queries.rs @@ -0,0 +1,659 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::{retrieval_limits, NYM_POOL_STORAGE}; +use cosmwasm_std::{Coin, Deps, Env, Order, StdResult}; +use cw_controllers::AdminResponse; +use cw_storage_plus::Bound; +use nym_pool_contract_common::{ + AvailableTokensResponse, GrantInformation, GrantResponse, GranterDetails, GranterResponse, + GrantersPagedResponse, GrantsPagedResponse, LockedTokens, LockedTokensPagedResponse, + LockedTokensResponse, NymPoolContractError, TotalLockedTokensResponse, +}; + +pub fn query_admin(deps: Deps) -> Result { + NYM_POOL_STORAGE + .contract_admin + .query_admin(deps) + .map_err(Into::into) +} + +pub fn query_available_tokens( + deps: Deps, + env: Env, +) -> Result { + Ok(AvailableTokensResponse { + available: NYM_POOL_STORAGE.available_tokens(deps, &env)?, + }) +} + +pub fn query_total_locked_tokens( + deps: Deps, +) -> Result { + let denom = NYM_POOL_STORAGE.pool_denomination.load(deps.storage)?; + let amount = NYM_POOL_STORAGE.locked.total_locked.load(deps.storage)?; + Ok(TotalLockedTokensResponse { + locked: Coin::new(amount, denom), + }) +} + +pub fn query_locked_tokens( + deps: Deps, + grantee: String, +) -> Result { + let grantee = deps.api.addr_validate(&grantee)?; + let denom = NYM_POOL_STORAGE.pool_denomination.load(deps.storage)?; + let amount = NYM_POOL_STORAGE + .locked + .maybe_grantee_locked(deps.storage, &grantee)?; + + Ok(LockedTokensResponse { + locked: amount.map(|amount| Coin::new(amount, denom)), + grantee, + }) +} + +pub fn query_locked_tokens_paged( + deps: Deps, + limit: Option, + start_after: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::LOCKED_TOKENS_DEFAULT_LIMIT) + .min(retrieval_limits::LOCKED_TOKENS_MAX_LIMIT) as usize; + let grantee = start_after + .map(|grantee| deps.api.addr_validate(&grantee)) + .transpose()?; + let denom = NYM_POOL_STORAGE.pool_denomination.load(deps.storage)?; + + let start = grantee.map(Bound::exclusive); + + let locked = NYM_POOL_STORAGE + .locked + .grantees + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(grantee, amount)| LockedTokens { + grantee, + locked: Coin::new(amount, &denom), + }) + }) + .collect::>>()?; + + let start_next_after = locked.last().map(|locked| locked.grantee.to_string()); + + Ok(LockedTokensPagedResponse { + locked, + start_next_after, + }) +} + +pub fn query_grant( + deps: Deps, + env: Env, + grantee: String, +) -> Result { + let grantee = deps.api.addr_validate(&grantee)?; + let grant = NYM_POOL_STORAGE.try_load_grant(deps, &grantee)?; + + Ok(GrantResponse { + grant: grant.map(|grant| GrantInformation { + expired: grant.allowance.expired(&env), + grant, + }), + grantee, + }) +} + +pub fn query_granter(deps: Deps, granter: String) -> Result { + let granter = deps.api.addr_validate(&granter)?; + + Ok(GranterResponse { + information: NYM_POOL_STORAGE.try_load_granter(deps, &granter)?, + granter, + }) +} + +pub fn query_grants_paged( + deps: Deps, + env: Env, + limit: Option, + start_after: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::GRANTERS_DEFAULT_LIMIT) + .min(retrieval_limits::GRANTERS_MAX_LIMIT) as usize; + let grantee = start_after + .map(|grantee| deps.api.addr_validate(&grantee)) + .transpose()?; + + let start = grantee.map(Bound::exclusive); + + let grants = NYM_POOL_STORAGE + .grants + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(_, grant)| GrantInformation { + expired: grant.allowance.expired(&env), + grant, + }) + }) + .collect::>>()?; + + let start_next_after = grants.last().map(|info| info.grant.grantee.to_string()); + + Ok(GrantsPagedResponse { + grants, + start_next_after, + }) +} + +pub fn query_granters_paged( + deps: Deps, + limit: Option, + start_after: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::GRANTERS_DEFAULT_LIMIT) + .min(retrieval_limits::GRANTERS_MAX_LIMIT) as usize; + let granter = start_after + .map(|granter| deps.api.addr_validate(&granter)) + .transpose()?; + let start = granter.map(Bound::exclusive); + + let granters = NYM_POOL_STORAGE + .granters + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(granter, info)| GranterDetails::from((granter, info)))) + .collect::>>()?; + + let start_next_after = granters.last().map(|details| details.granter.to_string()); + + Ok(GrantersPagedResponse { + granters, + start_next_after, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::contract::instantiate; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM}; + use cosmwasm_std::testing::{message_info, mock_dependencies_with_balance, mock_env}; + use cosmwasm_std::{coin, Uint128}; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt}; + use nym_pool_contract_common::{Allowance, BasicAllowance, GranterInformation, InstantiateMsg}; + + #[cfg(test)] + mod admin_query { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; + use nym_pool_contract_common::ExecuteMsg; + + #[test] + fn returns_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let initial_admin = test.admin_unchecked(); + + // initial + let res = query_admin(test.deps())?; + assert_eq!(res.admin, Some(initial_admin.to_string())); + + let new_admin = test.generate_account(); + + // sanity check + assert_ne!(initial_admin, new_admin); + + // after update + test.execute_msg( + initial_admin.clone(), + &ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + update_granter_set: None, + }, + )?; + + let updated_admin = query_admin(test.deps())?; + assert_eq!(updated_admin.admin, Some(new_admin.to_string())); + + Ok(()) + } + } + + #[test] + fn available_tokens_query() { + // no need to test the inner functionalities as this is dealt with in the storage tests + // (i.e. logic to do with calculating diff against locked tokens, etc.) + let env = mock_env(); + let mut deps = mock_dependencies_with_balance(&[coin(100, TEST_DENOM)]); + + let init_msg = InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants: Default::default(), + }; + + let some_sender = deps.api.addr_make("some_sender"); + instantiate( + deps.as_mut(), + env.clone(), + message_info(&some_sender, &[]), + init_msg, + ) + .unwrap(); + + assert_eq!( + query_available_tokens(deps.as_ref(), env) + .unwrap() + .available, + coin(100, TEST_DENOM) + ); + } + + #[test] + fn total_locked_tokens_query() { + let mut test = init_contract_tester(); + + let locked = query_total_locked_tokens(test.deps()).unwrap().locked; + assert!(locked.amount.is_zero()); + assert_eq!(locked.denom, test.denom()); + + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(grantee, Uint128::new(1234)); + + let locked = query_total_locked_tokens(test.deps()).unwrap().locked; + assert_eq!(locked.amount, Uint128::new(1234)); + assert_eq!(locked.denom, test.denom()); + } + + #[test] + fn locked_tokens_query() { + let mut test = init_contract_tester(); + + let grantee1 = test.add_dummy_grant().grantee; + test.lock_allowance(grantee1.as_str(), Uint128::new(1234)); + + let grantee2 = test.add_dummy_grant().grantee; + let not_grantee = test.generate_account(); + + let res = query_locked_tokens(test.deps(), grantee1.to_string()).unwrap(); + assert_eq!(res.grantee, grantee1); + assert_eq!(res.locked, Some(coin(1234, TEST_DENOM))); + + let res = query_locked_tokens(test.deps(), grantee2.to_string()).unwrap(); + assert_eq!(res.grantee, grantee2); + assert!(res.locked.is_none()); + + let res = query_locked_tokens(test.deps(), not_grantee.to_string()).unwrap(); + assert_eq!(res.grantee, not_grantee); + assert!(res.locked.is_none()); + } + + #[cfg(test)] + mod locked_tokens_paged_query { + use super::*; + use crate::testing::NymPoolContract; + use nym_contracts_common_testing::ContractTester; + + fn lock_sorted( + test: &mut ContractTester, + count: usize, + ) -> Vec { + let mut grantees = Vec::new(); + + for _ in 0..count { + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(grantee.as_str(), Uint128::new(100)); + grantees.push(LockedTokens { + grantee, + locked: coin(100, test.denom()), + }); + } + + grantees.sort_by_key(|g| g.grantee.clone()); + grantees + } + + #[test] + fn obeys_limits() { + let mut test = init_contract_tester(); + let _locked = lock_sorted(&mut test, 1000); + + let limit = 42; + let page1 = query_locked_tokens_paged(test.deps(), Some(limit), None).unwrap(); + assert_eq!(page1.locked.len(), limit as usize); + } + + #[test] + fn has_default_limit() { + let mut test = init_contract_tester(); + let _locked = lock_sorted(&mut test, 1000); + + // query without explicitly setting a limit + let page1 = query_locked_tokens_paged(test.deps(), None, None).unwrap(); + assert_eq!( + page1.locked.len() as u32, + retrieval_limits::LOCKED_TOKENS_DEFAULT_LIMIT + ); + } + + #[test] + fn has_max_limit() { + let mut test = init_contract_tester(); + let _locked = lock_sorted(&mut test, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_locked_tokens_paged(test.deps(), Some(crazy_limit), None).unwrap(); + + assert_eq!( + page1.locked.len() as u32, + retrieval_limits::LOCKED_TOKENS_MAX_LIMIT + ); + } + + #[test] + fn pagination_works() { + let mut test = init_contract_tester(); + let locked = lock_sorted(&mut test, 1000); + + // first page should return 2 results... + let page1 = query_locked_tokens_paged(test.deps(), Some(2), None).unwrap(); + assert_eq!(page1.locked, locked[..2].to_vec()); + + // if we start after 5th entry, the returned following page should have 6th and onwards + let second = locked[1].clone(); + + let page2 = + query_locked_tokens_paged(test.deps(), Some(3), Some(second.grantee.to_string())) + .unwrap(); + assert_eq!(page2.locked, locked[2..5].to_vec()); + } + } + + #[test] + fn grant_query() { + let mut test = init_contract_tester(); + let env = test.env(); + + // bad address + let bad_address = "not-valid-bech32"; + assert!(query_grant(test.deps(), env.clone(), bad_address.to_string()).is_err()); + + // exists + let grant = test.add_dummy_grant(); + let grantee = grant.grantee.clone(); + + assert_eq!( + query_grant(test.deps(), env.clone(), grantee.to_string()).unwrap(), + GrantResponse { + grantee, + grant: Some(GrantInformation { + grant, + expired: false, + }), + } + ); + + // exists expired + let grantee = test.generate_account(); + let exp = env.block.time.seconds() + 1; + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: Some(exp), + }); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE + .insert_new_grant(test.deps_mut(), &env, &admin, &grantee, allowance) + .unwrap(); + let grant = NYM_POOL_STORAGE.load_grant(test.deps(), &grantee).unwrap(); + + test.next_block(); + let env = test.env(); + + assert_eq!( + query_grant(test.deps(), env.clone(), grantee.to_string()).unwrap(), + GrantResponse { + grantee, + grant: Some(GrantInformation { + grant, + expired: true, + }), + } + ); + + // doesn't exist + let doesnt_exist = test.generate_account(); + assert_eq!( + query_grant(test.deps(), env.clone(), doesnt_exist.to_string()).unwrap(), + GrantResponse { + grantee: doesnt_exist, + grant: None, + } + ) + } + + #[test] + fn granter_query() { + let mut test = init_contract_tester(); + let admin = test.admin_unchecked(); + let env = test.env(); + + // bad address + let bad_address = "not-valid-bech32"; + assert!(query_granter(test.deps(), bad_address.to_string()).is_err()); + + // exists + let granter = test.generate_account(); + test.add_granter(&granter); + + assert_eq!( + query_granter(test.deps(), granter.to_string()).unwrap(), + GranterResponse { + granter, + information: Some(GranterInformation { + created_by: admin.clone(), + created_at_height: env.block.height, + }), + } + ); + + // (admin is also a granter) + assert_eq!( + query_granter(test.deps(), admin.to_string()).unwrap(), + GranterResponse { + information: Some(GranterInformation { + created_by: admin.clone(), + created_at_height: env.block.height, + }), + granter: admin, + } + ); + + // doesn't exist + let not_granter = test.generate_account(); + assert_eq!( + query_granter(test.deps(), not_granter.to_string()).unwrap(), + GranterResponse { + granter: not_granter, + information: None, + } + ); + } + + #[cfg(test)] + mod granters_paged_query { + use super::*; + use crate::testing::NymPoolContract; + use nym_contracts_common_testing::ContractTester; + + fn granters_sorted( + test: &mut ContractTester, + count: usize, + ) -> Vec { + let mut granters = Vec::new(); + + for _ in 0..count { + let granter = test.add_dummy_grant().grantee; + test.add_granter(&granter); + granters.push(GranterDetails { + granter, + information: GranterInformation { + created_by: test.admin_unchecked(), + created_at_height: test.env().block.height, + }, + }); + } + + granters.sort_by_key(|g| g.granter.clone()); + granters + } + + #[test] + fn obeys_limits() { + let mut test = init_contract_tester(); + let _granters = granters_sorted(&mut test, 1000); + + let limit = 42; + let page1 = query_granters_paged(test.deps(), Some(limit), None).unwrap(); + assert_eq!(page1.granters.len(), limit as usize); + } + + #[test] + fn has_default_limit() { + let mut test = init_contract_tester(); + let _granters = granters_sorted(&mut test, 1000); + + // query without explicitly setting a limit + let page1 = query_granters_paged(test.deps(), None, None).unwrap(); + assert_eq!( + page1.granters.len() as u32, + retrieval_limits::GRANTERS_DEFAULT_LIMIT + ); + } + + #[test] + fn has_max_limit() { + let mut test = init_contract_tester(); + let _granters = granters_sorted(&mut test, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_granters_paged(test.deps(), Some(crazy_limit), None).unwrap(); + + assert_eq!( + page1.granters.len() as u32, + retrieval_limits::GRANTERS_MAX_LIMIT + ); + } + + #[test] + fn pagination_works() { + let mut test = init_contract_tester(); + let locked = granters_sorted(&mut test, 1000); + + // first page should return 2 results... + let page1 = query_granters_paged(test.deps(), Some(2), None).unwrap(); + assert_eq!(page1.granters, locked[..2].to_vec()); + + // if we start after 5th entry, the returned following page should have 6th and onwards + let second = locked[1].clone(); + + let page2 = + query_granters_paged(test.deps(), Some(3), Some(second.granter.to_string())) + .unwrap(); + assert_eq!(page2.granters, locked[2..5].to_vec()); + } + } + + #[cfg(test)] + mod grants_paged_query { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContract}; + use nym_contracts_common_testing::{ContractOpts, ContractTester}; + + fn grants_sorted( + test: &mut ContractTester, + count: usize, + ) -> Vec { + let mut grantees = Vec::new(); + + for _ in 0..count { + let grant = test.add_dummy_grant(); + grantees.push(GrantInformation { + grant, + expired: false, + }); + } + + grantees.sort_by_key(|g| g.grant.grantee.clone()); + grantees + } + + #[test] + fn obeys_limits() { + let mut test = init_contract_tester(); + let _grantees = grants_sorted(&mut test, 1000); + + let limit = 42; + let page1 = query_grants_paged(test.deps(), test.env(), Some(limit), None).unwrap(); + assert_eq!(page1.grants.len(), limit as usize); + } + + #[test] + fn has_default_limit() { + let mut test = init_contract_tester(); + let _grantees = grants_sorted(&mut test, 1000); + + // query without explicitly setting a limit + let page1 = query_grants_paged(test.deps(), test.env(), None, None).unwrap(); + assert_eq!( + page1.grants.len() as u32, + retrieval_limits::GRANTS_DEFAULT_LIMIT + ); + } + + #[test] + fn has_max_limit() { + let mut test = init_contract_tester(); + let _grantees = grants_sorted(&mut test, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = + query_grants_paged(test.deps(), test.env(), Some(crazy_limit), None).unwrap(); + + assert_eq!( + page1.grants.len() as u32, + retrieval_limits::GRANTS_MAX_LIMIT + ); + } + + #[test] + fn pagination_works() { + let mut test = init_contract_tester(); + let grants = grants_sorted(&mut test, 1000); + + // first page should return 2 results... + let page1 = query_grants_paged(test.deps(), test.env(), Some(2), None).unwrap(); + assert_eq!(page1.grants, grants[..2].to_vec()); + + // if we start after 5th entry, the returned following page should have 6th and onwards + let second = grants[1].clone(); + + let page2 = query_grants_paged( + test.deps(), + test.env(), + Some(3), + Some(second.grant.grantee.to_string()), + ) + .unwrap(); + assert_eq!(page2.grants, grants[2..5].to_vec()); + } + } +} diff --git a/contracts/nym-pool/src/queued_migrations.rs b/contracts/nym-pool/src/queued_migrations.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/contracts/nym-pool/src/queued_migrations.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/nym-pool/src/storage.rs b/contracts/nym-pool/src/storage.rs new file mode 100644 index 0000000000..9d01d5796f --- /dev/null +++ b/contracts/nym-pool/src/storage.rs @@ -0,0 +1,2343 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::validate_usage_coin; +use cosmwasm_std::{coin, Addr, Coin, Deps, DepsMut, Env, Storage, Uint128}; +use cw_controllers::Admin; +use cw_storage_plus::{Item, Map}; +use nym_pool_contract_common::constants::storage_keys; +use nym_pool_contract_common::{ + Allowance, Grant, GranteeAddress, GranterAddress, GranterInformation, NymPoolContractError, +}; +use std::cmp::max; +use std::collections::HashMap; + +pub const NYM_POOL_STORAGE: NymPoolStorage = NymPoolStorage::new(); + +pub struct NymPoolStorage { + pub(crate) contract_admin: Admin, + pub(crate) pool_denomination: Item, + pub(crate) granters: Map, + + // pub(crate) expired: (), + + // unlike the feegrant module, we specifically don't allow multiple grants (from different granters) + // towards the same grantee + pub(crate) grants: Map, + pub(crate) locked: LockedStorage, +} + +impl NymPoolStorage { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + NymPoolStorage { + contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), + pool_denomination: Item::new(storage_keys::POOL_DENOMINATION), + granters: Map::new(storage_keys::GRANTERS), + grants: Map::new(storage_keys::GRANTS), + locked: LockedStorage::new(), + } + } + + pub fn initialise( + &self, + mut deps: DepsMut, + env: Env, + admin: Addr, + pool_denom: &String, + initial_grants: HashMap, + ) -> Result<(), NymPoolContractError> { + // set the denom + self.pool_denomination.save(deps.storage, pool_denom)?; + + // set the contract admin + self.contract_admin + .set(deps.branch(), Some(admin.clone()))?; + + // set the admin to be a whitelisted granter + self.add_new_granter(deps.branch(), &env, &admin, &admin)?; + + // initialise the locked storage (with the total of 0) + self.locked.initialise(deps.branch())?; + + let included_grants = !initial_grants.is_empty(); + + // add all initial grants + let mut required_amount = Uint128::zero(); + for (grantee, allowance) in initial_grants { + let grantee = deps.api.addr_validate(&grantee)?; + if let Some(ref limit) = allowance.basic().spend_limit { + required_amount += limit.amount; + } + self.insert_new_grant(deps.branch(), &env, &admin, &grantee, allowance)?; + } + + // special case: during initialisation, even if we're inserting unlimited grants, + // we have to have _some_ tokens available + if included_grants { + let balance = self.contract_balance(deps.as_ref(), &env)?; + if required_amount > balance.amount || balance.amount.is_zero() { + return Err(NymPoolContractError::InsufficientTokens { + required: coin(max(required_amount.u128(), 1), &balance.denom), + available: balance, + }); + } + } + + Ok(()) + } + + fn contract_balance(&self, deps: Deps, env: &Env) -> Result { + let denom = self.pool_denomination.load(deps.storage)?; + Ok(deps.querier.query_balance(&env.contract.address, denom)?) + } + + fn is_admin(&self, deps: Deps, addr: &Addr) -> Result { + self.contract_admin.is_admin(deps, addr).map_err(Into::into) + } + + fn ensure_is_admin(&self, deps: Deps, addr: &Addr) -> Result<(), NymPoolContractError> { + self.contract_admin + .assert_admin(deps, addr) + .map_err(Into::into) + } + + pub fn try_load_granter( + &self, + deps: Deps, + granter: &GranterAddress, + ) -> Result, NymPoolContractError> { + self.granters + .may_load(deps.storage, granter.clone()) + .map_err(Into::into) + } + + fn is_whitelisted_granter( + &self, + deps: Deps, + addr: &GranterAddress, + ) -> Result { + Ok(self.try_load_granter(deps, addr)?.is_some()) + } + + fn ensure_is_whitelisted_granter( + &self, + deps: Deps, + addr: &GranterAddress, + ) -> Result<(), NymPoolContractError> { + if !self.is_whitelisted_granter(deps, addr)? { + return Err(NymPoolContractError::InvalidGranter { + addr: addr.to_string(), + }); + } + Ok(()) + } + + pub fn add_new_granter( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + granter: &GranterAddress, + ) -> Result<(), NymPoolContractError> { + // currently only the admin is permitted to add new granters + self.ensure_is_admin(deps.as_ref(), sender)?; + + if self + .granters + .may_load(deps.storage, granter.clone())? + .is_some() + { + return Err(NymPoolContractError::AlreadyAGranter); + } + + self.granters.save( + deps.storage, + granter.clone(), + &GranterInformation { + created_by: sender.clone(), + created_at_height: env.block.height, + }, + )?; + + Ok(()) + } + + pub fn remove_granter( + &self, + deps: DepsMut, + admin: &Addr, + granter: &GranterAddress, + ) -> Result<(), NymPoolContractError> { + // only admin is permitted to remove granters + self.ensure_is_admin(deps.as_ref(), admin)?; + + // the granter has to be, well, an actual granter + self.ensure_is_whitelisted_granter(deps.as_ref(), granter)?; + + self.granters.remove(deps.storage, granter.clone()); + + Ok(()) + } + + pub fn available_tokens(&self, deps: Deps, env: &Env) -> Result { + let locked = self.locked.total_locked.load(deps.storage)?; + let balance = self.contract_balance(deps, env)?; + + // the amount of available tokens is the current contract balance minus all the locked tokens + let mut available = balance; + available.amount = available.amount.saturating_sub(locked); + Ok(available) + } + + pub fn try_load_grant( + &self, + deps: Deps, + grantee: &GranteeAddress, + ) -> Result, NymPoolContractError> { + self.grants + .may_load(deps.storage, grantee.clone()) + .map_err(Into::into) + } + + pub fn load_grant( + &self, + deps: Deps, + grantee: &GranteeAddress, + ) -> Result { + self.try_load_grant(deps, grantee)? + .ok_or(NymPoolContractError::GrantNotFound { + grantee: grantee.to_string(), + }) + } + + pub fn insert_new_grant( + &self, + deps: DepsMut, + env: &Env, + granter: &GranterAddress, + grantee: &GranteeAddress, + mut allowance: Allowance, + ) -> Result<(), NymPoolContractError> { + // the granter should be permitted to add new grants + self.ensure_is_whitelisted_granter(deps.as_ref(), granter)?; + + // check for existing grant + if let Some(existing_grant) = self.try_load_grant(deps.as_ref(), grantee)? { + return Err(NymPoolContractError::GrantAlreadyExist { + granter: existing_grant.granter.to_string(), + grantee: grantee.to_string(), + created_at_height: existing_grant.granted_at_height, + }); + } + + // the allowance should be well-formed + let expected_denom = self.pool_denomination.load(deps.storage)?; + allowance.validate_new(env, &expected_denom)?; + + // if allowance includes explicit limit, + // it should not be higher than the total remaining tokens + // note: we already verified denomination matched when we validated the allowance + if let Some(ref spend_limit) = allowance.basic().spend_limit { + let available = self.available_tokens(deps.as_ref(), env)?; + if spend_limit.amount > available.amount { + return Err(NymPoolContractError::InsufficientTokens { + available, + required: spend_limit.clone(), + }); + } + } + + // set initial state based on the env + allowance.set_initial_state(env); + + self.grants.save( + deps.storage, + grantee.clone(), + &Grant { + granter: granter.clone(), + grantee: grantee.clone(), + granted_at_height: env.block.height, + allowance, + }, + )?; + + Ok(()) + } + + pub fn try_spend_part_of_grant( + &self, + deps: DepsMut, + env: &Env, + grantee_address: &GranteeAddress, + amount: &Coin, + ) -> Result<(), NymPoolContractError> { + let mut grant = self.load_grant(deps.as_ref(), grantee_address)?; + grant.allowance.try_spend(env, amount)?; + + let locked = self.locked.grantee_locked(deps.storage, grantee_address)?; + + // if we used up all allowance and have no locked tokens, we can just remove the grant from storage + if grant.allowance.is_used_up() && locked.is_zero() { + self.grants.remove(deps.storage, grantee_address.clone()) + } else { + self.grants + .save(deps.storage, grantee_address.clone(), &grant)?; + } + + Ok(()) + } + + pub fn remove_grant( + &self, + deps: DepsMut, + grantee_address: &GranteeAddress, + ) -> Result<(), NymPoolContractError> { + self.grants.remove(deps.storage, grantee_address.clone()); + + // if there are any tokens still locked associated with this grantee, unlock them + if let Some(grantee_locked) = self + .locked + .maybe_grantee_locked(deps.storage, grantee_address)? + { + self.locked.unlock(deps, grantee_address, grantee_locked)?; + } + + Ok(()) + } + + pub fn revoke_grant( + &self, + deps: DepsMut, + grantee_address: &GranteeAddress, + revoker: &Addr, + ) -> Result<(), NymPoolContractError> { + let grant = self.load_grant(deps.as_ref(), grantee_address)?; + let original_granter = grant.granter; + + let is_admin = self.is_admin(deps.as_ref(), revoker)?; + + // grant can only be revoked by the granter who has originally granted it (assuming it's still whitelisted) + // or by the admin + if revoker != original_granter && !is_admin { + // request came from a random sender - neither the original granter nor the current admin + return Err(NymPoolContractError::UnauthorizedGrantRevocation); + } + + // at this point we know the request must have come from either the original granter or contract admin, + // however, if it was the former, we still need to verify whether it's still whitelisted + // (if the granter was removed, it shouldn't have any permissions to modify old grants anymore) + if !is_admin && !self.is_whitelisted_granter(deps.as_ref(), revoker)? { + return Err(NymPoolContractError::UnauthorizedGrantRevocation); + } + + self.remove_grant(deps, grantee_address) + } + + pub fn lock_part_of_allowance( + &self, + mut deps: DepsMut, + env: &Env, + grantee: &GranteeAddress, + amount: Coin, + ) -> Result<(), NymPoolContractError> { + // ensure correct coin has been specified + validate_usage_coin(deps.storage, &amount)?; + + // keep track of the locked coins + self.locked.lock(deps.branch(), grantee, amount.amount)?; + + // attempt to deduct the coins from the allowance + self.try_spend_part_of_grant(deps, env, grantee, &amount)?; + + Ok(()) + } + + pub fn unlock_part_of_allowance( + &self, + deps: DepsMut, + grantee: &GranteeAddress, + amount: &Coin, + ) -> Result<(), NymPoolContractError> { + // ensure correct coin has been specified + validate_usage_coin(deps.storage, amount)?; + + // update the underlying spend limit of the grant + let mut grant = self.load_grant(deps.as_ref(), grantee)?; + // note: this will only increase the basic spend limit and will not change any periodic allowances + grant.allowance.increase_spend_limit(amount.amount); + self.grants.save(deps.storage, grantee.clone(), &grant)?; + + // keep track of the locked coins (also checks whether sufficient tokens are locked, etc.) + self.locked.unlock(deps, grantee, amount.amount) + } +} + +pub(crate) struct LockedStorage { + pub(crate) total_locked: Item, + pub(crate) grantees: Map, +} + +impl LockedStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + LockedStorage { + total_locked: Item::new(storage_keys::TOTAL_LOCKED), + grantees: Map::new(storage_keys::LOCKED_GRANTEES), + } + } + + fn initialise(&self, deps: DepsMut) -> Result<(), NymPoolContractError> { + self.total_locked.save(deps.storage, &Uint128::zero())?; + Ok(()) + } + + pub fn grantee_locked( + &self, + storage: &dyn Storage, + grantee: &GranteeAddress, + ) -> Result { + Ok(self + .maybe_grantee_locked(storage, grantee)? + .unwrap_or_default()) + } + + pub fn maybe_grantee_locked( + &self, + storage: &dyn Storage, + grantee: &GranteeAddress, + ) -> Result, NymPoolContractError> { + Ok(self.grantees.may_load(storage, grantee.clone())?) + } + + /// unconditionally attempts to load specified amount of tokens for the particular grantee + /// it does not validate permissions nor allowances - that's up to the caller + fn lock( + &self, + deps: DepsMut, + grantee: &GranteeAddress, + amount: Uint128, + ) -> Result<(), NymPoolContractError> { + let existing_grantee = self.grantee_locked(deps.storage, grantee)?; + let new_locked_grantee = existing_grantee + amount; + + let existing_total = self.total_locked.load(deps.storage)?; + let new_locked_total = existing_total + amount; + + self.grantees + .save(deps.storage, grantee.clone(), &new_locked_grantee)?; + self.total_locked.save(deps.storage, &new_locked_total)?; + Ok(()) + } + + fn unlock( + &self, + deps: DepsMut, + grantee: &GranteeAddress, + amount: Uint128, + ) -> Result<(), NymPoolContractError> { + let locked_grantee = self.grantee_locked(deps.storage, grantee)?; + let total_locked = self.total_locked.load(deps.storage)?; + + if locked_grantee < amount { + return Err(NymPoolContractError::InsufficientLockedTokens { + grantee: grantee.to_string(), + locked: locked_grantee, + requested: amount, + }); + } + + let updated_grantee = locked_grantee - amount; + + // if the updated value is zero, just remove the map entry + if updated_grantee.is_zero() { + self.grantees.remove(deps.storage, grantee.clone()); + } else { + self.grantees + .save(deps.storage, grantee.clone(), &updated_grantee)?; + } + + // we're specifically not using saturating sub here because that operation should ALWAYS be valid + // if it fails, it means there's a pool inconsistency that has to be resolved + self.total_locked + .save(deps.storage, &(total_locked - amount))?; + + Ok(()) + } +} + +pub mod retrieval_limits { + pub const LOCKED_TOKENS_DEFAULT_LIMIT: u32 = 100; + pub const LOCKED_TOKENS_MAX_LIMIT: u32 = 200; + + pub const GRANTERS_DEFAULT_LIMIT: u32 = 100; + pub const GRANTERS_MAX_LIMIT: u32 = 200; + + pub const GRANTS_DEFAULT_LIMIT: u32 = 100; + pub const GRANTS_MAX_LIMIT: u32 = 200; +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_pool_contract_common::BasicAllowance; + + fn dummy_allowance() -> Allowance { + Allowance::Basic(BasicAllowance::unlimited()) + } + + #[cfg(test)] + mod nympool_storage { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt, TEST_DENOM}; + use cosmwasm_std::testing::{ + mock_dependencies, mock_env, MockApi, MockQuerier, MockStorage, + }; + use cosmwasm_std::{coin, coins, Empty, OwnedDeps}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + use nym_pool_contract_common::BasicAllowance; + + #[cfg(test)] + mod initialisation { + use super::*; + use crate::testing::TEST_DENOM; + use cosmwasm_std::testing::{mock_dependencies, mock_env}; + use cosmwasm_std::{coin, Order}; + use nym_contracts_common_testing::deps_with_balance; + use nym_pool_contract_common::BasicAllowance; + + fn all_grants(storage: &dyn Storage) -> HashMap { + NYM_POOL_STORAGE + .grants + .range(storage, None, None, Order::Ascending) + .collect::, _>>() + .unwrap() + } + + #[test] + fn requires_some_tokens_for_unlimited_initial_grants() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + + let mut grants = HashMap::new(); + grants.insert( + deps.api.addr_make("gr1").to_string(), + Allowance::Basic(BasicAllowance::unlimited()), + ); + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &TEST_DENOM.to_string(), + grants.clone(), + ); + // we haven't got any tokens - this should have failed + assert!(res.is_err()); + + let address = &env.contract.address; + let mem_storage = MockStorage::default(); + let api = MockApi::default(); + + let querier: MockQuerier = + MockQuerier::new(&[(address.as_str(), coins(1, TEST_DENOM).as_slice())]); + + let mut deps: OwnedDeps<_, _, _, Empty> = OwnedDeps { + storage: mem_storage, + api, + querier, + custom_query_type: Default::default(), + }; + + // while we don't have a lot, we have some tokens which should allow this tx to proceed + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &TEST_DENOM.to_string(), + grants.clone(), + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn requires_specified_amount_of_tokens_for_bounded_grants() -> anyhow::Result<()> { + fn bounded_allowance() -> Allowance { + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(100, TEST_DENOM)), + expiration_unix_timestamp: None, + }) + } + + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + + let mut grants = HashMap::new(); + grants.insert(deps.api.addr_make("gr1").to_string(), bounded_allowance()); + grants.insert(deps.api.addr_make("gr2").to_string(), bounded_allowance()); + grants.insert(deps.api.addr_make("gr3").to_string(), bounded_allowance()); + grants.insert(deps.api.addr_make("gr4").to_string(), bounded_allowance()); + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &TEST_DENOM.to_string(), + grants.clone(), + ); + // we haven't got any tokens - this should have failed + assert!(res.is_err()); + + let address = &env.contract.address; + let mem_storage = MockStorage::default(); + let api = MockApi::default(); + + let querier: MockQuerier = + MockQuerier::new(&[(address.as_str(), coins(399, TEST_DENOM).as_slice())]); + + let mut deps: OwnedDeps<_, _, _, Empty> = OwnedDeps { + storage: mem_storage, + api, + querier, + custom_query_type: Default::default(), + }; + + // we haven't got enough tokens (we need at least 400) - still a failure! + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &TEST_DENOM.to_string(), + grants.clone(), + ); + assert!(res.is_err()); + + // finally, with at least 400, it should work + let mem_storage = MockStorage::default(); + let api = MockApi::default(); + + let querier: MockQuerier = + MockQuerier::new(&[(address.as_str(), coins(400, TEST_DENOM).as_slice())]); + + let mut deps: OwnedDeps<_, _, _, Empty> = OwnedDeps { + storage: mem_storage, + api, + querier, + custom_query_type: Default::default(), + }; + + let res = storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &TEST_DENOM.to_string(), + grants, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn inserts_all_initial_grants() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let env = mock_env(); + + let mut deps = deps_with_balance(&env); + + let admin = deps.api.addr_make("admin"); + let denom = &TEST_DENOM.to_string(); + + // no grants + let grants = HashMap::new(); + storage.initialise(deps.as_mut(), env.clone(), admin.clone(), denom, grants)?; + assert!(all_grants(&deps.storage).is_empty()); + + // one grant + let mut deps = deps_with_balance(&env); + let mut grants = HashMap::new(); + grants.insert(deps.api.addr_make("gr1").to_string(), dummy_allowance()); + storage.initialise(deps.as_mut(), env.clone(), admin.clone(), denom, grants)?; + let all = all_grants(&deps.storage); + assert_eq!(all.len(), 1); + let grant = all.get(&deps.api.addr_make("gr1")).unwrap(); + assert_eq!(grant.grantee, deps.api.addr_make("gr1")); + assert_eq!(grant.allowance, dummy_allowance()); + + // multiple grants + let mut deps = deps_with_balance(&env); + let mut grants = HashMap::new(); + grants.insert(deps.api.addr_make("gr1").to_string(), dummy_allowance()); + grants.insert(deps.api.addr_make("gr2").to_string(), dummy_allowance()); + grants.insert(deps.api.addr_make("gr3").to_string(), dummy_allowance()); + grants.insert(deps.api.addr_make("gr4").to_string(), dummy_allowance()); + storage.initialise(deps.as_mut(), env.clone(), admin.clone(), denom, grants)?; + let all = all_grants(&deps.storage); + assert_eq!(all.len(), 4); + let grant = all.get(&deps.api.addr_make("gr1")).unwrap(); + assert_eq!(grant.grantee, deps.api.addr_make("gr1")); + let grant = all.get(&deps.api.addr_make("gr3")).unwrap(); + assert_eq!(grant.grantee, deps.api.addr_make("gr3")); + + // fails on invalid grantee address + let mut deps = deps_with_balance(&env); + let mut grants = HashMap::new(); + grants.insert(deps.api.addr_make("gr1").to_string(), dummy_allowance()); + grants.insert("invalid_address".to_string(), dummy_allowance()); + assert!(storage + .initialise(deps.as_mut(), env.clone(), admin.clone(), denom, grants) + .is_err()); + + // fails on invalid allowance + let mut deps = deps_with_balance(&env); + let mut grants = HashMap::new(); + grants.insert( + deps.api.addr_make("gr1").to_string(), + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(0, TEST_DENOM)), + expiration_unix_timestamp: None, + }), + ); + assert!(storage + .initialise(deps.as_mut(), env.clone(), admin, denom, grants) + .is_err()); + Ok(()) + } + + #[test] + fn sets_pool_denomination() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &"somedenom".to_string(), + HashMap::new(), + )?; + assert_eq!( + storage.pool_denomination.load(deps.as_ref().storage)?, + "somedenom".to_string() + ); + + let mut deps = mock_dependencies(); + storage.initialise( + deps.as_mut(), + env.clone(), + admin.clone(), + &"anotherdenom".to_string(), + HashMap::new(), + )?; + assert_eq!( + storage.pool_denomination.load(deps.as_ref().storage)?, + "anotherdenom".to_string() + ); + + Ok(()) + } + + #[test] + fn sets_contract_admin() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin1 = deps.api.addr_make("first-admin"); + let admin2 = deps.api.addr_make("secod-admin"); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin1.clone(), + &TEST_DENOM.to_string(), + HashMap::new(), + )?; + assert!(storage.ensure_is_admin(deps.as_ref(), &admin1).is_ok()); + + let mut deps = mock_dependencies(); + storage.initialise( + deps.as_mut(), + env.clone(), + admin2.clone(), + &TEST_DENOM.to_string(), + HashMap::new(), + )?; + assert!(storage.ensure_is_admin(deps.as_ref(), &admin2).is_ok()); + + Ok(()) + } + + #[test] + fn initialises_locked_storage() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + let denom = &TEST_DENOM.to_string(); + + storage.initialise(deps.as_mut(), env, admin, denom, HashMap::new())?; + assert!(storage + .locked + .total_locked + .load(deps.as_ref().storage)? + .is_zero()); + + Ok(()) + } + + #[test] + fn adds_admin_to_the_granters_set() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin1 = deps.api.addr_make("first-admin"); + let admin2 = deps.api.addr_make("second-admin"); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin1.clone(), + &TEST_DENOM.to_string(), + HashMap::new(), + )?; + assert_eq!( + storage + .granters + .load(&deps.storage, admin1.clone())? + .created_by, + admin1 + ); + + let mut deps = mock_dependencies(); + storage.initialise( + deps.as_mut(), + env.clone(), + admin2.clone(), + &TEST_DENOM.to_string(), + HashMap::new(), + )?; + assert_eq!( + storage + .granters + .load(&deps.storage, admin2.clone())? + .created_by, + admin2 + ); + + Ok(()) + } + } + + #[test] + fn getting_contract_balance() -> anyhow::Result<()> { + // it's a simple as running the bank query against the address set in the current env + // using the pool denom + let env = mock_env(); + let address = &env.contract.address; + + let storage = MockStorage::default(); + let api = MockApi::default(); + + let not_contract = api.addr_make("unrelated-address"); + let pool_denom = TEST_DENOM; + + // set some initial balances + let querier: MockQuerier = MockQuerier::new(&[ + ( + address.as_str(), + vec![coin(1000, pool_denom), coin(2000, "anotherdenom")].as_slice(), + ), + (not_contract.as_str(), coins(1234, pool_denom).as_slice()), + ]); + + let mut deps: OwnedDeps<_, _, _, Empty> = OwnedDeps { + storage, + api, + querier, + custom_query_type: Default::default(), + }; + let storage = NymPoolStorage::new(); + let admin = deps.api.addr_make("admin"); + + // regardless of other denoms and other accounts, the balance query only returns target denom + storage.initialise( + deps.as_mut(), + env.clone(), + admin, + &pool_denom.to_string(), + HashMap::new(), + )?; + assert_eq!( + storage.contract_balance(deps.as_ref(), &env)?, + coin(1000, pool_denom) + ); + + Ok(()) + } + + #[test] + fn checking_for_admin() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + let non_admin = deps.api.addr_make("non-admin"); + let denom = &TEST_DENOM.to_string(); + + storage.initialise(deps.as_mut(), env, admin.clone(), denom, HashMap::new())?; + assert!(storage.is_admin(deps.as_ref(), &admin)?); + assert!(!storage.is_admin(deps.as_ref(), &non_admin)?); + + Ok(()) + } + + #[test] + fn ensuring_admin_privileges() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut deps = mock_dependencies(); + let env = mock_env(); + let admin = deps.api.addr_make("admin"); + let non_admin = deps.api.addr_make("non-admin"); + let denom = &TEST_DENOM.to_string(); + + storage.initialise(deps.as_mut(), env, admin.clone(), denom, HashMap::new())?; + assert!(storage.ensure_is_admin(deps.as_ref(), &admin).is_ok()); + assert!(storage.ensure_is_admin(deps.as_ref(), &non_admin).is_err()); + + Ok(()) + } + + #[test] + fn loading_granter_information() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut test = init_contract_tester(); + + let granter = test.generate_account(); + + // not granter + let info = storage.try_load_granter(test.deps(), &granter)?; + assert!(info.is_none()); + + // granter + let admin = test.admin_unchecked(); + test.add_granter(&granter); + + let info = storage.try_load_granter(test.deps(), &granter)?; + assert_eq!( + info, + Some(GranterInformation { + created_by: admin, + created_at_height: test.env().block.height, + }) + ); + + Ok(()) + } + + #[test] + fn checking_granter_permission() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut test = init_contract_tester(); + + let granter = test.generate_account(); + test.add_granter(&granter); + let not_granter = test.generate_account(); + + let deps = test.deps(); + assert!(storage.is_whitelisted_granter(deps, &granter)?); + assert!(!storage.is_whitelisted_granter(deps, ¬_granter)?); + + Ok(()) + } + + #[test] + fn ensuring_granter_permission() -> anyhow::Result<()> { + let storage = NymPoolStorage::new(); + let mut test = init_contract_tester(); + + let granter = test.generate_account(); + test.add_granter(&granter); + let not_granter = test.generate_account(); + + let deps = test.deps(); + assert!(storage + .ensure_is_whitelisted_granter(deps, &granter) + .is_ok()); + assert!(storage + .ensure_is_whitelisted_granter(deps, ¬_granter) + .is_err()); + + Ok(()) + } + + #[test] + fn checking_available_tokens() -> anyhow::Result<()> { + // initialise the contract with some tokens + let env = mock_env(); + let address = &env.contract.address; + + let storage = MockStorage::default(); + let api = MockApi::default(); + let pool_denom = TEST_DENOM; + + // set some initial balances + let querier: MockQuerier = + MockQuerier::new(&[(address.as_str(), coins(1000, pool_denom).as_slice())]); + + let mut deps: OwnedDeps<_, _, _, Empty> = OwnedDeps { + storage, + api, + querier, + custom_query_type: Default::default(), + }; + let storage = NymPoolStorage::new(); + let admin = deps.api.addr_make("admin"); + + storage.initialise( + deps.as_mut(), + env.clone(), + admin, + &pool_denom.to_string(), + HashMap::new(), + )?; + + // if there are no locked tokens, the available equals to the balance + assert_eq!( + storage.available_tokens(deps.as_ref(), &env)?, + coin(1000, pool_denom) + ); + + // however, once we start locking them, it becomes the difference between those + + // some locked + let dummy_grantee = deps.api.addr_make("grantee"); + storage + .locked + .lock(deps.as_mut(), &dummy_grantee, Uint128::new(100))?; + + assert_eq!( + storage.available_tokens(deps.as_ref(), &env)?, + coin(900, pool_denom) + ); + + // all locked + storage + .locked + .lock(deps.as_mut(), &dummy_grantee, Uint128::new(900))?; + assert_eq!( + storage.available_tokens(deps.as_ref(), &env)?, + coin(0, pool_denom) + ); + + // locked beyond balance (to check for overflow errors) + storage + .locked + .lock(deps.as_mut(), &dummy_grantee, Uint128::new(1000000))?; + assert_eq!( + storage.available_tokens(deps.as_ref(), &env)?, + coin(0, pool_denom) + ); + + Ok(()) + } + + #[test] + fn attempting_to_load_grant() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + // doesn't exist... + let grantee = test.generate_account(); + assert_eq!(storage.try_load_grant(test.deps(), &grantee)?, None); + + // exists + test.add_dummy_grant_for(&grantee); + assert_eq!( + storage.try_load_grant(test.deps(), &grantee)?, + Some(Grant { + granter: test.admin_unchecked(), + grantee, + granted_at_height: test.env().block.height, + allowance: Allowance::Basic(BasicAllowance::unlimited()), + }) + ); + Ok(()) + } + + #[test] + fn loading_grant() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + // doesn't exist... + let grantee = test.generate_account(); + assert!(storage.load_grant(test.deps(), &grantee).is_err()); + + // exists + test.add_dummy_grant_for(&grantee); + assert_eq!( + storage.load_grant(test.deps(), &grantee)?, + Grant { + granter: test.admin_unchecked(), + grantee, + granted_at_height: test.env().block.height, + allowance: Allowance::Basic(BasicAllowance::unlimited()), + } + ); + Ok(()) + } + + #[cfg(test)] + mod adding_new_granter { + use super::*; + use crate::testing::init_contract_tester; + use cw_controllers::AdminError; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let not_admin = test.generate_account(); + + let granter = test.generate_account(); + + let (deps, env) = test.deps_mut_env(); + let res = storage + .add_new_granter(deps, &env, ¬_admin, &granter) + .unwrap_err(); + assert_eq!(res, NymPoolContractError::Admin(AdminError::NotAdmin {})); + + let (deps, env) = test.deps_mut_env(); + let res = storage.add_new_granter(deps, &env, &admin, &granter); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn can_only_be_performed_if_account_is_not_already_a_granter() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let granter = test.generate_account(); + + // adding it for the first time... + let (deps, env) = test.deps_mut_env(); + storage.add_new_granter(deps, &env, &admin, &granter)?; + + // it's already a granter + let (deps, env) = test.deps_mut_env(); + let res = storage + .add_new_granter(deps, &env, &admin, &granter) + .unwrap_err(); + assert_eq!(res, NymPoolContractError::AlreadyAGranter); + + Ok(()) + } + + #[test] + fn saves_basic_metadata() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let granter = test.generate_account(); + + // no metadata + let info = storage.granters.may_load(test.storage(), granter.clone())?; + assert!(info.is_none()); + + let (deps, env) = test.deps_mut_env(); + storage.add_new_granter(deps, &env, &admin, &granter)?; + + let info = storage.granters.may_load(test.storage(), granter.clone())?; + // it was added by the admin at the current height + assert_eq!( + info, + Some(GranterInformation { + created_by: admin.clone(), + created_at_height: env.block.height, + }) + ); + + // after changing admin, new address is set as the creator + let new_admin = test.generate_account(); + // sanity check: + assert_ne!(admin, new_admin); + test.change_admin(&new_admin); + + let new_granter = test.generate_account(); + let (deps, env) = test.deps_mut_env(); + storage.add_new_granter(deps, &env, &new_admin, &new_granter)?; + let info = storage + .granters + .may_load(test.storage(), new_granter.clone())?; + // it was added by the new admin at the current height + assert_eq!( + info, + Some(GranterInformation { + created_by: new_admin.clone(), + created_at_height: env.block.height, + }) + ); + + Ok(()) + } + } + + #[cfg(test)] + mod removing_granter { + use super::*; + use crate::testing::init_contract_tester; + + #[test] + fn requires_granter_to_exist() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let granter = test.generate_account(); + + assert!(storage + .remove_granter(test.deps_mut(), &admin, &granter) + .is_err()); + + test.add_granter(&granter); + assert!(storage + .remove_granter(test.deps_mut(), &admin, &granter) + .is_ok()); + + Ok(()) + } + + #[test] + fn can_only_be_performed_by_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let random_address = test.generate_account(); + let granter = test.generate_account(); + let admin = test.admin_unchecked(); + test.add_granter(&granter); + + // can't be removed by the granter itself + assert!(storage + .remove_granter(test.deps_mut(), &granter, &granter) + .is_err()); + + // not by some random address + assert!(storage + .remove_granter(test.deps_mut(), &random_address, &granter) + .is_err()); + + // admin can do it though! + assert!(storage + .remove_granter(test.deps_mut(), &admin, &granter) + .is_ok()); + + test.add_granter(&granter); + let new_admin = test.generate_account(); + test.change_admin(&new_admin); + + // old admin can't do anything : ( + assert!(storage + .remove_granter(test.deps_mut(), &admin, &granter) + .is_err()); + + // but new admin can! + assert!(storage + .remove_granter(test.deps_mut(), &new_admin, &granter) + .is_ok()); + + Ok(()) + } + + #[test] + fn removes_it_from_granter_list() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let granter = test.generate_account(); + test.add_granter(&granter); + + assert!(storage + .granters + .may_load(test.storage(), granter.clone())? + .is_some()); + + storage.remove_granter(test.deps_mut(), &admin, &granter)?; + + assert!(storage + .granters + .may_load(test.storage(), granter.clone())? + .is_none()); + Ok(()) + } + } + + #[cfg(test)] + mod adding_new_grant { + use super::*; + use crate::testing::init_contract_tester; + use nym_pool_contract_common::ClassicPeriodicAllowance; + + #[test] + fn can_only_be_done_by_whitelisted_granter() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let not_valid_granter = test.generate_account(); + let granter = test.generate_account(); + test.add_granter(&granter); + + let grantee = test.generate_account(); + + let (deps, env) = test.deps_mut_env(); + let res = storage + .insert_new_grant(deps, &env, ¬_valid_granter, &grantee, dummy_allowance()) + .unwrap_err(); + + assert_eq!( + res, + NymPoolContractError::InvalidGranter { + addr: not_valid_granter.to_string(), + } + ); + + let (deps, env) = test.deps_mut_env(); + let res = + storage.insert_new_grant(deps, &env, &granter, &grantee, dummy_allowance()); + + assert!(res.is_ok()); + Ok(()) + } + + #[test] + fn cant_be_done_if_grant_already_existed() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.add_dummy_grant().grantee; + + let (deps, env) = test.deps_mut_env(); + let res = storage + .insert_new_grant(deps, &env, &admin, &grantee, dummy_allowance()) + .unwrap_err(); + + assert!(matches!( + res, + NymPoolContractError::GrantAlreadyExist { .. } + )); + + Ok(()) + } + + #[test] + fn only_accepts_valid_allowances() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + // allowance with 0 limit and wrong denom + let bad_allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(0, "invalid-denom")), + expiration_unix_timestamp: None, + }); + + let admin = test.admin_unchecked(); + let grantee = test.generate_account(); + + let (deps, env) = test.deps_mut_env(); + let res = storage + .insert_new_grant(deps, &env, &admin, &grantee, bad_allowance) + .unwrap_err(); + + assert!(matches!(res, NymPoolContractError::InvalidDenom { .. })); + + Ok(()) + } + + #[test] + fn explicit_limit_cant_be_larger_than_available_tokens() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.generate_account(); + + let available = storage.available_tokens(test.deps(), &test.env())?; + + // just above the available + let mut limit = available.clone(); + limit.amount += Uint128::new(1); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(limit), + expiration_unix_timestamp: None, + }); + + let (deps, env) = test.deps_mut_env(); + let res = storage + .insert_new_grant(deps, &env, &admin, &grantee, allowance) + .unwrap_err(); + + assert!(matches!( + res, + NymPoolContractError::InsufficientTokens { .. } + )); + + // equal to the available + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(available.clone()), + expiration_unix_timestamp: None, + }); + + let (deps, env) = test.deps_mut_env(); + let res = storage.insert_new_grant(deps, &env, &admin, &grantee, allowance); + assert!(res.is_ok()); + + // and below the available + let mut test = init_contract_tester(); + let mut limit = available.clone(); + limit.amount -= Uint128::new(1); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(limit), + expiration_unix_timestamp: None, + }); + + let (deps, env) = test.deps_mut_env(); + let res = storage.insert_new_grant(deps, &env, &admin, &grantee, allowance); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn updates_allowances_initial_state_and_saves_it_to_storage() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.generate_account(); + + let inner = ClassicPeriodicAllowance { + basic: BasicAllowance::unlimited(), + period_duration_secs: 3600, + period_spend_limit: coin(100, TEST_DENOM), + period_can_spend: None, + period_reset_unix_timestamp: 0, + }; + let allowance = Allowance::ClassicPeriodic(inner.clone()); + + let (deps, env) = test.deps_mut_env(); + let res = storage.insert_new_grant(deps, &env, &admin, &grantee, allowance); + assert!(res.is_ok()); + + let stored_grant = storage.load_grant(test.deps(), &grantee)?; + let mut expected_inner = inner; + expected_inner.period_can_spend = Some(expected_inner.period_spend_limit.clone()); + expected_inner.period_reset_unix_timestamp = + env.block.time.seconds() + expected_inner.period_duration_secs; + let expected = Allowance::ClassicPeriodic(expected_inner); + + assert_eq!(stored_grant.allowance, expected); + assert_eq!(stored_grant.grantee, grantee); + assert_eq!(stored_grant.granter, admin); + assert_eq!(stored_grant.granted_at_height, env.block.height); + + Ok(()) + } + } + + #[cfg(test)] + mod spending_part_of_grant { + use super::*; + use crate::testing::init_contract_tester; + + #[test] + fn requires_grant_to_exist() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let grantee = test.generate_account(); + + let (deps, env) = test.deps_mut_env(); + let res = storage + .try_spend_part_of_grant(deps, &env, &grantee, &coin(100, TEST_DENOM)) + .unwrap_err(); + + assert!(matches!(res, NymPoolContractError::GrantNotFound { .. })); + + test.add_dummy_grant_for(&grantee); + assert!(storage + .try_spend_part_of_grant( + test.deps_mut(), + &env, + &grantee, + &coin(100, TEST_DENOM) + ) + .is_ok()); + Ok(()) + } + + #[test] + fn requires_grant_to_be_spendable() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.generate_account(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(100, TEST_DENOM)), + expiration_unix_timestamp: None, + }); + + let (deps, env) = test.deps_mut_env(); + storage.insert_new_grant(deps, &env, &admin, &grantee, allowance)?; + + let res = storage + .try_spend_part_of_grant( + test.deps_mut(), + &env, + &grantee, + &coin(200, TEST_DENOM), + ) + .unwrap_err(); + + assert_eq!(res, NymPoolContractError::SpendingAboveAllowance); + Ok(()) + } + + #[test] + fn updates_stored_grant() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.generate_account(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(100, TEST_DENOM)), + expiration_unix_timestamp: None, + }); + let (deps, env) = test.deps_mut_env(); + storage.insert_new_grant(deps, &env, &admin, &grantee, allowance)?; + + storage.try_spend_part_of_grant( + test.deps_mut(), + &env, + &grantee, + &coin(40, TEST_DENOM), + )?; + + let stored_grant = storage.load_grant(test.deps(), &grantee)?; + assert_eq!( + stored_grant.allowance, + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(60, TEST_DENOM)), + expiration_unix_timestamp: None, + }) + ); + + Ok(()) + } + + #[test] + fn removes_grant_from_storage_if_its_used_up() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee1 = test.generate_account(); + let grantee2 = test.generate_account(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(100, TEST_DENOM)), + expiration_unix_timestamp: None, + }); + let (deps, env) = test.deps_mut_env(); + storage.insert_new_grant(deps, &env, &admin, &grantee1, allowance.clone())?; + + let (deps, env) = test.deps_mut_env(); + storage.insert_new_grant(deps, &env, &admin, &grantee2, allowance)?; + + // use whole allowance with no locked tokens + storage.try_spend_part_of_grant( + test.deps_mut(), + &env, + &grantee1, + &coin(100, TEST_DENOM), + )?; + assert!(storage.try_load_grant(test.deps(), &grantee1)?.is_none()); + + // use whole allowance with some locked tokens + test.lock_allowance(grantee2.as_str(), Uint128::new(50)); + storage.try_spend_part_of_grant( + test.deps_mut(), + &env, + &grantee2, + &coin(50, TEST_DENOM), + )?; + + let stored_grant = storage.load_grant(test.deps(), &grantee2)?; + assert_eq!( + stored_grant.allowance, + Allowance::Basic(BasicAllowance { + spend_limit: Some(coin(0, TEST_DENOM)), + expiration_unix_timestamp: None, + }) + ); + + // unlock and attempt to spend again + storage.unlock_part_of_allowance( + test.deps_mut(), + &grantee2, + &coin(50, TEST_DENOM), + )?; + storage.try_spend_part_of_grant( + test.deps_mut(), + &env, + &grantee2, + &coin(50, TEST_DENOM), + )?; + assert!(storage.try_load_grant(test.deps(), &grantee2)?.is_none()); + + Ok(()) + } + } + + #[test] + fn removing_grant() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let grantee = test.generate_account(); + + // no-op if doesn't exist + assert!(storage.remove_grant(test.deps_mut(), &grantee).is_ok()); + + // removes the actual entry from the map + test.add_dummy_grant_for(&grantee); + + assert!(storage + .grants + .may_load(test.storage(), grantee.clone())? + .is_some()); + + assert!(storage.remove_grant(test.deps_mut(), &grantee).is_ok()); + + assert!(storage + .grants + .may_load(test.storage(), grantee.clone())? + .is_none()); + + // if applicable, unlocks any locked tokens + // (all the details of unlocking are already tested in different unit test(s), + // so it's sufficient to check any of those occurred) + let grantee2 = test.add_dummy_grant().grantee; + test.lock_allowance(grantee2.as_str(), Uint128::new(50)); + + assert!(storage.remove_grant(test.deps_mut(), &grantee2).is_ok()); + + assert!(storage + .locked + .grantees + .may_load(test.storage(), grantee2)? + .is_none()); + assert!(storage.locked.total_locked.load(test.storage())?.is_zero()); + + Ok(()) + } + + #[cfg(test)] + mod revoking_grant { + use super::*; + use crate::testing::init_contract_tester; + + #[test] + fn requires_grant_to_exist() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.generate_account(); + + assert_eq!( + storage + .revoke_grant(test.deps_mut(), &grantee, &admin) + .unwrap_err(), + NymPoolContractError::GrantNotFound { + grantee: grantee.to_string(), + } + ); + + test.add_dummy_grant_for(&grantee); + assert!(storage + .revoke_grant(test.deps_mut(), &grantee, &admin) + .is_ok()); + + Ok(()) + } + + #[test] + fn can_always_be_called_by_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let grantee = test.add_dummy_grant().grantee; + + // current admin + let admin = test.admin_unchecked(); + assert!(storage + .revoke_grant(test.deps_mut(), &grantee, &admin) + .is_ok()); + + // new admin + let new_admin = test.generate_account(); + let grantee = test.add_dummy_grant().grantee; + test.change_admin(&new_admin); + assert!(storage + .revoke_grant(test.deps_mut(), &grantee, &new_admin) + .is_ok()); + + // old admin + let grantee = test.add_dummy_grant().grantee; + assert_eq!( + storage + .revoke_grant(test.deps_mut(), &grantee, &admin) + .unwrap_err(), + NymPoolContractError::UnauthorizedGrantRevocation + ); + + Ok(()) + } + + #[test] + fn can_be_called_by_original_granter_if_its_still_whitelisted() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let granter = test.generate_account(); + let grantee1 = test.generate_account(); + let grantee2 = test.generate_account(); + + test.add_granter(&granter); + let env = test.env(); + storage.insert_new_grant( + test.deps_mut(), + &env, + &granter, + &grantee1, + dummy_allowance(), + )?; + storage.insert_new_grant( + test.deps_mut(), + &env, + &granter, + &grantee2, + dummy_allowance(), + )?; + + // still whitelisted + assert!(storage + .revoke_grant(test.deps_mut(), &grantee1, &granter) + .is_ok()); + + // not whitelisted anymore + storage.remove_granter(test.deps_mut(), &admin, &granter)?; + assert_eq!( + storage + .revoke_grant(test.deps_mut(), &grantee2, &granter) + .unwrap_err(), + NymPoolContractError::UnauthorizedGrantRevocation + ); + + Ok(()) + } + + #[test] + fn removes_the_underlying_grant() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + let admin = test.admin_unchecked(); + let grantee = test.add_dummy_grant().grantee; + + storage.revoke_grant(test.deps_mut(), &grantee, &admin)?; + assert!(storage + .grants + .may_load(test.storage(), grantee.clone())? + .is_none()); + + Ok(()) + } + } + + #[cfg(test)] + mod locking_part_of_allowance { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::DenomExt; + + #[test] + fn requires_providing_valid_coin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let grantee = test.add_dummy_grant().grantee; + + let bad_amount = coin(0, "invalid-denom"); + let good_amount = test.coin(100); + + let env = test.env(); + + assert!(storage + .lock_part_of_allowance(test.deps_mut(), &env, &grantee, bad_amount) + .is_err()); + assert!(storage + .lock_part_of_allowance(test.deps_mut(), &env, &grantee, good_amount) + .is_ok()); + + Ok(()) + } + + #[test] + fn requires_grant_to_exist() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let grantee = test.generate_account(); + let env = test.env(); + + let amount = test.coin(100); + + // doesn't exist + assert!(storage + .lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount.clone()) + .is_err()); + + // does exist + test.add_dummy_grant_for(&grantee); + assert!(storage + .lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount) + .is_ok()); + Ok(()) + } + + #[test] + fn does_not_allow_locking_more_than_spend_limit() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let admin = test.admin_unchecked(); + let env = test.env(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(test.coin(100)), + expiration_unix_timestamp: None, + }); + let grantee = test.generate_account(); + storage.insert_new_grant(test.deps_mut(), &env, &admin, &grantee, allowance)?; + + let amount = test.coin(101); + assert!(storage + .lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount) + .is_err()); + + let amount = test.coin(100); + assert!(storage + .lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount) + .is_ok()); + + Ok(()) + } + + #[test] + fn deducts_locked_amount_from_the_allowance() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let admin = test.admin_unchecked(); + let env = test.env(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(test.coin(100)), + expiration_unix_timestamp: None, + }); + let grantee = test.generate_account(); + storage.insert_new_grant(test.deps_mut(), &env, &admin, &grantee, allowance)?; + + let amount = test.coin(40); + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount)?; + + let allowance = storage + .grants + .load(test.storage(), grantee.clone())? + .allowance; + assert_eq!(allowance.basic().spend_limit, Some(test.coin(60))); + + // no-op if there's no limit + let grantee = test.generate_account(); + let unlimited = Allowance::Basic(BasicAllowance::unlimited()); + storage.insert_new_grant(test.deps_mut(), &env, &admin, &grantee, unlimited)?; + + let amount = test.coin(40); + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount)?; + let allowance = storage + .grants + .load(test.storage(), grantee.clone())? + .allowance; + assert_eq!(allowance.basic(), &BasicAllowance::unlimited()); + + Ok(()) + } + + #[test] + fn preserves_grant_even_if_resultant_allowance_is_zero() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let admin = test.admin_unchecked(); + let env = test.env(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(test.coin(100)), + expiration_unix_timestamp: None, + }); + let grantee = test.generate_account(); + storage.insert_new_grant(test.deps_mut(), &env, &admin, &grantee, allowance)?; + + let amount = test.coin(100); + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount)?; + + let allowance = storage + .grants + .load(test.storage(), grantee.clone())? + .allowance; + assert_eq!(allowance.basic().spend_limit, Some(test.coin(0))); + + Ok(()) + } + + #[test] + fn updates_internal_locked_counter() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let env = test.env(); + let grantee = test.add_dummy_grant().grantee; + let amount1 = test.coin(100); + let amount2 = test.coin(200); + + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount1)?; + assert_eq!( + storage.locked.grantee_locked(test.storage(), &grantee)?, + Uint128::new(100) + ); + assert_eq!( + storage.locked.total_locked.load(test.storage())?, + Uint128::new(100) + ); + + // more locked by same grantee + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount2)?; + assert_eq!( + storage.locked.grantee_locked(test.storage(), &grantee)?, + Uint128::new(300) + ); + assert_eq!( + storage.locked.total_locked.load(test.storage())?, + Uint128::new(300) + ); + + Ok(()) + } + } + + #[cfg(test)] + mod unlocking_part_of_allowance { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContract}; + use nym_contracts_common_testing::{ContractTester, DenomExt}; + + fn setup_locked_grant(test: &mut ContractTester) -> Addr { + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(100)); + grantee + } + + #[test] + fn requires_providing_valid_coin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let grantee = setup_locked_grant(&mut test); + + let bad_amount = coin(0, "invalid-denom"); + let good_amount = test.coin(100); + + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &bad_amount) + .is_err()); + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &good_amount) + .is_ok()); + + Ok(()) + } + + #[test] + fn does_not_allow_unlocking_more_than_currently_locked() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let grantee = setup_locked_grant(&mut test); + + let amount = test.coin(101); + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &amount) + .is_err()); + + let amount = test.coin(100); + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &amount) + .is_ok()); + Ok(()) + } + + #[test] + fn requires_grant_to_exist() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let grantee = test.generate_account(); + + let amount = test.coin(100); + + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &amount) + .is_err()); + test.add_dummy_grant_for(&grantee); + test.lock_allowance(&grantee, Uint128::new(100)); + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &amount) + .is_ok()); + Ok(()) + } + + #[test] + fn requires_having_locked_coins() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let grantee = test.add_dummy_grant().grantee; + + let amount = test.coin(100); + + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &amount) + .is_err()); + test.lock_allowance(&grantee, Uint128::new(100)); + assert!(storage + .unlock_part_of_allowance(test.deps_mut(), &grantee, &amount) + .is_ok()); + Ok(()) + } + + #[test] + fn increases_internal_grant_spend_limit() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + let admin = test.admin_unchecked(); + let env = test.env(); + + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(test.coin(100)), + expiration_unix_timestamp: None, + }); + let grantee = test.generate_account(); + storage.insert_new_grant(test.deps_mut(), &env, &admin, &grantee, allowance)?; + + let amount = test.coin(40); + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount)?; + + let amount = test.coin(20); + storage.unlock_part_of_allowance(test.deps_mut(), &grantee, &amount)?; + + let allowance = storage + .grants + .load(test.storage(), grantee.clone())? + .allowance; + assert_eq!(allowance.basic().spend_limit, Some(test.coin(80))); + + // no-op if there's no limit + let grantee = test.generate_account(); + let unlimited = Allowance::Basic(BasicAllowance::unlimited()); + storage.insert_new_grant(test.deps_mut(), &env, &admin, &grantee, unlimited)?; + + let amount = test.coin(40); + storage.lock_part_of_allowance(test.deps_mut(), &env, &grantee, amount)?; + + let amount = test.coin(20); + storage.unlock_part_of_allowance(test.deps_mut(), &grantee, &amount)?; + + let allowance = storage + .grants + .load(test.storage(), grantee.clone())? + .allowance; + assert_eq!(allowance.basic(), &BasicAllowance::unlimited()); + + Ok(()) + } + + #[test] + fn updates_internal_locked_counter() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = NymPoolStorage::new(); + + // 100tokens locked + let grantee = setup_locked_grant(&mut test); + + let amount = test.coin(20); + storage.unlock_part_of_allowance(test.deps_mut(), &grantee, &amount)?; + assert_eq!( + storage.locked.grantee_locked(test.storage(), &grantee)?, + Uint128::new(80) + ); + assert_eq!( + storage.locked.total_locked.load(test.storage())?, + Uint128::new(80) + ); + + let amount = test.coin(80); + storage.unlock_part_of_allowance(test.deps_mut(), &grantee, &amount)?; + assert!(storage + .locked + .grantees + .may_load(test.storage(), grantee)? + .is_none(),); + assert!(storage.locked.total_locked.load(test.storage())?.is_zero()); + + Ok(()) + } + } + } + + #[cfg(test)] + mod locked_storage { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; + use cosmwasm_std::testing::mock_dependencies; + use nym_contracts_common_testing::{ContractOpts, RandExt}; + + #[test] + fn is_initialised_with_zero_total_locked() -> anyhow::Result<()> { + let mut deps = mock_dependencies(); + let storage = LockedStorage::new(); + + // by default, when created, the `total_locked` is inaccessible + assert!(storage.total_locked.load(&deps.storage).is_err()); + + storage.initialise(deps.as_mut())?; + // but after proper initialisation, it's correctly set to 0 + assert_eq!(storage.total_locked.load(&deps.storage)?, Uint128::zero()); + + Ok(()) + } + + #[test] + fn getting_grantee_locked() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.generate_account(); + + let storage = LockedStorage::new(); + + // returns zero when there's nothing + assert!(storage + .grantee_locked(test.deps().storage, &grantee)? + .is_zero()); + + // even when a grant is created (but with nothing locked!) + test.add_dummy_grant_for(&grantee); + assert!(storage + .grantee_locked(test.deps().storage, &grantee)? + .is_zero()); + let to_lock = Uint128::new(100); + + // lock some tokens... + test.lock_allowance(&grantee, to_lock); + + // now we're talking! + assert_eq!( + storage.grantee_locked(test.deps().storage, &grantee)?, + to_lock + ); + + Ok(()) + } + + #[test] + fn getting_maybe_grantee_locked() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.generate_account(); + + let storage = LockedStorage::new(); + + // returns None when there's nothing + assert!(storage + .maybe_grantee_locked(test.deps().storage, &grantee)? + .is_none()); + + // even when a grant is created (but with nothing locked!) + test.add_dummy_grant_for(&grantee); + assert!(storage + .maybe_grantee_locked(test.deps().storage, &grantee)? + .is_none()); + let to_lock = Uint128::new(100); + + // lock some tokens... + test.lock_allowance(&grantee, to_lock); + + // now we're talking! + assert_eq!( + storage.maybe_grantee_locked(test.deps().storage, &grantee)?, + Some(to_lock) + ); + + Ok(()) + } + + #[test] + fn locking_tokens() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = LockedStorage::new(); + + let grantee1 = test.generate_account(); + let grantee2 = test.generate_account(); + + let first = Uint128::new(100); + let second = Uint128::new(200); + let third = Uint128::new(500); + + let all = test.full_locked_map(); + assert!(all.is_empty()); + + // fresh one creates entry for grantee with the amount + // and updates the total + let deps = test.deps_mut(); + storage.lock(deps, &grantee1, first)?; + + let deps = test.deps(); + assert_eq!(storage.total_locked.load(deps.storage)?, first); + assert_eq!(storage.grantee_locked(deps.storage, &grantee1)?, first); + + let all = test.full_locked_map(); + assert_eq!(all.len(), 1); + + // another one updates existing entries (and doesn't overwrite them!) + let deps = test.deps_mut(); + storage.lock(deps, &grantee1, second)?; + + let deps = test.deps(); + assert_eq!(storage.total_locked.load(deps.storage)?, first + second); + assert_eq!( + storage.grantee_locked(deps.storage, &grantee1)?, + first + second + ); + + let all = test.full_locked_map(); + assert_eq!(all.len(), 1); + + // if we do it for a new grantee, another entry is created, but the same total is updated + let deps = test.deps_mut(); + storage.lock(deps, &grantee2, third)?; + + let deps = test.deps(); + assert_eq!( + storage.total_locked.load(deps.storage)?, + first + second + third + ); + assert_eq!( + storage.grantee_locked(deps.storage, &grantee1)?, + first + second + ); + assert_eq!(storage.grantee_locked(deps.storage, &grantee2)?, third); + + let all = test.full_locked_map(); + assert_eq!(all.len(), 2); + Ok(()) + } + + #[test] + fn unlocking_tokens() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let storage = LockedStorage::new(); + + let grantee1 = test.generate_account(); + let grantee2 = test.generate_account(); + + test.add_dummy_grant_for(&grantee1); + test.add_dummy_grant_for(&grantee2); + + let first = Uint128::new(100); + let second = Uint128::new(200); + let third = Uint128::new(500); + + let all = test.full_locked_map(); + assert!(all.is_empty()); + + let deps = test.deps_mut(); + + // can't unlock anything if there's nothing locked + assert!(matches!( + storage.unlock(deps, &grantee1, first).unwrap_err(), + NymPoolContractError::InsufficientLockedTokens { .. } + )); + + test.lock_allowance(&grantee1, first); + + // can't unlock more than the total locked amount + let deps = test.deps_mut(); + assert!(matches!( + storage.unlock(deps, &grantee1, first + second).unwrap_err(), + NymPoolContractError::InsufficientLockedTokens { .. } + )); + test.lock_allowance(&grantee1, second); + test.lock_allowance(&grantee2, third); + let all = test.full_locked_map(); + assert_eq!(all.len(), 2); + + // unlocking partial amount correctly updates entries + let deps = test.deps_mut(); + assert!(storage.unlock(deps, &grantee1, first).is_ok()); + + let deps = test.deps_mut(); + assert_eq!(storage.total_locked.load(deps.storage)?, second + third); + assert_eq!(storage.grantee_locked(deps.storage, &grantee1)?, second); + let all = test.full_locked_map(); + assert_eq!(all.len(), 2); + + // unlocking the remaining amount will remove the entry + let deps = test.deps_mut(); + assert!(storage.unlock(deps, &grantee1, second).is_ok()); + let deps = test.deps_mut(); + assert_eq!(storage.total_locked.load(deps.storage)?, third); + assert!(storage.grantee_locked(deps.storage, &grantee1)?.is_zero()); + let all = test.full_locked_map(); + assert_eq!(all.len(), 1); + + // similarly if the full amount is unlocked immediately + let deps = test.deps_mut(); + assert!(storage.unlock(deps, &grantee2, third).is_ok()); + let deps = test.deps_mut(); + assert!(storage.total_locked.load(deps.storage)?.is_zero()); + assert!(storage.grantee_locked(deps.storage, &grantee2)?.is_zero()); + + let all = test.full_locked_map(); + assert!(all.is_empty()); + + Ok(()) + } + } +} diff --git a/contracts/nym-pool/src/testing/mod.rs b/contracts/nym-pool/src/testing/mod.rs new file mode 100644 index 0000000000..640b8eccd0 --- /dev/null +++ b/contracts/nym-pool/src/testing/mod.rs @@ -0,0 +1,133 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::{execute, instantiate, migrate, query}; +use crate::storage::NYM_POOL_STORAGE; +use cosmwasm_std::{Addr, Order, Uint128}; +use nym_contracts_common_testing::{ + AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractTester, DenomExt, + PermissionedFn, QueryFn, RandExt, TestableNymContract, +}; +use nym_pool_contract_common::constants::storage_keys; +use nym_pool_contract_common::{ + Allowance, BasicAllowance, ExecuteMsg, Grant, InstantiateMsg, MigrateMsg, NymPoolContractError, + QueryMsg, +}; +use std::collections::HashMap; + +pub use nym_contracts_common_testing::TEST_DENOM; + +pub struct NymPoolContract; + +impl TestableNymContract for NymPoolContract { + const NAME: &'static str = "nym-pool-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = NymPoolContractError; + + fn instantiate() -> ContractFn { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + InstantiateMsg { + pool_denomination: TEST_DENOM.to_string(), + grants: Default::default(), + } + } +} + +pub fn init_contract_tester() -> ContractTester { + NymPoolContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) + .with_common_storage_key(CommonStorageKeys::Denom, storage_keys::POOL_DENOMINATION) +} + +pub trait NymPoolContractTesterExt: + ContractOpts + + ChainOpts + + AdminExt + + DenomExt + + RandExt +{ + fn change_admin(&mut self, new_admin: &Addr) { + self.execute_msg( + self.admin_unchecked(), + &ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + update_granter_set: Some(true), + }, + ) + .unwrap(); + } + + #[track_caller] + fn add_dummy_grant(&mut self) -> Grant { + let grantee = self.generate_account(); + self.add_dummy_grant_for(&grantee) + } + + #[track_caller] + fn add_dummy_grant_for(&mut self, grantee: impl Into) -> Grant { + let grantee = Addr::unchecked(grantee); + let granter = self.admin_unchecked(); + let env = self.env(); + NYM_POOL_STORAGE + .insert_new_grant( + self.deps_mut(), + &env, + &granter, + &grantee, + Allowance::Basic(BasicAllowance::unlimited()), + ) + .unwrap(); + + NYM_POOL_STORAGE.load_grant(self.deps(), &grantee).unwrap() + } + + #[track_caller] + fn lock_allowance(&mut self, grantee: impl Into, amount: impl Into) { + self.execute_msg( + Addr::unchecked(grantee), + &ExecuteMsg::LockAllowance { + amount: self.coin(amount.into().u128()), + }, + ) + .unwrap(); + } + + #[track_caller] + fn full_locked_map(&self) -> HashMap { + NYM_POOL_STORAGE + .locked + .grantees + .range(self.deps().storage, None, None, Order::Ascending) + .collect::, _>>() + .unwrap() + } + + #[track_caller] + fn add_granter(&mut self, granter: &Addr) { + let env = self.env(); + let admin = self.admin_unchecked(); + NYM_POOL_STORAGE + .add_new_granter(self.deps_mut(), &env, &admin, granter) + .unwrap(); + } +} + +impl NymPoolContractTesterExt for ContractTester {} diff --git a/contracts/nym-pool/src/transactions.rs b/contracts/nym-pool/src/transactions.rs new file mode 100644 index 0000000000..3274a31c34 --- /dev/null +++ b/contracts/nym-pool/src/transactions.rs @@ -0,0 +1,1603 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::validate_usage_coin; +use crate::storage::NYM_POOL_STORAGE; +use cosmwasm_std::{BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, Uint128}; +use nym_pool_contract_common::{Allowance, NymPoolContractError, TransferRecipient}; + +pub fn try_update_contract_admin( + mut deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + new_admin: String, + update_granter_set: Option, +) -> Result { + let new_admin = deps.api.addr_validate(&new_admin)?; + let old_admin = info.sender.clone(); + + if let Some(true) = update_granter_set { + // remove current/old admin from the granter set, if present + NYM_POOL_STORAGE + .granters + .remove(deps.storage, old_admin.clone()); + + // insert new admin into the granter set + NYM_POOL_STORAGE.add_new_granter(deps.branch(), &env, &old_admin, &new_admin)?; + } + + let res = NYM_POOL_STORAGE + .contract_admin + .execute_update_admin(deps, info, Some(new_admin))?; + + Ok(res) +} + +pub fn try_grant_allowance( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + grantee: String, + allowance: Allowance, +) -> Result { + let grantee = deps.api.addr_validate(&grantee)?; + + NYM_POOL_STORAGE.insert_new_grant(deps, &env, &info.sender, &grantee, allowance)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_revoke_grant( + deps: DepsMut<'_>, + _env: Env, + info: MessageInfo, + grantee: String, +) -> Result { + let grantee = deps.api.addr_validate(&grantee)?; + + NYM_POOL_STORAGE.revoke_grant(deps, &grantee, &info.sender)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_use_allowance( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + recipients: Vec, +) -> Result { + let denom = NYM_POOL_STORAGE.pool_denomination.load(deps.storage)?; + + if recipients.is_empty() { + return Err(NymPoolContractError::EmptyUsageRequest); + } + + let mut amount = Uint128::zero(); + let mut messages = Vec::new(); + for recipient in recipients { + validate_usage_coin(deps.storage, &recipient.amount)?; + + amount += recipient.amount.amount; + messages.push(CosmosMsg::Bank(BankMsg::Send { + to_address: deps.api.addr_validate(&recipient.recipient)?.to_string(), + amount: vec![recipient.amount], + })) + } + + let available = NYM_POOL_STORAGE.available_tokens(deps.as_ref(), &env)?; + // even if the contract has sufficient amount of tokens (which would be implicit from BankMsg not failing) + // the locked ones take priority + if available.amount < amount { + return Err(NymPoolContractError::InsufficientTokens { + available, + required: Coin { amount, denom }, + }); + } + + NYM_POOL_STORAGE.try_spend_part_of_grant(deps, &env, &info.sender, &Coin { amount, denom })?; + + // TODO: emit events + Ok(Response::new().add_messages(messages)) +} + +pub fn try_withdraw_allowance( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + validate_usage_coin(deps.storage, &amount)?; + + let available = NYM_POOL_STORAGE.available_tokens(deps.as_ref(), &env)?; + + // even if the contract has sufficient amount of tokens (which would be implicit from BankMsg not failing) + // the locked ones take priority + if available.amount < amount.amount { + return Err(NymPoolContractError::InsufficientTokens { + available, + required: amount, + }); + } + + NYM_POOL_STORAGE.try_spend_part_of_grant(deps, &env, &info.sender, &amount)?; + + // TODO: emit events + // TODO2: after migrating common to cw2.2 use `send_tokens` from `ResponseExt` trait + Ok(Response::new().add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![amount], + }))) +} + +pub fn try_lock_allowance( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + NYM_POOL_STORAGE.lock_part_of_allowance(deps, &env, &info.sender, amount)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_unlock_allowance( + deps: DepsMut<'_>, + _env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + NYM_POOL_STORAGE.unlock_part_of_allowance(deps, &info.sender, &amount)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_use_locked_allowance( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + recipients: Vec, +) -> Result { + let mut amount = Uint128::zero(); + let mut messages = Vec::new(); + for recipient in recipients { + validate_usage_coin(deps.storage, &recipient.amount)?; + + amount += recipient.amount.amount; + messages.push(CosmosMsg::Bank(BankMsg::Send { + to_address: deps.api.addr_validate(&recipient.recipient)?.to_string(), + amount: vec![recipient.amount], + })) + } + + // if the grant has already expired, locked coins can no longer be used, + // ideally, they'd be immediately unlocked here, but we need to revert the transaction + let grant = NYM_POOL_STORAGE.load_grant(deps.as_ref(), &info.sender)?; + if grant.allowance.expired(&env) { + return Err(NymPoolContractError::GrantExpired); + } + + let denom = NYM_POOL_STORAGE.pool_denomination.load(deps.storage)?; + + // we remove those coins from the locked pool before transferring them to the specified account + NYM_POOL_STORAGE.unlock_part_of_allowance(deps, &info.sender, &Coin { amount, denom })?; + + // TODO: emit events + Ok(Response::new().add_messages(messages)) +} + +pub fn try_withdraw_locked_allowance( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + // if the grant has already expired, locked coins can no longer be used, + // ideally, they'd be immediately unlocked here, but we need to revert the transaction + let grant = NYM_POOL_STORAGE.load_grant(deps.as_ref(), &info.sender)?; + if grant.allowance.expired(&env) { + return Err(NymPoolContractError::GrantExpired); + } + + // we remove those coins from the locked pool before transferring them to the specified account + NYM_POOL_STORAGE.unlock_part_of_allowance(deps, &info.sender, &amount)?; + + // TODO: emit events + // TODO2: after migrating common to cw2.2 use `send_tokens` from `ResponseExt` trait + Ok(Response::new().add_message(CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![amount], + }))) +} + +pub fn try_add_new_granter( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + granter: String, +) -> Result { + let granter = deps.api.addr_validate(&granter)?; + NYM_POOL_STORAGE.add_new_granter(deps, &env, &info.sender, &granter)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_revoke_granter( + deps: DepsMut<'_>, + _env: Env, + info: MessageInfo, + granter: String, +) -> Result { + let granter = deps.api.addr_validate(&granter)?; + NYM_POOL_STORAGE.remove_granter(deps, &info.sender, &granter)?; + + // TODO: emit events + Ok(Response::new()) +} + +// can be called by anyone, because expired grants are unusable anyway +pub fn try_remove_expired( + deps: DepsMut<'_>, + env: Env, + _info: MessageInfo, + grantee: String, +) -> Result { + let grantee = deps.api.addr_validate(&grantee)?; + let grant = NYM_POOL_STORAGE.load_grant(deps.as_ref(), &grantee)?; + + if !grant.allowance.expired(&env) { + return Err(NymPoolContractError::GrantNotExpired); + } + + NYM_POOL_STORAGE.remove_grant(deps, &grantee)?; + + // TODO: emit events + Ok(Response::new()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, DenomExt, RandExt}; + use nym_pool_contract_common::ExecuteMsg; + + #[cfg(test)] + mod updating_contract_admin { + use super::*; + use cosmwasm_std::{Deps, Order}; + use cw_controllers::AdminError; + use nym_contracts_common_testing::{AdminExt, RandExt}; + use nym_pool_contract_common::{ExecuteMsg, GranterAddress, GranterInformation}; + use std::collections::HashMap; + + #[test] + fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let random_acc = test.generate_account(); + let new_admin = test.generate_account(); + let res = test + .execute_raw( + random_acc, + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + update_granter_set: None, + }, + ) + .unwrap_err(); + + assert_eq!(res, NymPoolContractError::Admin(AdminError::NotAdmin {})); + + let actual_admin = test.admin_unchecked(); + let res = test.execute_raw( + actual_admin.clone(), + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + update_granter_set: None, + }, + ); + assert!(res.is_ok()); + + let updated_admin = test.admin_unchecked(); + assert_eq!(new_admin, updated_admin); + + Ok(()) + } + + #[test] + fn requires_providing_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_account = "definitely-not-valid-account"; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: bad_account.to_string(), + update_granter_set: None, + }, + ); + + assert!(res.is_err()); + + let empty_account = ""; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: empty_account.to_string(), + update_granter_set: None, + }, + ); + + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn updates_granter_set_if_specified() { + fn granters(deps: Deps) -> HashMap { + NYM_POOL_STORAGE + .granters + .range(deps.storage, None, None, Order::Ascending) + .map(|res| res.unwrap()) + .collect() + } + + let mut test = init_contract_tester(); + let current_admin = test.admin_unchecked(); + let new_admin = test.generate_account(); + + let old_granters = granters(test.deps()); + + // no change to the granter set + let res = test.execute_raw( + current_admin.clone(), + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + update_granter_set: Some(false), + }, + ); + assert!(res.is_ok()); + let new_granters = granters(test.deps()); + assert_eq!(old_granters, new_granters); + + // + // + // + + let mut test = init_contract_tester(); + let current_admin = test.admin_unchecked(); + let new_admin = test.generate_account(); + let old_granters = granters(test.deps()); + + let res = test.execute_raw( + current_admin.clone(), + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + update_granter_set: Some(true), + }, + ); + assert!(res.is_ok()); + let new_granters = granters(test.deps()); + assert_ne!(old_granters, new_granters); + assert!(old_granters.contains_key(¤t_admin)); + assert!(new_granters.contains_key(&new_admin)); + } + } + + #[cfg(test)] + mod granting_allowance { + use super::*; + use cosmwasm_std::StdError; + use nym_contracts_common_testing::{AdminExt, RandExt}; + use nym_pool_contract_common::BasicAllowance; + + #[test] + fn requires_providing_valid_grantee_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let env = test.env(); + let admin = test.admin_msg(); + let dummy_grant = Allowance::Basic(BasicAllowance::unlimited()); + + assert!(matches!( + try_grant_allowance( + test.deps_mut(), + env.clone(), + admin.clone(), + "not-a-valid-address".to_string(), + dummy_grant.clone() + ) + .unwrap_err(), + NymPoolContractError::StdErr(StdError::GenericErr { msg, .. }) if msg == "Error decoding bech32" + )); + + let valid_address = test.generate_account(); + assert!(try_grant_allowance( + test.deps_mut(), + env.clone(), + admin.clone(), + valid_address.to_string(), + dummy_grant + ) + .is_ok()); + + Ok(()) + } + } + + #[cfg(test)] + mod revoking_allowance { + use super::*; + use cosmwasm_std::StdError; + use nym_contracts_common_testing::{AdminExt, RandExt}; + + #[test] + fn requires_providing_valid_grantee_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let env = test.env(); + let admin = test.admin_msg(); + let grant = test.add_dummy_grant(); + + assert!(matches!( + try_revoke_grant( + test.deps_mut(), + env.clone(), + admin.clone(), + "not-a-valid-address".to_string() + ) + .unwrap_err(), + NymPoolContractError::StdErr(StdError::GenericErr { msg, .. }) if msg == "Error decoding bech32" + )); + + // use a valid address + // note the different error + let valid_address = test.generate_account(); + assert_eq!( + try_revoke_grant( + test.deps_mut(), + env.clone(), + admin.clone(), + valid_address.to_string() + ) + .unwrap_err(), + NymPoolContractError::GrantNotFound { + grantee: valid_address.to_string() + } + ); + + // for sanity’s sake check with an existing grant + assert!(try_revoke_grant( + test.deps_mut(), + env.clone(), + admin.clone(), + grant.grantee.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + #[cfg(test)] + mod using_allowance { + use super::*; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; + use nym_pool_contract_common::{BasicAllowance, ExecuteMsg}; + + #[test] + fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw(grantee, ExecuteMsg::UseAllowance { recipients: vec![] }); + assert_eq!(res.unwrap_err(), NymPoolContractError::EmptyUsageRequest); + + Ok(()) + } + + #[test] + fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> { + // 1 bad receiver + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseAllowance { + recipients: vec![TransferRecipient { + recipient: "invalid-address".to_string(), + amount: test.coin(1234), + }], + }, + ); + assert!(res.is_err()); + + // 3 receivers, one invalid + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let addr1 = test.generate_account(); + let addr2 = test.generate_account(); + let addr3 = test.generate_account(); + let res = test.execute_raw( + grantee, + ExecuteMsg::UseAllowance { + recipients: vec![ + TransferRecipient { + recipient: addr1.to_string(), + amount: test.coin(1234), + }, + TransferRecipient { + recipient: addr2.to_string(), + amount: test.coin(0), + }, + TransferRecipient { + recipient: addr3.to_string(), + amount: test.coin(1234), + }, + ], + }, + ); + assert!(res.is_err()); + + // all fine + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseAllowance { + recipients: vec![ + TransferRecipient { + recipient: addr1.to_string(), + amount: test.coin(1234), + }, + TransferRecipient { + recipient: addr2.to_string(), + amount: test.coin(1), + }, + TransferRecipient { + recipient: addr3.to_string(), + amount: test.coin(1234), + }, + ], + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn requires_the_total_to_be_available_for_spending() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let recipient = test.generate_account(); + + // contract balance < required + let grantee = test.add_dummy_grant().grantee; + test.set_contract_balance(test.coin(100)); + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseAllowance { + recipients: vec![ + TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(50), + }, + TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(51), + }, + ], + }, + ); + assert_eq!( + NymPoolContractError::InsufficientTokens { + available: test.coin(100), + required: test.coin(101) + }, + res.unwrap_err() + ); + + // contract balance == required + let res = test.execute_raw( + grantee, + ExecuteMsg::UseAllowance { + recipients: vec![ + TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(50), + }, + TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(50), + }, + ], + }, + ); + assert!(res.is_ok()); + + // contract balance > required + let grantee = test.add_dummy_grant().grantee; + test.set_contract_balance(test.coin(100)); + let res = test.execute_raw( + grantee, + ExecuteMsg::UseAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(50), + }], + }, + ); + assert!(res.is_ok()); + + // contract balance > required BUT (contract balance - locked) < required + let grantee = test.add_dummy_grant().grantee; + test.set_contract_balance(test.coin(100)); + test.lock_allowance(&grantee, Uint128::new(40)); + let another_grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + another_grantee, + ExecuteMsg::UseAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(61), + }], + }, + ); + assert_eq!( + NymPoolContractError::InsufficientTokens { + available: test.coin(60), + required: test.coin(61) + }, + res.unwrap_err() + ); + + Ok(()) + } + + #[test] + fn requires_the_total_to_be_within_spend_limit() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(test.coin(100)), + expiration_unix_timestamp: None, + }); + let grantee = test.generate_account(); + let env = test.env(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE.insert_new_grant( + test.deps_mut(), + &env, + &admin, + &grantee, + allowance, + )?; + + let recipient = test.generate_account(); + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(101), + }], + }, + ); + assert_eq!( + NymPoolContractError::SpendingAboveAllowance, + res.unwrap_err() + ); + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(100), + }], + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let grantee = test.add_dummy_grant().grantee; + + let recipient1 = test.generate_account(); + let recipient2 = test.generate_account(); + let recipient3 = test.generate_account(); + + let mut res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseAllowance { + recipients: vec![ + TransferRecipient { + recipient: recipient1.to_string(), + amount: test.coin(100), + }, + TransferRecipient { + recipient: recipient2.to_string(), + amount: test.coin(200), + }, + TransferRecipient { + recipient: recipient3.to_string(), + amount: test.coin(300), + }, + ], + }, + )?; + + // last + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, recipient3.to_string()); + assert_eq!(amount, test.coins(300)); + + // second + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, recipient2.to_string()); + assert_eq!(amount, test.coins(200)); + + // first + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, recipient1.to_string()); + assert_eq!(amount, test.coins(100)); + + assert!(res.messages.is_empty()); + + Ok(()) + } + + #[test] + fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let env = test.env(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: Some(env.block.time.seconds() + 1), + }); + let grantee = test.generate_account(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE.insert_new_grant( + test.deps_mut(), + &env, + &admin, + &grantee, + allowance, + )?; + test.next_block(); + + let recipient = test.generate_account(); + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(100), + }], + }, + ); + assert_eq!(NymPoolContractError::GrantExpired, res.unwrap_err()); + + Ok(()) + } + } + + #[cfg(test)] + mod withdrawing_from_allowance { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContractTesterExt}; + use cosmwasm_std::coin; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, DenomExt, RandExt}; + use nym_pool_contract_common::{BasicAllowance, ExecuteMsg}; + + #[test] + fn requires_valid_coin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: coin(1234, "wtf-denom"), + }, + ); + assert!(res.is_err()); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: test.coin(0), + }, + ); + assert!(res.is_err()); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: test.coin(123), + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn requires_the_amount_to_be_available_for_spending() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + // contract balance < required + let grantee = test.add_dummy_grant().grantee; + test.set_contract_balance(test.coin(100)); + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: test.coin(101), + }, + ); + assert_eq!( + NymPoolContractError::InsufficientTokens { + available: test.coin(100), + required: test.coin(101) + }, + res.unwrap_err() + ); + + // contract balance == required + let res = test.execute_raw( + grantee, + ExecuteMsg::WithdrawAllowance { + amount: test.coin(100), + }, + ); + assert!(res.is_ok()); + + // contract balance > required + let grantee = test.add_dummy_grant().grantee; + test.set_contract_balance(test.coin(100)); + let res = test.execute_raw( + grantee, + ExecuteMsg::WithdrawAllowance { + amount: test.coin(50), + }, + ); + assert!(res.is_ok()); + + // contract balance > required BUT (contract balance - locked) < required + let grantee = test.add_dummy_grant().grantee; + test.set_contract_balance(test.coin(100)); + test.lock_allowance(&grantee, Uint128::new(40)); + let another_grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + another_grantee, + ExecuteMsg::WithdrawAllowance { + amount: test.coin(61), + }, + ); + assert_eq!( + NymPoolContractError::InsufficientTokens { + available: test.coin(60), + required: test.coin(61) + }, + res.unwrap_err() + ); + + Ok(()) + } + + #[test] + fn requires_the_amount_to_be_within_spend_limit() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: Some(test.coin(100)), + expiration_unix_timestamp: None, + }); + let grantee = test.generate_account(); + let env = test.env(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE.insert_new_grant( + test.deps_mut(), + &env, + &admin, + &grantee, + allowance, + )?; + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: test.coin(101), + }, + ); + assert_eq!( + NymPoolContractError::SpendingAboveAllowance, + res.unwrap_err() + ); + + let res = test.execute_raw( + grantee, + ExecuteMsg::WithdrawAllowance { + amount: test.coin(100), + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn attaches_appropriate_bank_message() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let grantee = test.add_dummy_grant().grantee; + + let mut res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: test.coin(100), + }, + )?; + + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, grantee.to_string()); + assert_eq!(amount, test.coins(100)); + + assert!(res.messages.is_empty()); + + Ok(()) + } + + #[test] + fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let env = test.env(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: Some(env.block.time.seconds() + 1), + }); + let grantee = test.generate_account(); + let env = test.env(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE.insert_new_grant( + test.deps_mut(), + &env, + &admin, + &grantee, + allowance, + )?; + test.next_block(); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawAllowance { + amount: test.coin(101), + }, + ); + assert_eq!(NymPoolContractError::GrantExpired, res.unwrap_err()); + + Ok(()) + } + } + + #[test] + fn locking_allowance() -> anyhow::Result<()> { + // internals got tested in storage tests, so this is mostly about checking events (TODO) + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::LockAllowance { + amount: test.coin(100), + }, + ); + assert!(res.is_ok()); + + assert_eq!( + NYM_POOL_STORAGE + .locked + .grantee_locked(test.storage(), &grantee)?, + Uint128::new(100) + ); + + Ok(()) + } + + #[test] + fn unlocking_allowance() -> anyhow::Result<()> { + // internals got tested in storage tests, so this is mostly about checking events (TODO) + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(100)); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UnlockAllowance { + amount: test.coin(40), + }, + ); + assert!(res.is_ok()); + + assert_eq!( + NYM_POOL_STORAGE + .locked + .grantee_locked(test.storage(), &grantee)?, + Uint128::new(60) + ); + + Ok(()) + } + + #[cfg(test)] + mod using_locked_allowance { + use super::*; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; + use nym_pool_contract_common::BasicAllowance; + + #[test] + fn requires_at_least_a_single_coin_receiver() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseLockedAllowance { recipients: vec![] }, + ); + assert_eq!(res.unwrap_err(), NymPoolContractError::EmptyUsageRequest); + + Ok(()) + } + + #[test] + fn requires_valid_coin_for_each_receiver() -> anyhow::Result<()> { + // 1 bad receiver + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(10000)); + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseLockedAllowance { + recipients: vec![TransferRecipient { + recipient: "invalid-address".to_string(), + amount: test.coin(1234), + }], + }, + ); + assert!(res.is_err()); + + // 3 receivers, one invalid + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(10000)); + + let addr1 = test.generate_account(); + let addr2 = test.generate_account(); + let addr3 = test.generate_account(); + let res = test.execute_raw( + grantee, + ExecuteMsg::UseLockedAllowance { + recipients: vec![ + TransferRecipient { + recipient: addr1.to_string(), + amount: test.coin(1234), + }, + TransferRecipient { + recipient: addr2.to_string(), + amount: test.coin(0), + }, + TransferRecipient { + recipient: addr3.to_string(), + amount: test.coin(1234), + }, + ], + }, + ); + assert!(res.is_err()); + + // all fine + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(10000)); + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseLockedAllowance { + recipients: vec![ + TransferRecipient { + recipient: addr1.to_string(), + amount: test.coin(1234), + }, + TransferRecipient { + recipient: addr2.to_string(), + amount: test.coin(1), + }, + TransferRecipient { + recipient: addr3.to_string(), + amount: test.coin(1234), + }, + ], + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn requires_the_total_to_be_locked_by_grantee() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(100)); + + let recipient = test.generate_account(); + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseLockedAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(101), + }], + }, + ); + assert_eq!( + NymPoolContractError::InsufficientLockedTokens { + grantee: grantee.to_string(), + locked: Uint128::new(100), + requested: Uint128::new(101), + }, + res.unwrap_err() + ); + + let res = test.execute_raw( + grantee, + ExecuteMsg::UseLockedAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(100), + }], + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn attaches_appropriate_bank_message_for_each_receiver() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(10000)); + + let recipient1 = test.generate_account(); + let recipient2 = test.generate_account(); + let recipient3 = test.generate_account(); + + let mut res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseLockedAllowance { + recipients: vec![ + TransferRecipient { + recipient: recipient1.to_string(), + amount: test.coin(100), + }, + TransferRecipient { + recipient: recipient2.to_string(), + amount: test.coin(200), + }, + TransferRecipient { + recipient: recipient3.to_string(), + amount: test.coin(300), + }, + ], + }, + )?; + + // last + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, recipient3.to_string()); + assert_eq!(amount, test.coins(300)); + + // second + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, recipient2.to_string()); + assert_eq!(amount, test.coins(200)); + + // first + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, recipient1.to_string()); + assert_eq!(amount, test.coins(100)); + + assert!(res.messages.is_empty()); + + Ok(()) + } + + #[test] + fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let env = test.env(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: Some(env.block.time.seconds() + 1), + }); + let grantee = test.generate_account(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE.insert_new_grant( + test.deps_mut(), + &env, + &admin, + &grantee, + allowance, + )?; + test.lock_allowance(&grantee, Uint128::new(10000)); + test.next_block(); + + let recipient = test.generate_account(); + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::UseLockedAllowance { + recipients: vec![TransferRecipient { + recipient: recipient.to_string(), + amount: test.coin(100), + }], + }, + ); + assert_eq!(NymPoolContractError::GrantExpired, res.unwrap_err()); + + Ok(()) + } + } + + #[cfg(test)] + mod withdrawing_from_locked_allowance { + use super::*; + use cosmwasm_std::coin; + use nym_contracts_common_testing::{AdminExt, ChainOpts, RandExt}; + use nym_pool_contract_common::BasicAllowance; + + #[test] + fn requires_valid_coin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(10000)); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawLockedAllowance { + amount: coin(1234, "wtf-denom"), + }, + ); + assert!(res.is_err()); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawLockedAllowance { + amount: test.coin(0), + }, + ); + assert!(res.is_err()); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawLockedAllowance { + amount: test.coin(123), + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn attaches_appropriate_bank_message() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(10000)); + + let mut res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawLockedAllowance { + amount: test.coin(100), + }, + )?; + + let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = + res.messages.pop().unwrap().msg + else { + panic!("invalid message") + }; + assert_eq!(to_address, grantee.to_string()); + assert_eq!(amount, test.coins(100)); + + assert!(res.messages.is_empty()); + + Ok(()) + } + + #[test] + fn requires_grant_to_not_be_expired() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let env = test.env(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: Some(env.block.time.seconds() + 1), + }); + let grantee = test.generate_account(); + let env = test.env(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE.insert_new_grant( + test.deps_mut(), + &env, + &admin, + &grantee, + allowance, + )?; + test.lock_allowance(&grantee, Uint128::new(10000)); + + test.next_block(); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawLockedAllowance { + amount: test.coin(101), + }, + ); + assert_eq!(NymPoolContractError::GrantExpired, res.unwrap_err()); + + Ok(()) + } + + #[test] + fn requires_the_amount_to_be_locked_by_grantee() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let grantee = test.add_dummy_grant().grantee; + test.lock_allowance(&grantee, Uint128::new(100)); + + let res = test.execute_raw( + grantee.clone(), + ExecuteMsg::WithdrawLockedAllowance { + amount: test.coin(101), + }, + ); + assert_eq!( + NymPoolContractError::InsufficientLockedTokens { + grantee: grantee.to_string(), + locked: Uint128::new(100), + requested: Uint128::new(101), + }, + res.unwrap_err() + ); + + let res = test.execute_raw( + grantee, + ExecuteMsg::WithdrawLockedAllowance { + amount: test.coin(100), + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + } + + #[test] + fn adding_new_granter() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let bad_address = "foomp"; + let good_address = test.generate_account(); + + // requires valid address + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::AddNewGranter { + granter: bad_address.to_string(), + }, + ); + assert!(res.is_err()); + + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::AddNewGranter { + granter: good_address.to_string(), + }, + ); + assert!(res.is_ok()); + + // introduces new granter + assert!(NYM_POOL_STORAGE + .granters + .may_load(test.storage(), good_address)? + .is_some()); + + Ok(()) + } + + #[test] + fn revoking_granter() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let bad_address = "foomp"; + let good_address = test.generate_account(); + let granter_address = test.generate_account(); + test.add_granter(&granter_address); + + // requires valid address + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::RevokeGranter { + granter: bad_address.to_string(), + }, + ); + assert!(res.is_err()); + + // requires an actual granter + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::RevokeGranter { + granter: good_address.to_string(), + }, + ); + assert!(res.is_err()); + + // revokes the granter + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::RevokeGranter { + granter: granter_address.to_string(), + }, + ); + assert!(res.is_ok()); + + assert!(NYM_POOL_STORAGE + .granters + .may_load(test.storage(), granter_address)? + .is_none()); + + Ok(()) + } + + #[cfg(test)] + mod removing_expired { + use super::*; + use crate::testing::{init_contract_tester, NymPoolContract, NymPoolContractTesterExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, ContractTester, RandExt}; + use nym_pool_contract_common::{BasicAllowance, GranteeAddress}; + + fn setup_with_expired_grant() -> (ContractTester, GranteeAddress) { + let mut test = init_contract_tester(); + let env = test.env(); + let allowance = Allowance::Basic(BasicAllowance { + spend_limit: None, + expiration_unix_timestamp: Some(env.block.time.seconds() + 1), + }); + let grantee = test.generate_account(); + let admin = test.admin_unchecked(); + NYM_POOL_STORAGE + .insert_new_grant(test.deps_mut(), &env, &admin, &grantee, allowance) + .unwrap(); + test.next_block(); + (test, grantee) + } + + #[test] + fn requires_valid_grantee_address() -> anyhow::Result<()> { + let (mut test, grantee) = setup_with_expired_grant(); + let sender = test.generate_account(); + let res = test.execute_raw( + sender.clone(), + ExecuteMsg::RemoveExpiredGrant { + grantee: "bad grantee".to_string(), + }, + ); + assert!(res.is_err()); + + let res = test.execute_raw( + sender, + ExecuteMsg::RemoveExpiredGrant { + grantee: grantee.to_string(), + }, + ); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn requires_grant_to_actually_exist_and_be_expired() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + let sender = test.generate_account(); + let grantee = test.add_dummy_grant().grantee; + let not_grantee = test.generate_account(); + + // doesn't exist + let res = test.execute_raw( + sender.clone(), + ExecuteMsg::RemoveExpiredGrant { + grantee: not_grantee.to_string(), + }, + ); + assert!(res.is_err()); + + // exists but not expired + let res = test.execute_raw( + sender.clone(), + ExecuteMsg::RemoveExpiredGrant { + grantee: grantee.to_string(), + }, + ); + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn removes_the_grant() -> anyhow::Result<()> { + let (mut test, grantee) = setup_with_expired_grant(); + let sender = test.generate_account(); + + assert!(NYM_POOL_STORAGE + .grants + .may_load(test.storage(), grantee.clone())? + .is_some()); + + test.execute_raw( + sender.clone(), + ExecuteMsg::RemoveExpiredGrant { + grantee: grantee.to_string(), + }, + )?; + + assert!(NYM_POOL_STORAGE + .grants + .may_load(test.storage(), grantee)? + .is_none()); + + Ok(()) + } + } +} diff --git a/contracts/performance/.cargo/config b/contracts/performance/.cargo/config new file mode 100644 index 0000000000..2fb2c1afdb --- /dev/null +++ b/contracts/performance/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema --features=schema-gen" \ No newline at end of file diff --git a/contracts/performance/Cargo.toml b/contracts/performance/Cargo.toml new file mode 100644 index 0000000000..abeef17f40 --- /dev/null +++ b/contracts/performance/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nym-performance-contract" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "schema" +required-features = ["schema-gen"] + +[lib] +name = "nym_performance_contract" +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-controllers = { workspace = true } +serde = { workspace = true } + +cosmwasm-schema = { workspace = true, optional = true } + +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-performance-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-performance-contract" } +nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } + +[dev-dependencies] +anyhow = { workspace = true } +nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" } +nym-mixnet-contract = { path = "../mixnet", features = ["testable-mixnet-contract"] } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } + +[features] +schema-gen = ["nym-performance-contract-common/schema", "cosmwasm-schema"] + +[lints] +workspace = true \ No newline at end of file diff --git a/contracts/performance/Makefile b/contracts/performance/Makefile new file mode 100644 index 0000000000..086fa71ad3 --- /dev/null +++ b/contracts/performance/Makefile @@ -0,0 +1,5 @@ +wasm: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + +generate-schema: + cargo schema diff --git a/contracts/performance/schema/nym-performance-contract.json b/contracts/performance/schema/nym-performance-contract.json new file mode 100644 index 0000000000..2242aefd75 --- /dev/null +++ b/contracts/performance/schema/nym-performance-contract.json @@ -0,0 +1,1240 @@ +{ + "contract_name": "nym-performance-contract", + "contract_version": "0.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "epoch_measurements_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "epoch_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "full_historical_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "last_submitted_measurement": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "network_monitor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "node_measurements": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "node_performance_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } + }, + "retired_network_monitors_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/performance/schema/raw/execute.json b/contracts/performance/schema/raw/execute.json new file mode 100644 index 0000000000..c4b5c8be04 --- /dev/null +++ b/contracts/performance/schema/raw/execute.json @@ -0,0 +1,163 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a particular node for given epoch", + "type": "object", + "required": [ + "submit" + ], + "properties": { + "submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to submit performance data of a batch of nodes for given epoch", + "type": "object", + "required": [ + "batch_submit" + ], + "properties": { + "batch_submit": { + "type": "object", + "required": [ + "data", + "epoch" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to authorise new network monitor for submitting performance data", + "type": "object", + "required": [ + "authorise_network_monitor" + ], + "properties": { + "authorise_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to retire an existing network monitor and forbid it from submitting any future performance data", + "type": "object", + "required": [ + "retire_network_monitor" + ], + "properties": { + "retire_network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/instantiate.json b/contracts/performance/schema/raw/instantiate.json new file mode 100644 index 0000000000..4380993e57 --- /dev/null +++ b/contracts/performance/schema/raw/instantiate.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "authorised_network_monitors", + "mixnet_contract_address" + ], + "properties": { + "authorised_network_monitors": { + "type": "array", + "items": { + "type": "string" + } + }, + "mixnet_contract_address": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/migrate.json b/contracts/performance/schema/raw/migrate.json new file mode 100644 index 0000000000..7fbe8c5708 --- /dev/null +++ b/contracts/performance/schema/raw/migrate.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/query.json b/contracts/performance/schema/raw/query.json new file mode 100644 index 0000000000..bcca411780 --- /dev/null +++ b/contracts/performance/schema/raw/query.json @@ -0,0 +1,339 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns performance of particular node for the provided epoch", + "type": "object", + "required": [ + "node_performance" + ], + "properties": { + "node_performance": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns historical performance for particular node", + "type": "object", + "required": [ + "node_performance_paged" + ], + "properties": { + "node_performance_paged": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns all submitted measurements for the particular node", + "type": "object", + "required": [ + "node_measurements" + ], + "properties": { + "node_measurements": { + "type": "object", + "required": [ + "epoch_id", + "node_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) measurements for particular epoch", + "type": "object", + "required": [ + "epoch_measurements_paged" + ], + "properties": { + "epoch_measurements_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns (paged) performance for particular epoch", + "type": "object", + "required": [ + "epoch_performance_paged" + ], + "properties": { + "epoch_performance_paged": { + "type": "object", + "required": [ + "epoch_id" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns full (paged) historical performance of the whole network", + "type": "object", + "required": [ + "full_historical_performance_paged" + ], + "properties": { + "full_historical_performance_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about particular network monitor", + "type": "object", + "required": [ + "network_monitor" + ], + "properties": { + "network_monitor": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all network monitors", + "type": "object", + "required": [ + "network_monitors_paged" + ], + "properties": { + "network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about all retired network monitors", + "type": "object", + "required": [ + "retired_network_monitors_paged" + ], + "properties": { + "retired_network_monitors_paged": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information regarding the latest submitted performance data", + "type": "object", + "required": [ + "last_submitted_measurement" + ], + "properties": { + "last_submitted_measurement": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/performance/schema/raw/response_to_admin.json b/contracts/performance/schema/raw/response_to_admin.json new file mode 100644 index 0000000000..c73969ab04 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_admin.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { + "admin": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json new file mode 100644 index 0000000000..4c16b66bb7 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_measurements_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochMeasurementsPagedResponse", + "type": "object", + "required": [ + "epoch_id", + "measurements" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "measurements": { + "type": "array", + "items": { + "$ref": "#/definitions/NodeMeasurement" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeMeasurement": { + "type": "object", + "required": [ + "measurements", + "node_id" + ], + "properties": { + "measurements": { + "$ref": "#/definitions/NodeResults" + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_epoch_performance_paged.json b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json new file mode 100644 index 0000000000..235c12b64f --- /dev/null +++ b/contracts/performance/schema/raw/response_to_epoch_performance_paged.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochPerformancePagedResponse", + "type": "object", + "required": [ + "epoch_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json new file mode 100644 index 0000000000..6f0eb6e191 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_full_historical_performance_paged.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FullHistoricalPerformancePagedResponse", + "type": "object", + "required": [ + "performance" + ], + "properties": { + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalPerformance" + } + }, + "start_next_after": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HistoricalPerformance": { + "type": "object", + "required": [ + "epoch_id", + "node_id", + "performance" + ], + "properties": { + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_last_submitted_measurement.json b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json new file mode 100644 index 0000000000..7c3779d9a6 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_last_submitted_measurement.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LastSubmission", + "type": "object", + "required": [ + "block_height", + "block_time" + ], + "properties": { + "block_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "block_time": { + "$ref": "#/definitions/Timestamp" + }, + "data": { + "anyOf": [ + { + "$ref": "#/definitions/LastSubmittedData" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "LastSubmittedData": { + "type": "object", + "required": [ + "data", + "epoch_id", + "sender" + ], + "properties": { + "data": { + "$ref": "#/definitions/NodePerformance" + }, + "epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "sender": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NodePerformance": { + "type": "object", + "required": [ + "n", + "p" + ], + "properties": { + "n": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "p": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 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 `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitor.json b/contracts/performance/schema/raw/response_to_network_monitor.json new file mode 100644 index 0000000000..eecd26abe3 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitor.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorResponse", + "type": "object", + "properties": { + "info": { + "anyOf": [ + { + "$ref": "#/definitions/NetworkMonitorInformation" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_network_monitors_paged.json new file mode 100644 index 0000000000..a811963072 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_network_monitors_paged.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkMonitorInformation" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "NetworkMonitorInformation": { + "type": "object", + "required": [ + "current_submission_metadata", + "details" + ], + "properties": { + "current_submission_metadata": { + "$ref": "#/definitions/NetworkMonitorSubmissionMetadata" + }, + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + } + }, + "additionalProperties": false + }, + "NetworkMonitorSubmissionMetadata": { + "type": "object", + "required": [ + "last_submitted_epoch_id", + "last_submitted_node_id" + ], + "properties": { + "last_submitted_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_submitted_node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_measurements.json b/contracts/performance/schema/raw/response_to_node_measurements.json new file mode 100644 index 0000000000..e1add3c02d --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_measurements.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeMeasurementsResponse", + "type": "object", + "properties": { + "measurements": { + "anyOf": [ + { + "$ref": "#/definitions/NodeResults" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeResults": { + "type": "array", + "items": { + "$ref": "#/definitions/Percent" + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance.json b/contracts/performance/schema/raw/response_to_node_performance.json new file mode 100644 index 0000000000..d9bae2e00d --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformanceResponse", + "type": "object", + "properties": { + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_node_performance_paged.json b/contracts/performance/schema/raw/response_to_node_performance_paged.json new file mode 100644 index 0000000000..f63c28f477 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_node_performance_paged.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodePerformancePagedResponse", + "type": "object", + "required": [ + "node_id", + "performance" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "type": "array", + "items": { + "$ref": "#/definitions/EpochNodePerformance" + } + }, + "start_next_after": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EpochNodePerformance": { + "type": "object", + "required": [ + "epoch" + ], + "properties": { + "epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "performance": { + "anyOf": [ + { + "$ref": "#/definitions/Percent" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + } +} diff --git a/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json new file mode 100644 index 0000000000..f5310e9438 --- /dev/null +++ b/contracts/performance/schema/raw/response_to_retired_network_monitors_paged.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RetiredNetworkMonitorsPagedResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "array", + "items": { + "$ref": "#/definitions/RetiredNetworkMonitor" + } + }, + "start_next_after": { + "type": [ + "string", + "null" + ] + } + }, + "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" + }, + "NetworkMonitorDetails": { + "type": "object", + "required": [ + "address", + "authorised_at_height", + "authorised_by" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "authorised_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "authorised_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + }, + "RetiredNetworkMonitor": { + "type": "object", + "required": [ + "details", + "retired_at_height", + "retired_by" + ], + "properties": { + "details": { + "$ref": "#/definitions/NetworkMonitorDetails" + }, + "retired_at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "retired_by": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/performance/src/bin/schema.rs b/contracts/performance/src/bin/schema.rs new file mode 100644 index 0000000000..d34f6f9b74 --- /dev/null +++ b/contracts/performance/src/bin/schema.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::write_api; +use nym_performance_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/performance/src/contract.rs b/contracts/performance/src/contract.rs new file mode 100644 index 0000000000..2f488f3961 --- /dev/null +++ b/contracts/performance/src/contract.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::queries::{ + query_admin, query_epoch_measurements_paged, query_epoch_performance_paged, + query_full_historical_performance_paged, query_last_submission, query_network_monitor_details, + query_network_monitors_paged, query_node_measurements, query_node_performance, + query_node_performance_paged, query_retired_network_monitors_paged, +}; +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use crate::transactions::{ + try_authorise_network_monitor, try_batch_submit_performance_results, + try_remove_epoch_measurements, try_remove_node_measurements, try_retire_network_monitor, + try_submit_performance_results, try_update_contract_admin, +}; +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, +}; +use nym_contracts_common::set_build_information; +use nym_performance_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NymPerformanceContractError, QueryMsg, +}; + +const CONTRACT_NAME: &str = "crate:nym-performance-contract"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + set_build_information!(deps.storage)?; + + let mixnet_contract_address = deps.api.addr_validate(&msg.mixnet_contract_address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.initialise( + deps, + env, + info.sender, + mixnet_contract_address.clone(), + msg.authorised_network_monitors, + )?; + + Ok(Response::default()) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin), + ExecuteMsg::Submit { epoch, data } => { + try_submit_performance_results(deps, env, info, epoch, data) + } + ExecuteMsg::BatchSubmit { epoch, data } => { + try_batch_submit_performance_results(deps, env, info, epoch, data) + } + ExecuteMsg::AuthoriseNetworkMonitor { address } => { + try_authorise_network_monitor(deps, env, info, address) + } + ExecuteMsg::RetireNetworkMonitor { address } => { + try_retire_network_monitor(deps, env, info, address) + } + ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => { + try_remove_node_measurements(deps, info, epoch_id, node_id) + } + ExecuteMsg::RemoveEpochMeasurements { epoch_id } => { + try_remove_epoch_measurements(deps, info, epoch_id) + } + } +} + +#[entry_point] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?), + QueryMsg::NodePerformance { epoch_id, node_id } => Ok(to_json_binary( + &query_node_performance(deps, epoch_id, node_id)?, + )?), + QueryMsg::NodePerformancePaged { + node_id, + start_after, + limit, + } => Ok(to_json_binary(&query_node_performance_paged( + deps, + node_id, + start_after, + limit, + )?)?), + QueryMsg::EpochPerformancePaged { + epoch_id, + start_after, + limit, + } => Ok(to_json_binary(&query_epoch_performance_paged( + deps, + epoch_id, + start_after, + limit, + )?)?), + QueryMsg::FullHistoricalPerformancePaged { start_after, limit } => Ok(to_json_binary( + &query_full_historical_performance_paged(deps, start_after, limit)?, + )?), + QueryMsg::NetworkMonitor { address } => Ok(to_json_binary( + &query_network_monitor_details(deps, address)?, + )?), + QueryMsg::NetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary( + &query_network_monitors_paged(deps, start_after, limit)?, + )?), + QueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => Ok(to_json_binary( + &query_retired_network_monitors_paged(deps, start_after, limit)?, + )?), + QueryMsg::NodeMeasurements { epoch_id, node_id } => Ok(to_json_binary( + &query_node_measurements(deps, epoch_id, node_id)?, + )?), + QueryMsg::EpochMeasurementsPaged { + epoch_id, + start_after, + limit, + } => Ok(to_json_binary(&query_epoch_measurements_paged( + deps, + epoch_id, + start_after, + limit, + )?)?), + QueryMsg::LastSubmittedMeasurement {} => Ok(to_json_binary(&query_last_submission(deps)?)?), + } +} + +#[entry_point] +pub fn migrate( + deps: DepsMut, + _: Env, + _msg: MigrateMsg, +) -> Result { + set_build_information!(deps.storage)?; + cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod contract_instantiation { + use super::*; + use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; + use crate::testing::PreInitContract; + use cosmwasm_std::testing::message_info; + + #[test] + fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> { + // we need to mock dependencies in a state where mixnet contract has already been instantiated + // (we query it at init) + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + let mixnet_contract_address = pre_init.mixnet_contract_address.to_string(); + let some_sender = pre_init.addr_make("some_sender"); + let deps = pre_init.deps_mut(); + + instantiate( + deps, + env, + message_info(&some_sender, &[]), + InstantiateMsg { + mixnet_contract_address, + authorised_network_monitors: vec![], + }, + )?; + + let deps = pre_init.deps(); + + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .assert_admin(deps, &some_sender)?; + + Ok(()) + } + } +} diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs new file mode 100644 index 0000000000..51ccb4f4b2 --- /dev/null +++ b/contracts/performance/src/helpers.rs @@ -0,0 +1,118 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult}; +use cw_storage_plus::{Key, Namespace, Path, PrimaryKey}; +use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond}; +use nym_performance_contract_common::{EpochId, NodeId}; +use serde::de::DeserializeOwned; +use std::ops::Deref; + +pub(crate) trait MixnetContractQuerier { + #[allow(dead_code)] + fn query_mixnet_contract( + &self, + address: impl Into, + msg: &nym_mixnet_contract_common::QueryMsg, + ) -> StdResult; + + fn query_mixnet_contract_storage( + &self, + address: impl Into, + key: impl Into, + ) -> StdResult>>; + + fn query_mixnet_contract_storage_value( + &self, + address: impl Into, + key: impl Into, + ) -> StdResult> { + match self.query_mixnet_contract_storage(address, key)? { + None => Ok(None), + Some(value) => Ok(Some(from_json(&value)?)), + } + } + + fn query_current_mixnet_interval(&self, address: impl Into) -> StdResult { + self.query_mixnet_contract_storage_value(address, b"ci")? + .ok_or(StdError::not_found( + "unable to retrieve interval information from the mixnet contract storage", + )) + } + + fn query_current_absolute_mixnet_epoch_id( + &self, + address: impl Into, + ) -> StdResult { + self.query_current_mixnet_interval(address) + .map(|interval| interval.current_epoch_absolute_id()) + } + + fn check_node_existence(&self, address: impl Into, node_id: NodeId) -> StdResult { + let mixnet_contract_address = address.into(); + + // 1. check if it's a nym-node + if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? { + return Ok(!nym_node.is_unbonding); + } + + // 2. try a legacy mixnode + if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? { + return Ok(!nym_node.is_unbonding); + } + Ok(false) + } + + fn query_nymnode_bond( + &self, + address: impl Into, + node_id: NodeId, + ) -> StdResult> { + // construct proper map key + let pk_namespace = "nn"; + let path: Path = Path::new( + Namespace::from_static_str(pk_namespace).as_slice(), + &node_id.key().iter().map(Key::as_ref).collect::>(), + ); + let storage_key = path.deref(); + + self.query_mixnet_contract_storage_value(address, storage_key) + } + + fn query_mixnode_bond( + &self, + address: impl Into, + node_id: NodeId, + ) -> StdResult> { + // construct proper map key + let pk_namespace = "mnn"; + let path: Path = Path::new( + Namespace::from_static_str(pk_namespace).as_slice(), + &node_id.key().iter().map(Key::as_ref).collect::>(), + ); + let storage_key = path.deref(); + + self.query_mixnet_contract_storage_value(address, storage_key) + } +} + +impl MixnetContractQuerier for QuerierWrapper<'_, C> +where + C: CustomQuery, +{ + fn query_mixnet_contract( + &self, + address: impl Into, + msg: &nym_mixnet_contract_common::QueryMsg, + ) -> StdResult { + self.query_wasm_smart(address, msg) + } + + fn query_mixnet_contract_storage( + &self, + address: impl Into, + key: impl Into, + ) -> StdResult>> { + self.query_wasm_raw(address, key) + } +} diff --git a/contracts/performance/src/lib.rs b/contracts/performance/src/lib.rs new file mode 100644 index 0000000000..08a2b9651d --- /dev/null +++ b/contracts/performance/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod contract; +pub mod queued_migrations; +pub mod storage; + +mod helpers; +mod queries; +mod transactions; + +#[cfg(test)] +pub mod testing; diff --git a/contracts/performance/src/queries.rs b/contracts/performance/src/queries.rs new file mode 100644 index 0000000000..5fe2c3ed91 --- /dev/null +++ b/contracts/performance/src/queries.rs @@ -0,0 +1,667 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::{retrieval_limits, NYM_PERFORMANCE_CONTRACT_STORAGE}; +use cosmwasm_std::{Addr, Deps, Order, StdResult}; +use cw_controllers::AdminResponse; +use cw_storage_plus::Bound; +use nym_performance_contract_common::{ + EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse, + FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission, + NetworkMonitorInformation, NetworkMonitorResponse, NetworkMonitorsPagedResponse, NodeId, + NodeMeasurement, NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse, + NodePerformanceResponse, NymPerformanceContractError, RetiredNetworkMonitorsPagedResponse, +}; + +pub fn query_admin(deps: Deps) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .query_admin(deps) + .map_err(Into::into) +} + +pub fn query_node_performance( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + let performance = + NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance(deps.storage, epoch_id, node_id)?; + Ok(NodePerformanceResponse { performance }) +} + +pub fn query_node_measurements( + deps: Deps, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .may_load(deps.storage, (epoch_id, node_id))?; + Ok(NodeMeasurementsResponse { measurements }) +} + +pub fn query_node_performance_paged( + deps: Deps, + node_id: NodeId, + start_after: Option, + limit: Option, +) -> Result { + let current_epoch_id = NYM_PERFORMANCE_CONTRACT_STORAGE.current_mixnet_epoch_id(deps)?; + + let start = match start_after { + None => NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_epoch_id_at_creation + .load(deps.storage)?, + Some(start_after) => start_after + 1, + }; + + let mut performance = Vec::new(); + + if current_epoch_id < start { + return Ok(NodePerformancePagedResponse { + node_id, + performance, + start_next_after: None, + }); + } + + let limit = limit + .unwrap_or(retrieval_limits::NODE_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_PERFORMANCE_MAX_LIMIT) as usize; + + for epoch_id in (start..=current_epoch_id).take(limit) { + performance.push(EpochNodePerformance { + epoch: epoch_id, + performance: NYM_PERFORMANCE_CONTRACT_STORAGE.try_load_performance( + deps.storage, + epoch_id, + node_id, + )?, + }) + } + + let start_next_after = performance.last().and_then(|last| { + if last.epoch != current_epoch_id { + Some(last.epoch) + } else { + None + } + }); + + Ok(NodePerformancePagedResponse { + node_id, + performance, + start_next_after, + }) +} + +pub fn query_epoch_performance_paged( + deps: Deps, + epoch_id: EpochId, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_EPOCH_PERFORMANCE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .prefix(epoch_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|(node_id, results)| NodePerformance { + node_id, + performance: results.median(), + }) + }) + .collect::>>()?; + + let start_next_after = performance.last().map(|last| last.node_id); + + Ok(EpochPerformancePagedResponse { + epoch_id, + performance, + start_next_after, + }) +} + +pub fn query_epoch_measurements_paged( + deps: Deps, + epoch_id: EpochId, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_EPOCH_MEASUREMENTS_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let measurements = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .prefix(epoch_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|(node_id, measurements)| NodeMeasurement { + node_id, + measurements, + }) + }) + .collect::>>()?; + + let start_next_after = measurements.last().map(|last| last.node_id); + + Ok(EpochMeasurementsPagedResponse { + epoch_id, + measurements, + start_next_after, + }) +} + +pub fn query_full_historical_performance_paged( + deps: Deps, + start_after: Option<(EpochId, NodeId)>, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT) + .min(retrieval_limits::NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let performance = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.map(|((epoch_id, node_id), results)| HistoricalPerformance { + epoch_id, + node_id, + performance: results.median(), + }) + }) + .collect::>>()?; + + let start_next_after = performance.last().map(|last| (last.epoch_id, last.node_id)); + + Ok(FullHistoricalPerformancePagedResponse { + performance, + start_next_after, + }) +} + +fn get_network_monitor_information( + deps: Deps, + address: &Addr, +) -> Result, NymPerformanceContractError> { + let Some(details) = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .authorised + .may_load(deps.storage, address)? + else { + return Ok(None); + }; + + let current_submission_metadata = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .submission_metadata + .load(deps.storage, address)?; + + Ok(Some(NetworkMonitorInformation { + details, + current_submission_metadata, + })) +} + +pub fn query_network_monitor_details( + deps: Deps, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + + Ok(NetworkMonitorResponse { + info: get_network_monitor_information(deps, &address)?, + }) +} + +pub fn query_network_monitors_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::NETWORK_MONITORS_DEFAULT_LIMIT) + .min(retrieval_limits::NETWORK_MONITORS_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let start = addr.as_ref().map(Bound::exclusive); + + let info = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .authorised + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| { + record.and_then(|(address, details)| { + NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .submission_metadata + .load(deps.storage, &address) + .map(|current_submission_metadata| NetworkMonitorInformation { + details, + current_submission_metadata, + }) + }) + }) + .collect::>>()?; + + let start_next_after = info.last().map(|last| last.details.address.to_string()); + + Ok(NetworkMonitorsPagedResponse { + info, + start_next_after, + }) +} + +pub fn query_retired_network_monitors_paged( + deps: Deps, + start_after: Option, + limit: Option, +) -> Result { + let limit = limit + .unwrap_or(retrieval_limits::RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT) + .min(retrieval_limits::RETIRED_NETWORK_MONITORS_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let start = addr.as_ref().map(Bound::exclusive); + + let info = NYM_PERFORMANCE_CONTRACT_STORAGE + .network_monitors + .retired + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| record.map(|(_, details)| details)) + .collect::>>()?; + + let start_next_after = info.last().map(|last| last.details.address.to_string()); + + Ok(RetiredNetworkMonitorsPagedResponse { + info, + start_next_after, + }) +} + +pub fn query_last_submission(deps: Deps) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE + .last_performance_submission + .load(deps.storage) + .map_err(Into::into) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::LastSubmittedData; + + #[cfg(test)] + mod admin_query { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt}; + use nym_performance_contract_common::ExecuteMsg; + + #[test] + fn returns_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let initial_admin = test.admin_unchecked(); + + // initial + let res = query_admin(test.deps())?; + assert_eq!(res.admin, Some(initial_admin.to_string())); + + let new_admin = test.generate_account(); + + // sanity check + assert_ne!(initial_admin, new_admin); + + // after update + test.execute_msg( + initial_admin.clone(), + &ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + )?; + + let updated_admin = query_admin(test.deps())?; + assert_eq!(updated_admin.admin, Some(new_admin.to_string())); + + Ok(()) + } + } + + #[test] + fn querying_node_performance_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let node_id = test.bond_dummy_nymnode()?; + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + + // epoch 0 + test.insert_raw_performance(&nm, node_id, "0")?; + + // epoch 1 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.1")?; + + // epoch 2 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.2")?; + + // epoch 3 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.3")?; + + // epoch 4 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.4")?; + + // epoch 5 + test.advance_mixnet_epoch()?; + test.insert_raw_performance(&nm, node_id, "0.5")?; + + let deps = test.deps(); + let res = query_node_performance_paged(deps, node_id, Some(5), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_node_performance_paged(deps, node_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_node_performance_paged(deps, node_id, Some(4), None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + }] + ); + + let res = query_node_performance_paged(deps, node_id, Some(2), None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![ + EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }, + EpochNodePerformance { + epoch: 4, + performance: Some("0.4".parse()?), + }, + EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + } + ] + ); + + let res = query_node_performance_paged(deps, node_id, None, None)?; + assert!(res.start_next_after.is_none()); + assert_eq!( + res.performance, + vec![ + EpochNodePerformance { + epoch: 0, + performance: Some("0".parse()?), + }, + EpochNodePerformance { + epoch: 1, + performance: Some("0.1".parse()?), + }, + EpochNodePerformance { + epoch: 2, + performance: Some("0.2".parse()?), + }, + EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }, + EpochNodePerformance { + epoch: 4, + performance: Some("0.4".parse()?), + }, + EpochNodePerformance { + epoch: 5, + performance: Some("0.5".parse()?), + } + ] + ); + + let res = query_node_performance_paged(deps, node_id, Some(2), Some(1))?; + assert_eq!(res.start_next_after, Some(3)); + assert_eq!( + res.performance, + vec![EpochNodePerformance { + epoch: 3, + performance: Some("0.3".parse()?), + }] + ); + + Ok(()) + } + + #[test] + fn querying_epoch_performance_paged() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let nm = test.generate_account(); + test.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(test.bond_dummy_nymnode()?); + } + + let epoch_id = 5; + test.set_mixnet_epoch(epoch_id)?; + + test.insert_raw_performance(&nm, nodes[1], "0.1")?; + test.insert_raw_performance(&nm, nodes[2], "0.2")?; + test.insert_raw_performance(&nm, nodes[3], "0.3")?; + // 4 is missing + test.insert_raw_performance(&nm, nodes[5], "0.5")?; + test.insert_raw_performance(&nm, nodes[6], "0.6")?; + + let deps = test.deps(); + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[6]), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(42), None)?; + assert!(res.start_next_after.is_none()); + assert!(res.performance.is_empty()); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[4]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[3]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }, + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, None, None)?; + assert_eq!(res.start_next_after, Some(nodes[6])); + assert_eq!( + res.performance, + vec![ + NodePerformance { + node_id: nodes[1], + performance: "0.1".parse()?, + }, + NodePerformance { + node_id: nodes[2], + performance: "0.2".parse()?, + }, + NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }, + NodePerformance { + node_id: nodes[5], + performance: "0.5".parse()?, + }, + NodePerformance { + node_id: nodes[6], + performance: "0.6".parse()?, + } + ] + ); + + let res = query_epoch_performance_paged(deps, epoch_id, Some(nodes[2]), Some(1))?; + assert_eq!(res.start_next_after, Some(nodes[3])); + assert_eq!( + res.performance, + vec![NodePerformance { + node_id: nodes[3], + performance: "0.3".parse()?, + }] + ); + + Ok(()) + } + + #[test] + fn last_submission_query() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let env = test.env(); + + let id1 = test.bond_dummy_nymnode()?; + let id2 = test.bond_dummy_nymnode()?; + + // initial + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + } + ); + + let nm1 = test.generate_account(); + let nm2 = test.generate_account(); + test.authorise_network_monitor(&nm1)?; + test.authorise_network_monitor(&nm2)?; + test.set_mixnet_epoch(10)?; + + test.insert_raw_performance(&nm1, id1, "0.2")?; + + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm1.clone(), + epoch_id: 10, + data: NodePerformance { + node_id: id1, + performance: "0.2".parse()? + }, + }), + } + ); + + test.next_block(); + let env = test.env(); + + test.insert_epoch_performance(&nm2, 5, id2, "0.3".parse()?)?; + + // note that even though it's "earlier" data, last submission is still updated accordingly + let data = query_last_submission(test.deps())?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm2.clone(), + epoch_id: 5, + data: NodePerformance { + node_id: id2, + performance: "0.3".parse()? + }, + }), + } + ); + + Ok(()) + } +} diff --git a/contracts/performance/src/queued_migrations.rs b/contracts/performance/src/queued_migrations.rs new file mode 100644 index 0000000000..7e1e3cacd6 --- /dev/null +++ b/contracts/performance/src/queued_migrations.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs new file mode 100644 index 0000000000..acba90762b --- /dev/null +++ b/contracts/performance/src/storage.rs @@ -0,0 +1,3091 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::MixnetContractQuerier; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, StdError, Storage}; +use cw_controllers::Admin; +use cw_storage_plus::{Item, Map}; +use nym_contracts_common::Percent; +use nym_performance_contract_common::constants::storage_keys; +use nym_performance_contract_common::{ + BatchSubmissionResult, EpochId, LastSubmission, LastSubmittedData, NetworkMonitorDetails, + NetworkMonitorSubmissionMetadata, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, RemoveEpochMeasurementsResponse, RetiredNetworkMonitor, +}; + +pub const NYM_PERFORMANCE_CONTRACT_STORAGE: NymPerformanceContractStorage = + NymPerformanceContractStorage::new(); + +pub struct NymPerformanceContractStorage { + pub(crate) contract_admin: Admin, + pub(crate) mixnet_epoch_id_at_creation: Item, + pub(crate) last_performance_submission: Item, + + pub(crate) mixnet_contract_address: Item, + + pub(crate) network_monitors: NetworkMonitorsStorage, + + pub(crate) performance_results: PerformanceResultsStorage, +} + +impl NymPerformanceContractStorage { + #[allow(clippy::new_without_default)] + pub(crate) const fn new() -> Self { + NymPerformanceContractStorage { + contract_admin: Admin::new(storage_keys::CONTRACT_ADMIN), + mixnet_epoch_id_at_creation: Item::new(storage_keys::INITIAL_EPOCH_ID), + last_performance_submission: Item::new(storage_keys::LAST_SUBMISSION), + mixnet_contract_address: Item::new(storage_keys::MIXNET_CONTRACT), + network_monitors: NetworkMonitorsStorage::new(), + performance_results: PerformanceResultsStorage::new(), + } + } + + pub fn current_mixnet_epoch_id( + &self, + deps: Deps, + ) -> Result { + let mixnet_contract_address = self.mixnet_contract_address.load(deps.storage)?; + let current_epoch_id = deps + .querier + .query_current_absolute_mixnet_epoch_id(&mixnet_contract_address)?; + Ok(current_epoch_id) + } + + pub fn node_bonded( + &self, + deps: Deps, + node_id: NodeId, + ) -> Result { + let mixnet_contract_address = self.mixnet_contract_address.load(deps.storage)?; + + let exists = deps + .querier + .check_node_existence(mixnet_contract_address, node_id)?; + Ok(exists) + } + + pub fn initialise( + &self, + mut deps: DepsMut, + env: Env, + admin: Addr, + mixnet_contract_address: Addr, + initial_authorised_network_monitors: Vec, + ) -> Result<(), NymPerformanceContractError> { + // set the mixnet contract address + self.mixnet_contract_address + .save(deps.storage, &mixnet_contract_address)?; + + let initial_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + + // set the last submission to the initial value + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }, + )?; + + // set the initial epoch id + self.mixnet_epoch_id_at_creation + .save(deps.storage, &initial_epoch_id)?; + + // set the contract admin + self.contract_admin + .set(deps.branch(), Some(admin.clone()))?; + + // initialise the network monitors storage (by setting the current count to 0) + self.network_monitors.initialise(deps.branch())?; + + // add all initial network monitors + for network_monitor in initial_authorised_network_monitors { + let network_monitor = deps.api.addr_validate(&network_monitor)?; + self.authorise_network_monitor(deps.branch(), &env, &admin, network_monitor)?; + } + + Ok(()) + } + + pub fn submit_performance_data( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + epoch_id: EpochId, + data: NodePerformance, + ) -> Result<(), NymPerformanceContractError> { + // 1. check if the sender is authorised to submit performance data + self.network_monitors + .ensure_authorised(deps.storage, sender)?; + + // 2. check if current submission metadata is consistent with the result we want to submit + self.performance_results.ensure_non_stale_submission( + deps.storage, + sender, + epoch_id, + data.node_id, + )?; + + // 3. check if the node is bonded + if !self.node_bonded(deps.as_ref(), data.node_id)? { + return Err(NymPerformanceContractError::NodeNotBonded { + node_id: data.node_id, + }); + } + + // 4 insert performance data into the storage + self.performance_results + .insert_performance_data(deps.storage, epoch_id, &data)?; + + // 5. update submission metadata based on the last result we submitted + self.performance_results.update_submission_metadata( + deps.storage, + sender, + epoch_id, + data.node_id, + )?; + + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data, + }), + }, + )?; + + Ok(()) + } + + pub fn batch_submit_performance_results( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + epoch_id: EpochId, + data: Vec, + ) -> Result { + // 1. check if the sender is authorised to submit performance data + self.network_monitors + .ensure_authorised(deps.storage, sender)?; + + let Some(first) = data.first() else { + // no performance data + return Ok(BatchSubmissionResult::default()); + }; + + // 2. check if current submission metadata is consistent with the first result we want to submit + self.performance_results.ensure_non_stale_submission( + deps.storage, + sender, + epoch_id, + first.node_id, + )?; + + let mut accepted_scores = 0; + let mut non_existent_nodes = Vec::new(); + + // 3. submit it + if self.node_bonded(deps.as_ref(), first.node_id)? { + self.performance_results + .insert_performance_data(deps.storage, epoch_id, first)?; + accepted_scores += 1; + } else { + non_existent_nodes.push(first.node_id); + } + + // not point in using peekable iterator, we can just keep track of the previous + // element we've seen + let mut previous = first.node_id; + + for perf in data.iter().skip(1) { + // 4. ensure provided data is sorted (if the check fails in later iteration, + // the whole tx will get reverted so it's fine to just set the storage within the same loop + if perf.node_id <= previous { + return Err(NymPerformanceContractError::UnsortedBatchSubmission); + } + previous = perf.node_id; + + // 5. insert performance data into the storage + if self.node_bonded(deps.as_ref(), perf.node_id)? { + self.performance_results + .insert_performance_data(deps.storage, epoch_id, perf)?; + accepted_scores += 1; + } else { + non_existent_nodes.push(perf.node_id); + } + } + + // SAFETY: we know this vector is not empty + #[allow(clippy::unwrap_used)] + let last = data.last().unwrap(); + + // 5. update submission metadata based on the last result we submitted + self.performance_results.update_submission_metadata( + deps.storage, + sender, + epoch_id, + last.node_id, + )?; + + // 6. update latest submitted + self.last_performance_submission.save( + deps.storage, + &LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: sender.clone(), + epoch_id, + data: *last, + }), + }, + )?; + + Ok(BatchSubmissionResult { + accepted_scores, + non_existent_nodes, + }) + } + + #[cfg(test)] + fn is_admin(&self, deps: Deps, addr: &Addr) -> Result { + self.contract_admin.is_admin(deps, addr).map_err(Into::into) + } + + fn ensure_is_admin(&self, deps: Deps, addr: &Addr) -> Result<(), NymPerformanceContractError> { + self.contract_admin + .assert_admin(deps, addr) + .map_err(Into::into) + } + + pub fn authorise_network_monitor( + &self, + mut deps: DepsMut, + env: &Env, + sender: &Addr, + network_monitor: Addr, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + // make sure this address is not already authorised (it'd mess up the total count) + if self + .network_monitors + .authorised + .has(deps.storage, &network_monitor) + { + return Err(NymPerformanceContractError::AlreadyAuthorised { + address: network_monitor, + }); + } + + // insert the new entry and adjust the total count + self.network_monitors + .insert_new(deps.branch(), env, sender, &network_monitor)?; + + // finally, set submission metadata to disallow this NM from submitting data for epochs before it was authorised + let current_epoch_id = self.current_mixnet_epoch_id(deps.as_ref())?; + + self.performance_results.submission_metadata.save( + deps.storage, + &network_monitor, + &NetworkMonitorSubmissionMetadata { + last_submitted_epoch_id: current_epoch_id, + last_submitted_node_id: 0, + }, + )?; + Ok(()) + } + + pub fn retire_network_monitor( + &self, + deps: DepsMut, + env: Env, + sender: &Addr, + network_monitor: Addr, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + self.network_monitors + .retire(deps, &env, sender, &network_monitor) + } + + pub fn try_load_performance( + &self, + storage: &dyn Storage, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result, NymPerformanceContractError> { + Ok(self + .performance_results + .results + .may_load(storage, (epoch_id, node_id))? + .map(|r| r.median())) + } + + pub fn remove_node_measurements( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + self.ensure_is_admin(deps.as_ref(), sender)?; + + self.performance_results + .results + .remove(deps.storage, (epoch_id, node_id)); + Ok(()) + } + + pub fn remove_epoch_measurements( + &self, + deps: DepsMut, + sender: &Addr, + epoch_id: EpochId, + ) -> Result { + self.ensure_is_admin(deps.as_ref(), sender)?; + + // 1. purge the entries according to the limit + self.performance_results.results.prefix(epoch_id).clear( + deps.storage, + Some(retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT), + ); + + // 2. see if there's anything left + let additional_entries_to_remove_remaining = !self + .performance_results + .results + .prefix(epoch_id) + .is_empty(deps.storage); + + Ok(RemoveEpochMeasurementsResponse { + additional_entries_to_remove_remaining, + }) + } +} + +pub(crate) struct NetworkMonitorsStorage { + pub(crate) authorised_count: Item, + pub(crate) authorised: Map<&'static Addr, NetworkMonitorDetails>, + pub(crate) retired: Map<&'static Addr, RetiredNetworkMonitor>, +} + +impl NetworkMonitorsStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + NetworkMonitorsStorage { + authorised_count: Item::new(storage_keys::AUTHORISED_COUNT), + authorised: Map::new(storage_keys::AUTHORISED), + retired: Map::new(storage_keys::RETIRED), + } + } + + fn initialise(&self, deps: DepsMut) -> Result<(), NymPerformanceContractError> { + self.authorised_count.save(deps.storage, &0)?; + Ok(()) + } + + fn ensure_authorised( + &self, + storage: &dyn Storage, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + if !self.authorised.has(storage, addr) { + return Err(NymPerformanceContractError::NotAuthorised { + address: addr.clone(), + }); + } + Ok(()) + } + + fn insert_new( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + address: &Addr, + ) -> Result<(), NymPerformanceContractError> { + // if this address has already been retired in the past, restore it + self.retired.remove(deps.storage, address); + + self.authorised_count + .update(deps.storage, |authorised_count| { + Ok::<_, StdError>(authorised_count + 1) + })?; + self.authorised.save( + deps.storage, + address, + &NetworkMonitorDetails { + address: address.clone(), + authorised_by: sender.clone(), + authorised_at_height: env.block.height, + }, + )?; + Ok(()) + } + + fn retire( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + // NOTE: if the NM hasn't been authorised before, the `load` call will fail + // and thus `authorised_count` won't be updated (nor further code executed) + let details = self.authorised.load(deps.storage, addr)?; + self.authorised.remove(deps.storage, addr); + + self.authorised_count + .update(deps.storage, |authorised_count| { + Ok::<_, StdError>(authorised_count - 1) + })?; + + self.retired + .save(deps.storage, addr, &details.retire(env, sender))?; + Ok(()) + } +} + +pub(crate) struct PerformanceResultsStorage { + pub(crate) results: Map<(EpochId, NodeId), NodeResults>, + + // in order to ensure NM does not resubmit results, we keep metadata + // of the latest submitted information + // this requires them to submit everything sorted by node_id + pub(crate) submission_metadata: Map<&'static Addr, NetworkMonitorSubmissionMetadata>, +} + +impl PerformanceResultsStorage { + #[allow(clippy::new_without_default)] + const fn new() -> Self { + PerformanceResultsStorage { + results: Map::new(storage_keys::PERFORMANCE_RESULTS), + submission_metadata: Map::new(storage_keys::SUBMISSION_METADATA), + } + } + + // note: this method assumes authorisation has been checked and invariants validated + // (such as attempting to insert stale data) + fn insert_performance_data( + &self, + storage: &mut dyn Storage, + epoch_id: EpochId, + data: &NodePerformance, + ) -> Result<(), NymPerformanceContractError> { + let performance = data.performance; + + let key = (epoch_id, data.node_id); + let updated = match self.results.may_load(storage, key)? { + None => NodeResults::new(performance), + Some(mut existing) => { + existing.insert_new(performance); + existing + } + }; + + self.results.save(storage, key, &updated)?; + Ok(()) + } + + fn update_submission_metadata( + &self, + storage: &mut dyn Storage, + address: &Addr, + last_submitted_epoch_id: EpochId, + last_submitted_node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + self.submission_metadata.save( + storage, + address, + &NetworkMonitorSubmissionMetadata { + last_submitted_epoch_id, + last_submitted_node_id, + }, + )?; + Ok(()) + } + + fn ensure_non_stale_submission( + &self, + storage: &dyn Storage, + address: &Addr, + epoch_id: EpochId, + new_node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + let last_submission = self.submission_metadata.load(storage, address)?; + + // we can't submit data for past epochs + if last_submission.last_submitted_epoch_id > epoch_id { + return Err(NymPerformanceContractError::StalePerformanceSubmission { + epoch_id, + node_id: new_node_id, + last_epoch_id: last_submission.last_submitted_epoch_id, + last_node_id: last_submission.last_submitted_node_id, + }); + } + + // if we're submitting for the same epoch, the node id has to be greater than the previous one + if last_submission.last_submitted_epoch_id == epoch_id + && last_submission.last_submitted_node_id >= new_node_id + { + return Err(NymPerformanceContractError::StalePerformanceSubmission { + epoch_id, + node_id: new_node_id, + last_epoch_id: last_submission.last_submitted_epoch_id, + last_node_id: last_submission.last_submitted_node_id, + }); + } + // if we're submitting for new epoch, node id doesn't matter + Ok(()) + } +} + +pub mod retrieval_limits { + pub const NODE_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NODE_EPOCH_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_EPOCH_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NODE_EPOCH_MEASUREMENTS_DEFAULT_LIMIT: u32 = 50; + pub const NODE_EPOCH_MEASUREMENTS_MAX_LIMIT: u32 = 100; + + pub const NODE_HISTORICAL_PERFORMANCE_DEFAULT_LIMIT: u32 = 100; + pub const NODE_HISTORICAL_PERFORMANCE_MAX_LIMIT: u32 = 200; + + pub const NETWORK_MONITORS_DEFAULT_LIMIT: u32 = 50; + pub const NETWORK_MONITORS_MAX_LIMIT: u32 = 100; + + pub const RETIRED_NETWORK_MONITORS_DEFAULT_LIMIT: u32 = 50; + pub const RETIRED_NETWORK_MONITORS_MAX_LIMIT: u32 = 100; + + pub const EPOCH_PERFORMANCE_PURGE_LIMIT: usize = 200; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod performance_contract_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt, PreInitContract}; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + + #[cfg(test)] + mod initialisation { + use super::*; + use nym_contracts_common_testing::{ArbitraryContractStorageWriter, FullReader}; + + fn initialise_storage( + pre_init: &mut PreInitContract, + admin: Option, + ) -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + let env = pre_init.env(); + let admin = admin.unwrap_or(pre_init.addr_make("admin")); + let deps = pre_init.deps_mut(); + + storage.initialise(deps, env, admin, mixnet_contract.clone(), Vec::new())?; + Ok(()) + } + + #[test] + fn sets_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + let admin1 = pre_init.api.addr_make("first-admin"); + let admin2 = pre_init.api.addr_make("second-admin"); + + initialise_storage(&mut pre_init, Some(admin1.clone()))?; + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin1).is_ok()); + + let mut pre_init = PreInitContract::new(); + initialise_storage(&mut pre_init, Some(admin2.clone()))?; + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin2).is_ok()); + + Ok(()) + } + + #[test] + fn sets_provided_mixnet_contract_address() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + + let expected_mixnet_contract_address = pre_init.mixnet_contract_address.clone(); + let deps = pre_init.deps(); + let mixnet_contract = storage.mixnet_contract_address.load(deps.storage)?; + assert_eq!(expected_mixnet_contract_address, mixnet_contract); + Ok(()) + } + + #[test] + fn sets_initial_submission_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let env = pre_init.env(); + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + + let expected = LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: None, + }; + let data = storage.last_performance_submission.load(deps.storage)?; + assert_eq!(expected, data); + Ok(()) + } + + #[test] + fn retrieves_initial_epoch_id_from_mixnet_contract() -> anyhow::Result<()> { + // base case + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + assert_eq!(0, storage.mixnet_epoch_id_at_creation.load(deps.storage)?); + + // non-0 epoch + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + let address = pre_init.mixnet_contract_address.clone(); + + // advance the epoch few times... + let interval_details = pre_init + .querier() + .query_current_mixnet_interval(&address)? + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch() + .advance_epoch(); + + pre_init.set_contract_storage_value(&address, b"ci", &interval_details)?; + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + assert_eq!(7, storage.mixnet_epoch_id_at_creation.load(deps.storage)?); + + Ok(()) + } + + #[test] + fn authorises_provided_network_monitors() -> anyhow::Result<()> { + // no NM + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + + initialise_storage(&mut pre_init, None)?; + let deps = pre_init.deps(); + let authorised_count = storage + .network_monitors + .authorised_count + .load(deps.storage)?; + assert_eq!(authorised_count, 0); + + let authorised = storage + .network_monitors + .authorised + .all_values(deps.storage)?; + assert!(authorised.is_empty()); + + let mut pre_init = PreInitContract::new(); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + let env = pre_init.env(); + let admin = pre_init.addr_make("admin"); + let nm1 = pre_init.addr_make("nm1"); + let nm2 = pre_init.addr_make("nm2"); + + let deps = pre_init.deps_mut(); + storage.initialise( + deps, + env.clone(), + admin.clone(), + mixnet_contract.clone(), + vec![nm1.to_string(), nm2.to_string()], + )?; + + let deps = pre_init.deps(); + let authorised_count = storage + .network_monitors + .authorised_count + .load(deps.storage)?; + assert_eq!(authorised_count, 2); + + let authorised = storage + .network_monitors + .authorised + .all_values(deps.storage)?; + + let expected = vec![ + NetworkMonitorDetails { + address: nm1, + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + NetworkMonitorDetails { + address: nm2, + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + ]; + assert_eq!(authorised, expected); + + Ok(()) + } + } + + #[test] + fn getting_current_mixnet_epoch_id() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 0); + tester.advance_mixnet_epoch()?; + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 1); + + tester.set_mixnet_epoch(1000)?; + assert_eq!(storage.current_mixnet_epoch_id(tester.deps())?, 1000); + + Ok(()) + } + + #[cfg(test)] + mod submitting_performance_data { + use super::*; + + #[test] + fn is_only_allowed_by_authorised_network_monitors() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm1 = tester.addr_make("network-monitor-1"); + let nm2 = tester.addr_make("network-monitor-2"); + let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); + + tester.authorise_network_monitor(&nm1)?; + + // authorised network monitor can submit the results just fine + let perf = tester.dummy_node_performance(); + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm1, 0, perf) + .is_ok()); + + // unauthorised address is rejected + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: nm2.clone() + } + ); + + // it is fine after explicit authorisation though + tester.authorise_network_monitor(&nm2)?; + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm2, 0, perf) + .is_ok()); + + // and address that was never authorised still fails + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &unauthorised, 0, perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: unauthorised + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + // first submission + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .is_ok()); + + // second submission + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id1, + } + ); + + // another submission works fine + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) + .is_ok()); + + // original one works IF it's for next epoch + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 1, data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 1, + last_node_id: id1, + } + ); + + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, another_data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id2, + } + ); + + // check across epochs + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, data) + .is_ok()); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, data) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: id1, + last_epoch_id: 10, + last_node_id: id1, + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + tester.set_mixnet_epoch(10)?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + // if NM got authorised at epoch 10, it can only submit data for epochs >=10 + let perf = tester.dummy_node_performance(); + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 9, perf) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 10, perf) + .is_ok()); + assert!(storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 11, perf) + .is_ok()); + + Ok(()) + } + + #[test] + fn updates_submission_metadata() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, 0); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[0]); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[3]); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[1]); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 12345, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 12345); + assert_eq!(metadata.last_submitted_node_id, nodes[8]); + + Ok(()) + } + + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 0, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 1, + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + }), + } + ); + + storage.submit_performance_data( + tester.deps_mut(), + env.clone(), + &nm, + 12345, + NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 12345, + data: NodePerformance { + node_id: nodes[9], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + + #[test] + fn requires_associated_node_to_be_bonded() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let dummy_perf = NodePerformance { + node_id: 12345, + performance: Percent::from_percentage_value(69)?, + }; + + // no node bonded at this point + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + + // bonded nym-node + let node_id = tester.bond_dummy_nymnode()?; + let perf = NodePerformance { + node_id, + performance: Default::default(), + }; + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); + assert!(res.is_ok()); + + // unbonded + tester.unbond_nymnode(node_id)?; + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + + // bonded legacy mix-node + let node_id = tester.bond_dummy_legacy_mixnode()?; + let perf = NodePerformance { + node_id, + performance: Default::default(), + }; + let res = + storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); + assert!(res.is_ok()); + + // unbonded + tester.unbond_legacy_mixnode(node_id)?; + + let res = storage + .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NodeNotBonded { + node_id: dummy_perf.node_id + } + ); + Ok(()) + } + } + + #[cfg(test)] + mod batch_submitting_performance_data { + use super::*; + + #[test] + fn is_only_allowed_by_authorised_network_monitors() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm1 = tester.addr_make("network-monitor-1"); + let nm2 = tester.addr_make("network-monitor-2"); + let unauthorised = tester.addr_make("unauthorised"); + let env = tester.env(); + + tester.authorise_network_monitor(&nm1)?; + + let perf = tester.dummy_node_performance(); + // authorised network monitor can submit the results just fine + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm1, + 0, + vec![perf] + ) + .is_ok()); + + // unauthorised address is rejected + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf], + ) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: nm2.clone() + } + ); + + // it is fine after explicit authorisation though + tester.authorise_network_monitor(&nm2)?; + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm2, + 0, + vec![perf] + ) + .is_ok()); + + // and address that was never authorised still fails + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &unauthorised, + 0, + vec![perf], + ) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::NotAuthorised { + address: unauthorised + } + ); + Ok(()) + } + + #[test] + fn requires_sorted_list_of_performances() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let id3 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + let more_data = NodePerformance { + node_id: id3, + performance: Percent::hundred(), + }; + + let duplicates = vec![data, data]; + let another_dups = vec![another_data, another_data]; + let unsorted = vec![another_data, data]; + let semi_sorted = vec![data, more_data, another_data]; + let sorted = vec![data, another_data, more_data]; + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + duplicates, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + another_dups, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + unsorted, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + semi_sorted, + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::UnsortedBatchSubmission); + + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + sorted + ) + .is_ok()); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_same_node_again() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + // first submission + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data] + ) + .is_ok()); + + // second submission + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id1, + } + ); + + // another submission works fine + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) + .is_ok()); + + // original one works IF it's for next epoch + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![data] + ) + .is_ok()); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 1, + last_node_id: id1, + } + ); + + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_out_of_order() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let data = NodePerformance { + node_id: id1, + performance: Percent::hundred(), + }; + let another_data = NodePerformance { + node_id: id2, + performance: Percent::hundred(), + }; + + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![another_data] + ) + .is_ok()); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: id1, + last_epoch_id: 0, + last_node_id: id2, + } + ); + + // check across epochs + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![data] + ) + .is_ok()); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![data], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: id1, + last_epoch_id: 10, + last_node_id: id1, + } + ); + Ok(()) + } + + #[test] + fn its_not_possible_to_submit_data_for_past_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + tester.set_mixnet_epoch(10)?; + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let perf = tester.dummy_node_performance(); + + // if NM got authorised at epoch 10, it can only submit data for epochs >=10 + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![perf], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 0, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + let res = storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 9, + vec![perf], + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::StalePerformanceSubmission { + epoch_id: 9, + node_id: perf.node_id, + last_epoch_id: 10, + last_node_id: 0, + } + ); + + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 10, + vec![perf] + ) + .is_ok()); + assert!(storage + .batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 11, + vec![perf] + ) + .is_ok()); + + Ok(()) + } + + #[test] + fn updates_submission_metadata() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, 0); + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 0); + assert_eq!(metadata.last_submitted_node_id, nodes[0]); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[1]); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 1); + assert_eq!(metadata.last_submitted_node_id, nodes[4]); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[6], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let metadata = storage + .performance_results + .submission_metadata + .load(&tester, &nm)?; + assert_eq!(metadata.last_submitted_epoch_id, 2); + assert_eq!(metadata.last_submitted_node_id, nodes[8]); + + Ok(()) + } + + #[test] + fn updates_latest_submitted_information() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + let mut nodes = Vec::new(); + for _ in 0..10 { + nodes.push(tester.bond_dummy_nymnode()?); + } + + // single submission + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 0, + data: NodePerformance { + node_id: nodes[0], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + }), + } + ); + + // multiple submissions + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nodes[2], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[3], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 1, + data: NodePerformance { + node_id: nodes[4], + performance: Default::default(), + }, + }), + } + ); + + // another epoch + storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: nodes[1], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[7], + performance: Default::default(), + }, + NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + ], + )?; + let data = storage.last_performance_submission.load(&tester)?; + assert_eq!( + data, + LastSubmission { + block_height: env.block.height, + block_time: env.block.time, + data: Some(LastSubmittedData { + sender: nm.clone(), + epoch_id: 2, + data: NodePerformance { + node_id: nodes[8], + performance: Default::default(), + }, + }), + } + ); + + Ok(()) + } + + #[test] + fn informs_if_associated_node_is_not_bonded() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + // bond and unbond some nodes to advance the id counter + for _ in 0..10 { + let node_id = tester.bond_dummy_nymnode()?; + tester.unbond_nymnode(node_id)?; + } + + let nym_node1 = tester.bond_dummy_nymnode()?; + let nym_node_between = tester.bond_dummy_nymnode()?; + tester.unbond_nymnode(nym_node_between)?; + let nym_node2 = tester.bond_dummy_nymnode()?; + + let mix_node1 = tester.bond_dummy_legacy_mixnode()?; + let mixnode_between = tester.bond_dummy_legacy_mixnode()?; + tester.unbond_legacy_mixnode(mixnode_between)?; + let mix_node2 = tester.bond_dummy_legacy_mixnode()?; + + let env = tester.env(); + + // single id - nothing bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 0, + vec![NodePerformance { + node_id: 999999, + performance: Default::default(), + }], + )?; + assert_eq!(res.accepted_scores, 0); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // one bonded nym-node, one not bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 1, + vec![ + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: 999999, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 1); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // not-bonded, bonded, not-bonded, bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 2, + vec![ + NodePerformance { + node_id: 2, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node_between, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![2, nym_node_between]); + + // MIXNODES + + // one bonded mixnode, one not bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 3, + vec![ + NodePerformance { + node_id: mix_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: 999999, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 1); + assert_eq!(res.non_existent_nodes, vec![999999]); + + // not-bonded, bonded, not-bonded, bonded + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 4, + vec![ + NodePerformance { + node_id: 2, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: mixnode_between, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![2, mixnode_between]); + + // nym-node, not bonded, mixnode + let res = storage.batch_submit_performance_results( + tester.deps_mut(), + env.clone(), + &nm, + 5, + vec![ + NodePerformance { + node_id: 3, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node1, + performance: Default::default(), + }, + NodePerformance { + node_id: nym_node_between, + performance: Default::default(), + }, + NodePerformance { + node_id: mix_node2, + performance: Default::default(), + }, + ], + )?; + assert_eq!(res.accepted_scores, 2); + assert_eq!(res.non_existent_nodes, vec![3, nym_node_between]); + + Ok(()) + } + } + + #[test] + fn checking_for_admin() -> anyhow::Result<()> { + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + let admin = pre_init.api.addr_make("admin"); + let non_admin = pre_init.api.addr_make("non-admin"); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + + let storage = NymPerformanceContractStorage::new(); + + let deps = pre_init.deps_mut(); + storage.initialise(deps, env, admin.clone(), mixnet_contract, Vec::new())?; + + let deps = pre_init.deps(); + assert!(storage.is_admin(deps, &admin)?); + assert!(!storage.is_admin(deps, &non_admin)?); + + Ok(()) + } + + #[test] + fn ensuring_admin_privileges() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut pre_init = PreInitContract::new(); + let env = pre_init.env(); + + let admin = pre_init.api.addr_make("admin"); + let non_admin = pre_init.api.addr_make("non-admin"); + let mixnet_contract = pre_init.mixnet_contract_address.clone(); + + let deps = pre_init.deps_mut(); + storage.initialise(deps, env, admin.clone(), mixnet_contract, Vec::new())?; + + let deps = pre_init.deps(); + assert!(storage.ensure_is_admin(deps, &admin).is_ok()); + assert!(storage.ensure_is_admin(deps, &non_admin).is_err()); + + Ok(()) + } + + #[cfg(test)] + mod authorising_network_monitor { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::AdminExt; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, ¬_admin, nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + let another_nm = tester.addr_make("another-network-monitor"); + + // old one no longer works + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, another_nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .authorise_network_monitor(tester.deps_mut(), &env, &new_admin, another_nm) + .is_ok()); + Ok(()) + } + + #[test] + fn network_monitor_must_not_already_be_authorised() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let res = storage + .authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone()) + .unwrap_err(); + assert_eq!( + res, + NymPerformanceContractError::AlreadyAuthorised { address: nm } + ); + + Ok(()) + } + + #[test] + fn for_valid_network_monitor_storage_is_updated() -> anyhow::Result<()> { + // note: detailed invariants are checked in network_monitors_storage + // here we just want to ensure **something** happens (i.e. `insert_new` is called) + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 0); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 1); + + Ok(()) + } + + #[test] + fn initial_metadata_uses_current_mixnet_epoch() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm1.clone())?; + assert_eq!( + 0, + storage + .performance_results + .submission_metadata + .load(&tester, &nm1)? + .last_submitted_epoch_id + ); + + tester.advance_mixnet_epoch()?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; + assert_eq!( + 1, + storage + .performance_results + .submission_metadata + .load(&tester, &nm2)? + .last_submitted_epoch_id + ); + + tester.set_mixnet_epoch(1000)?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + assert_eq!( + 1000, + storage + .performance_results + .submission_metadata + .load(&tester, &nm3)? + .last_submitted_epoch_id + ); + + Ok(()) + } + } + + #[cfg(test)] + mod retiring_network_monitor { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::AdminExt; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let another_nm = tester.addr_make("another-network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + storage.authorise_network_monitor( + tester.deps_mut(), + &env, + &admin, + another_nm.clone(), + )?; + + let res = storage + .retire_network_monitor(tester.deps_mut(), env.clone(), ¬_admin, nm.clone()) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .retire_network_monitor(tester.deps_mut(), env.clone(), &admin, nm) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .retire_network_monitor( + tester.deps_mut(), + env.clone(), + &admin, + another_nm.clone(), + ) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .retire_network_monitor(tester.deps_mut(), env, &new_admin, another_nm) + .is_ok()); + + Ok(()) + } + + #[test] + fn for_valid_network_monitor_storage_is_updated() -> anyhow::Result<()> { + // note: detailed invariants are checked in network_monitors_storage + // here we just want to ensure **something** happens (i.e. `retire` is called) + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 1); + + storage.retire_network_monitor(tester.deps_mut(), env, &admin, nm)?; + + let current_authorised = storage.network_monitors.authorised_count.load(&tester)?; + assert_eq!(current_authorised, 0); + + Ok(()) + } + } + + #[test] + fn loading_performance_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + let admin = tester.admin_unchecked(); + let mut nms = Vec::new(); + + // pre-authorise some network monitors + for i in 0..6 { + let env = tester.env(); + let nm = tester.addr_make(&format!("network-monitor{i}")); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + nms.push(nm); + } + + // no results + let node_id = tester.bond_dummy_nymnode()?; + assert_eq!(storage.try_load_performance(&tester, 0, node_id)?, None); + + // + // always returns median value with 2decimal places precision + // + + // single result + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.42")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.42" + ); + + // two results (median doesn't require changing decimal places) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.50")?; + tester.insert_raw_performance(&nms[1], node_id, "0.40")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.45" + ); + + // two results (median requires changing decimal places) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.58")?; + tester.insert_raw_performance(&nms[1], node_id, "0.45")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.52" + ); + + // three results (median is the middle value rather than the average) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.12")?; + tester.insert_raw_performance(&nms[1], node_id, "0.34")?; + tester.insert_raw_performance(&nms[2], node_id, "0.56")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.34" + ); + + // five results (notice how they're not inserted sorted) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, "0.1")?; + tester.insert_raw_performance(&nms[5], node_id, "0.7")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.7" + ); + + // six results (same as above, but average of middle values) + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nms[0], node_id, "0.9")?; + tester.insert_raw_performance(&nms[1], node_id, "0.9")?; + tester.insert_raw_performance(&nms[2], node_id, "0.1")?; + tester.insert_raw_performance(&nms[3], node_id, "0.1")?; + tester.insert_raw_performance(&nms[4], node_id, "0.2")?; + tester.insert_raw_performance(&nms[5], node_id, "0.3")?; + assert_eq!( + storage + .try_load_performance(&tester, 0, node_id)? + .unwrap() + .value() + .to_string(), + "0.25" + ); + + Ok(()) + } + + #[cfg(test)] + mod removing_node_measurements { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::FullReader; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + let epoch_id = 0; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + let res = storage + .remove_node_measurements(tester.deps_mut(), ¬_admin, epoch_id, id1) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_node_measurements(tester.deps_mut(), &new_admin, epoch_id, id2) + .is_ok()); + + Ok(()) + } + + #[test] + fn is_noop_if_entry_didnt_exist() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let epoch_id = 0; + let node_id = 0; + + let before = storage.performance_results.results.all_values(&tester)?; + assert!(before.is_empty()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, node_id)?; + + let after = storage.performance_results.results.all_values(&tester)?; + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn removes_the_underlying_data() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + + let env = tester.env(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + let epoch_id = 0; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm1.clone())?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm2.clone())?; + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm3.clone())?; + + // single measurement + tester.insert_raw_performance(&nm1, id1, "0.42")?; + + let before = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id1))?; + assert!(before.is_some()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id1)?; + + let after = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id1))?; + assert!(after.is_none()); + + // multiple measurements + tester.insert_raw_performance(&nm1, id2, "0.42")?; + tester.insert_raw_performance(&nm2, id2, "0.69")?; + tester.insert_raw_performance(&nm3, id2, "1")?; + + let before = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id2))?; + assert!(before.is_some()); + + storage.remove_node_measurements(tester.deps_mut(), &admin, epoch_id, id2)?; + + let after = storage + .performance_results + .results + .may_load(&tester, (epoch_id, id2))?; + assert!(after.is_none()); + + Ok(()) + } + } + + #[cfg(test)] + mod removing_epoch_measurements { + use super::*; + use cw_controllers::AdminError::NotAdmin; + use nym_contracts_common_testing::FullReader; + + #[test] + fn can_only_be_performed_by_contract_admin() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let not_admin = tester.addr_make("not-admin"); + let nm = tester.addr_make("network-monitor"); + let env = tester.env(); + + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + + // epoch 0 + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + // epoch 1 + tester.advance_mixnet_epoch()?; + tester.insert_raw_performance(&nm, id1, "0.42")?; + tester.insert_raw_performance(&nm, id2, "0.42")?; + + let res = storage + .remove_epoch_measurements(tester.deps_mut(), ¬_admin, 0) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 0) + .is_ok()); + + // change admin + let new_admin = tester.addr_make("new-admin"); + tester.update_admin(&Some(new_admin.clone()))?; + + // old one no longer works + let res = storage + .remove_epoch_measurements(tester.deps_mut(), &admin, 1) + .unwrap_err(); + assert_eq!(res, NymPerformanceContractError::Admin(NotAdmin {})); + + assert!(storage + .remove_epoch_measurements(tester.deps_mut(), &new_admin, 1) + .is_ok()); + + Ok(()) + } + + #[test] + fn is_noop_for_empty_epochs() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let epoch_id = 0; + + let before = storage.performance_results.results.all_values(&tester)?; + assert!(before.is_empty()); + + storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + + let after = storage.performance_results.results.all_values(&tester)?; + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn removes_the_underlying_data_below_limit() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + + let env = tester.env(); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + // just few entries + let epoch_id = 0; + for _ in 0..10 { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let before = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + assert_eq!(before.len(), 10); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + // EXACT limit + let epoch_id = 1; + tester.advance_mixnet_epoch()?; + for _ in 0..retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + Ok(()) + } + + #[test] + fn indicates_need_for_further_calls_above_limit() -> anyhow::Result<()> { + let storage = NymPerformanceContractStorage::new(); + let mut tester = init_contract_tester(); + + let admin = tester.admin_unchecked(); + let nm = tester.addr_make("network-monitor"); + + let env = tester.env(); + storage.authorise_network_monitor(tester.deps_mut(), &env, &admin, nm.clone())?; + + // just few entries + let epoch_id = 0; + for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let before = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + assert_eq!( + before.len(), + 2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 + ); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert_eq!( + after.len(), + retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT + 50 + ); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert_eq!(after.len(), 50); + + let res = storage.remove_epoch_measurements(tester.deps_mut(), &admin, epoch_id)?; + assert!(!res.additional_entries_to_remove_remaining); + let after = storage + .performance_results + .results + .prefix(epoch_id) + .all_values(&tester)?; + + assert!(after.is_empty()); + + Ok(()) + } + } + } + + #[cfg(test)] + mod network_monitors_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + + #[test] + fn inserting_new_entry() -> anyhow::Result<()> { + let main_storage = NymPerformanceContractStorage::new(); + + let storage = NetworkMonitorsStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok()); + + // total authorised count is incremented + assert_eq!(storage.authorised_count.load(&tester)?, 1); + + // its current data is saved + assert_eq!( + storage.authorised.load(&tester, &nm1)?, + NetworkMonitorDetails { + address: nm1.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + } + ); + + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm2) + .is_ok()); + + assert_eq!(storage.authorised_count.load(&tester)?, 2); + assert_eq!( + storage.authorised.load(&tester, &nm2)?, + NetworkMonitorDetails { + address: nm2.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + } + ); + + main_storage.retire_network_monitor( + tester.deps_mut(), + env.clone(), + &admin, + nm1.clone(), + )?; + assert!(storage.retired.may_load(&tester, &nm1)?.is_some()); + + // if it was previously retired, that information is purged + assert!(storage + .insert_new(tester.deps_mut(), &env, &admin, &nm1) + .is_ok()); + + assert!(storage.retired.may_load(&tester, &nm1)?.is_none()); + + Ok(()) + } + + #[test] + fn retiring_existing_monitor() -> anyhow::Result<()> { + let storage = NetworkMonitorsStorage::new(); + let mut tester = init_contract_tester(); + let env = tester.env(); + + let admin = tester.admin_unchecked(); + let nm1 = tester.addr_make("network-monitor1"); + let nm2 = tester.addr_make("network-monitor2"); + let nm3 = tester.addr_make("network-monitor3"); + + tester.authorise_network_monitor(&nm1)?; + tester.authorise_network_monitor(&nm2)?; + + // fails on unauthorised NMs + assert!(storage + .retire(tester.deps_mut(), &env, &admin, &nm3) + .is_err()); + + assert_eq!(storage.authorised_count.load(&tester)?, 2); + + storage.retire(tester.deps_mut(), &env, &admin, &nm1)?; + + // total authorised count is decremented + assert_eq!(storage.authorised_count.load(&tester)?, 1); + + // data is removed + assert!(storage.authorised.may_load(&tester, &nm1)?.is_none()); + assert_eq!( + storage.retired.load(&tester, &nm1)?, + RetiredNetworkMonitor { + details: NetworkMonitorDetails { + address: nm1.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + retired_by: admin.clone(), + retired_at_height: env.block.height, + } + ); + + storage.retire(tester.deps_mut(), &env, &admin, &nm2)?; + + assert_eq!(storage.authorised_count.load(&tester)?, 0); + assert!(storage.authorised.may_load(&tester, &nm2)?.is_none()); + assert_eq!( + storage.retired.load(&tester, &nm2)?, + RetiredNetworkMonitor { + details: NetworkMonitorDetails { + address: nm2.clone(), + authorised_by: admin.clone(), + authorised_at_height: env.block.height, + }, + retired_by: admin.clone(), + retired_at_height: env.block.height, + } + ); + + Ok(()) + } + } + + #[cfg(test)] + mod performance_storage { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::ContractOpts; + use std::str::FromStr; + + #[test] + fn inserting_new_entry() -> anyhow::Result<()> { + // essentially make sure there are no silly bugs that epoch_id and node_id got accidentally mixed up + // when constructing map key... + let storage = PerformanceResultsStorage::new(); + let mut tester = init_contract_tester(); + + let node_id1 = 123; + let node_id2 = 456; + + let data1 = NodePerformance { + node_id: node_id1, + performance: Percent::from_str("0.23")?, + }; + + let data2 = NodePerformance { + node_id: node_id1, + performance: Percent::hundred(), + }; + + let data3 = NodePerformance { + node_id: node_id2, + performance: Percent::from_str("0.23643634")?, + }; + + let data4 = NodePerformance { + node_id: node_id2, + performance: Percent::hundred(), + }; + + assert!(storage.results.may_load(&tester, (1, node_id1))?.is_none()); + assert!(storage.results.may_load(&tester, (1, node_id2))?.is_none()); + + storage.insert_performance_data(&mut tester, 1, &data1)?; + assert_eq!( + tester.read_raw_scores(1, node_id1)?.inner(), + &[data1.performance] + ); + storage.insert_performance_data(&mut tester, 1, &data2)?; + assert_eq!( + tester.read_raw_scores(1, node_id1)?.inner(), + &[data1.performance, data2.performance] + ); + + storage.insert_performance_data(&mut tester, 1, &data3)?; + assert_eq!( + tester.read_raw_scores(1, node_id2)?.inner(), + &[data3.performance.round_to_two_decimal_places()] + ); + storage.insert_performance_data(&mut tester, 1, &data4)?; + assert_eq!( + tester.read_raw_scores(1, node_id2)?.inner(), + &[ + data3.performance.round_to_two_decimal_places(), + data4.performance + ] + ); + + storage.insert_performance_data(&mut tester, 2, &data2)?; + storage.insert_performance_data(&mut tester, 2, &data2)?; + assert_eq!( + tester.read_raw_scores(2, node_id1)?.inner(), + &[data2.performance, data2.performance] + ); + + storage.insert_performance_data(&mut tester, 2, &data4)?; + storage.insert_performance_data(&mut tester, 2, &data4)?; + assert_eq!( + tester.read_raw_scores(2, node_id2)?.inner(), + &[data4.performance, data4.performance] + ); + + Ok(()) + } + + #[test] + fn checking_for_submission_staleness() -> anyhow::Result<()> { + let storage = PerformanceResultsStorage::new(); + let mut tester = init_contract_tester(); + + let id1 = tester.bond_dummy_nymnode()?; + let id2 = tester.bond_dummy_nymnode()?; + let id3 = tester.bond_dummy_nymnode()?; + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + tester.insert_epoch_performance(&nm, 2, id2, Percent::hundred())?; + + // illegal to submit anything < than last used epoch + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 0, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1, id3) + .is_err()); + + // for the current epoch, node id has to be greater than what has already been submitted + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id1) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id2) + .is_err()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 2, id3) + .is_ok()); + + // and anything for future epochs is fine (as long as it's the first entry) + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 3, id1) + .is_ok()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 3, id2) + .is_ok()); + assert!(storage + .ensure_non_stale_submission(&tester, &nm, 1111, id3) + .is_ok()); + + Ok(()) + } + } +} diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs new file mode 100644 index 0000000000..4f8e7cf4bd --- /dev/null +++ b/contracts/performance/src/testing/mod.rs @@ -0,0 +1,608 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::{execute, instantiate, migrate, query}; +use crate::helpers::MixnetContractQuerier; +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use cosmwasm_std::testing::{message_info, mock_env, MockApi}; +use cosmwasm_std::{ + coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, + StdError, StdResult, +}; +use cw_storage_plus::PrimaryKey; +use mixnet_contract::testable_mixnet_contract::MixnetContract; +use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; +use nym_contracts_common::Percent; +use nym_contracts_common_testing::{ + addr, AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, + ChainOpts, CommonStorageKeys, ContractFn, ContractOpts, ContractStorageWrapper, ContractTester, + ContractTesterBuilder, DenomExt, PermissionedFn, QueryFn, RandExt, TestableNymContract, + TEST_DENOM, +}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role}; +use nym_mixnet_contract_common::{ + CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse, + NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment, + SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_performance_contract_common::constants::storage_keys; +use nym_performance_contract_common::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NodeId, NodePerformance, NodeResults, + NymPerformanceContractError, QueryMsg, +}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +pub struct PerformanceContract; + +impl TestableNymContract for PerformanceContract { + const NAME: &'static str = "performance-contract"; + type InitMsg = InstantiateMsg; + type ExecuteMsg = ExecuteMsg; + type QueryMsg = QueryMsg; + type MigrateMsg = MigrateMsg; + type ContractError = NymPerformanceContractError; + + fn instantiate() -> ContractFn { + instantiate + } + + fn execute() -> ContractFn { + execute + } + + fn query() -> QueryFn { + query + } + + fn migrate() -> PermissionedFn { + migrate + } + + fn base_init_msg() -> Self::InitMsg { + InstantiateMsg { + mixnet_contract_address: addr("mixnet-contract").to_string(), + authorised_network_monitors: vec![], + } + } + + fn init() -> ContractTester + where + Self: Sized, + { + let builder = ContractTesterBuilder::new().instantiate::(None); + + // we just instantiated it + let mixnet_address = builder + .well_known_contracts + .get(MixnetContract::NAME) + .unwrap() + .clone(); + + builder + .instantiate::(Some(InstantiateMsg { + mixnet_contract_address: mixnet_address.to_string(), + authorised_network_monitors: vec![], + })) + .build() + } +} + +pub fn init_contract_tester() -> ContractTester { + PerformanceContract::init() + .with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN) +} + +// we need to be able to test instantiation, but for that we require +// deps in a state that already includes instantiated mixnet contract +pub(crate) struct PreInitContract { + tester_builder: ContractTesterBuilder, + pub(crate) mixnet_contract_address: Addr, + pub(crate) api: MockApi, + storage: ContractStorageWrapper, + placeholder_address: Addr, +} + +#[allow(dead_code)] +impl PreInitContract { + pub(crate) fn new() -> PreInitContract { + let tester_builder = + ContractTesterBuilder::::new().instantiate::(None); + + let mixnet_contract = tester_builder + .well_known_contracts + .get(&MixnetContract::NAME) + .unwrap(); + + let api = tester_builder.api(); + let placeholder_address = api.addr_make("to-be-performance-contract"); + + let storage = tester_builder.contract_storage_wrapper(&placeholder_address); + + PreInitContract { + mixnet_contract_address: mixnet_contract.clone(), + tester_builder, + api, + storage, + placeholder_address, + } + } + + pub(crate) fn deps(&self) -> Deps { + Deps { + storage: &self.storage, + api: &self.api, + querier: self.tester_builder.querier(), + } + } + + pub(crate) fn deps_mut(&mut self) -> DepsMut { + DepsMut { + storage: &mut self.storage, + api: &self.api, + querier: self.tester_builder.querier(), + } + } + + pub(crate) fn querier(&self) -> QuerierWrapper { + self.tester_builder.querier() + } + + pub(crate) fn env(&self) -> Env { + Env { + contract: ContractInfo { + address: self.placeholder_address.clone(), + }, + ..mock_env() + } + } + + pub(crate) fn addr_make(&self, input: &str) -> Addr { + self.api.addr_make(input) + } + + pub(crate) fn write_to_mixnet_contract_storage( + &mut self, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) -> StdResult<()> { + let address = NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage)?; + + self.set_contract_storage(address, key, value); + Ok(()) + } + + pub(crate) fn write_to_mixnet_contract_storage_value( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + let address = NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage)?; + + self.set_contract_storage_value(address, key, value) + } +} + +impl ArbitraryContractStorageWriter for PreInitContract { + fn set_contract_storage( + &mut self, + address: impl Into, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) { + self.storage + .as_inner_storage_mut() + .set_contract_storage(address, key, value); + } +} + +#[allow(dead_code)] +pub(crate) trait PerformanceContractTesterExt: + ContractOpts< + ExecuteMsg = ExecuteMsg, + QueryMsg = QueryMsg, + ContractError = NymPerformanceContractError, + > + ChainOpts + + AdminExt + + DenomExt + + RandExt + + BankExt + + ArbitraryContractStorageReader + + ArbitraryContractStorageWriter +{ + fn mixnet_contract_address(&self) -> StdResult { + NYM_PERFORMANCE_CONTRACT_STORAGE + .mixnet_contract_address + .load(self.deps().storage) + } + + fn execute_mixnet_contract( + &mut self, + sender: MessageInfo, + msg: &nym_mixnet_contract_common::ExecuteMsg, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + self.execute_arbitrary_contract(address, sender, msg) + .map_err(|err| { + StdError::generic_err(format!("mixnet contract execution failure: {err}")) + })?; + Ok(()) + } + + fn read_from_mixnet_contract_storage( + &self, + key: impl AsRef<[u8]>, + ) -> StdResult { + let address = self.mixnet_contract_address()?; + + self.must_read_value_from_contract_storage(address, key) + } + + fn write_to_mixnet_contract_storage( + &mut self, + key: impl AsRef<[u8]>, + value: impl AsRef<[u8]>, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + ::set_contract_storage(self, address, key, value); + Ok(()) + } + + fn write_to_mixnet_contract_storage_value( + &mut self, + key: impl AsRef<[u8]>, + value: &T, + ) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + self.set_contract_storage_value(address, key, value) + } + + fn current_mixnet_epoch(&self) -> StdResult { + let address = self.mixnet_contract_address()?; + + Ok(self + .deps() + .querier + .query_current_mixnet_interval(address.clone())? + .current_epoch_absolute_id()) + } + + fn advance_mixnet_epoch(&mut self) -> StdResult<()> { + let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetCurrentIntervalDetails {}, + )?; + let until_end = interval_details.time_until_current_epoch_end().as_secs(); + let timestamp = self.env().block.time.plus_seconds(until_end + 1); + self.set_block_time(timestamp); + self.next_block(); + + // this was hardcoded in mixnet init + let mixnet_rewarder = self.addr_make("rewarder"); + let rewarder = message_info(&mixnet_rewarder, &[]); + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::BeginEpochTransition {}, + )?; + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::ReconcileEpochEvents { limit: None }, + )?; + + for role in [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ] { + self.execute_mixnet_contract( + rewarder.clone(), + &nym_mixnet_contract_common::ExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role, + nodes: vec![], + }, + }, + )?; + } + Ok(()) + } + + fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> { + let address = self.mixnet_contract_address()?; + + let interval = self + .deps() + .querier + .query_current_mixnet_interval(address.clone())?; + + let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id { + interval + } else { + Interval::init_interval( + interval.epochs_in_interval(), + interval.epoch_length(), + &mock_env(), + ) + }; + + let current = to_update.current_epoch_absolute_id(); + let diff = epoch_id - current; + for _ in 0..diff { + to_update = to_update.advance_epoch(); + } + self.set_contract_storage_value(&address, b"ci", &to_update) + } + + fn authorise_network_monitor( + &mut self, + addr: &Addr, + ) -> Result<(), NymPerformanceContractError> { + let admin = self.admin_unchecked(); + self.execute_raw( + admin, + ExecuteMsg::AuthoriseNetworkMonitor { + address: addr.to_string(), + }, + )?; + Ok(()) + } + + fn dummy_node_performance(&mut self) -> NodePerformance { + let node_id = self.bond_dummy_nymnode().unwrap(); + NodePerformance { + node_id, + performance: Percent::from_percentage_value(69).unwrap(), + } + } + + fn retire_network_monitor(&mut self, addr: &Addr) -> Result<(), NymPerformanceContractError> { + let admin = self.admin_unchecked(); + self.execute_raw( + admin, + ExecuteMsg::RetireNetworkMonitor { + address: addr.to_string(), + }, + )?; + Ok(()) + } + + fn insert_epoch_performance( + &mut self, + addr: &Addr, + epoch_id: EpochId, + node_id: NodeId, + performance: Percent, + ) -> Result<(), NymPerformanceContractError> { + let env = self.env(); + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + self.deps_mut(), + env, + addr, + epoch_id, + NodePerformance { + node_id, + performance, + }, + ) + } + + fn insert_performance( + &mut self, + addr: &Addr, + node_id: NodeId, + performance: Percent, + ) -> Result<(), NymPerformanceContractError> { + let epoch_id = self.current_mixnet_epoch()?; + + self.insert_epoch_performance(addr, epoch_id, node_id, performance) + } + + // makes testing easier + fn insert_raw_performance( + &mut self, + addr: &Addr, + node_id: NodeId, + raw: &str, + ) -> Result<(), NymPerformanceContractError> { + self.insert_performance( + addr, + node_id, + Percent::from_str(raw).map_err(|err| { + NymPerformanceContractError::StdErr(StdError::parse_err("Percent", err.to_string())) + })?, + ) + } + + fn read_raw_scores( + &self, + epoch_id: EpochId, + node_id: NodeId, + ) -> Result { + let scores = NYM_PERFORMANCE_CONTRACT_STORAGE + .performance_results + .results + .load(self.deps().storage, (epoch_id, node_id))?; + Ok(scores) + } + + fn bond_dummy_nymnode(&mut self) -> Result { + let node_owner = self.generate_account_with_balance(); + let pledge = coins(100_000000, TEST_DENOM); + let keypair = ed25519::KeyPair::new(self.raw_rng()); + let identity_key = keypair.public_key().to_base58_string(); + + let node = NymNode { + host: "1.2.3.4".to_string(), + custom_http_port: None, + identity_key, + }; + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), + }; + // initial signing nonce is 0 for a new address + let signing_nonce = 0; + + let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone()); + let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload); + let msg = SignableNymNodeBondingMsg::new(signing_nonce, content); + + let owner_signature = keypair.private_key().sign(msg.to_plaintext()?); + let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref()); + + self.execute_mixnet_contract( + message_info(&node_owner, &pledge), + &nym_mixnet_contract_common::ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + }, + )?; + + let bond: NodeOwnershipResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetOwnedNymNode { + address: node_owner.to_string(), + }, + )?; + + Ok(bond.details.unwrap().bond_information.node_id) + } + + fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), NymPerformanceContractError> { + let bond: NodeDetailsResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetNymNodeDetails { node_id }, + )?; + + let node_owner = bond.details.unwrap().bond_information.owner; + + self.execute_mixnet_contract( + message_info(&node_owner, &[]), + &nym_mixnet_contract_common::ExecuteMsg::UnbondNymNode {}, + )?; + + self.advance_mixnet_epoch()?; + Ok(()) + } + + fn bond_dummy_legacy_mixnode(&mut self) -> Result { + #[derive(Deserialize, Serialize)] + pub(crate) struct UniqueRef { + // note, we collapse the pk - combining everything under the namespace - even if it is composite + pk: Binary, + value: T, + } + + // there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit, + // ensuring all invariants still hold + let owner = self.generate_account_with_balance(); + + let mixnode = MixNode { + host: "1.2.3.4".to_string(), + mix_port: 123, + verloc_port: 123, + http_api_port: 123, + sphinx_key: "aaaa".to_string(), + identity_key: "bbbbb".to_string(), + version: "ccc".to_string(), + }; + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), + }; + + // adjust node counter + let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?; + let node_id = node_id_counter + 1; + self.write_to_mixnet_contract_storage_value("nic", &node_id)?; + + let current_epoch = self.current_mixnet_epoch()?; + let pledge = coin(100_000000, TEST_DENOM); + let mixnode_rewarding = + NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap(); + let env = self.env(); + let mixnode_bond = MixNodeBond { + mix_id: node_id, + owner, + original_pledge: pledge, + mix_node: mixnode, + proxy: None, + bonding_height: env.block.height, + is_unbonding: false, + }; + + // save to the main mixnode storage + self.set_contract_map_value( + self.mixnet_contract_address()?, + "mnn", + node_id, + &mixnode_bond, + )?; + // update indices + let pk = node_id.joined_key(); + let unique_ref = UniqueRef { + pk: pk.into(), + value: mixnode_bond.clone(), + }; + + // owner index + let idx = mixnode_bond.owner.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?; + + // identity key index + let idx = mixnode_bond.mix_node.identity_key.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?; + + // sphinx key index + let idx = mixnode_bond.mix_node.sphinx_key.clone(); + self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?; + + // update rewarding data + self.set_contract_map_value( + self.mixnet_contract_address()?, + "mnr", + node_id, + &mixnode_rewarding, + )?; + + Ok(node_id) + } + + fn unbond_legacy_mixnode( + &mut self, + node_id: NodeId, + ) -> Result<(), NymPerformanceContractError> { + let bond: MixnodeDetailsResponse = self.query_arbitrary_contract( + self.mixnet_contract_address()?, + &nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id }, + )?; + + let node_owner = bond.mixnode_details.unwrap().bond_information.owner; + + self.execute_mixnet_contract( + message_info(&node_owner, &[]), + &nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {}, + )?; + + self.advance_mixnet_epoch()?; + Ok(()) + } +} + +impl PerformanceContractTesterExt for ContractTester {} diff --git a/contracts/performance/src/transactions.rs b/contracts/performance/src/transactions.rs new file mode 100644 index 0000000000..86a2d99772 --- /dev/null +++ b/contracts/performance/src/transactions.rs @@ -0,0 +1,314 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; +use cosmwasm_std::{to_json_binary, DepsMut, Env, Event, MessageInfo, Response}; +use nym_performance_contract_common::{ + EpochId, NodeId, NodePerformance, NymPerformanceContractError, +}; + +pub fn try_update_contract_admin( + deps: DepsMut<'_>, + info: MessageInfo, + new_admin: String, +) -> Result { + let new_admin = deps.api.addr_validate(&new_admin)?; + + let res = NYM_PERFORMANCE_CONTRACT_STORAGE + .contract_admin + .execute_update_admin(deps, info, Some(new_admin))?; + + Ok(res) +} + +pub fn try_submit_performance_results( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + epoch_id: EpochId, + data: NodePerformance, +) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE.submit_performance_data( + deps, + env, + &info.sender, + epoch_id, + data, + )?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_batch_submit_performance_results( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + epoch_id: EpochId, + data: Vec, +) -> Result { + let res = NYM_PERFORMANCE_CONTRACT_STORAGE.batch_submit_performance_results( + deps, + env, + &info.sender, + epoch_id, + data, + )?; + + let response = Response::new().set_data(to_json_binary(&res)?).add_event( + Event::new("batch_performance_submission") + .add_attribute("accepted_scores", res.accepted_scores.to_string()) + .add_attribute( + "non_existent_nodes", + format!("{:?}", res.non_existent_nodes), + ), + ); + Ok(response) +} + +pub fn try_authorise_network_monitor( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.authorise_network_monitor( + deps, + &env, + &info.sender, + address, + )?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_retire_network_monitor( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + + NYM_PERFORMANCE_CONTRACT_STORAGE.retire_network_monitor(deps, env, &info.sender, address)?; + + // TODO: emit events + Ok(Response::new()) +} + +pub fn try_remove_node_measurements( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, + node_id: NodeId, +) -> Result { + NYM_PERFORMANCE_CONTRACT_STORAGE.remove_node_measurements( + deps, + &info.sender, + epoch_id, + node_id, + )?; + + Ok(Response::new()) +} + +pub fn try_remove_epoch_measurements( + deps: DepsMut<'_>, + info: MessageInfo, + epoch_id: EpochId, +) -> Result { + let res = + NYM_PERFORMANCE_CONTRACT_STORAGE.remove_epoch_measurements(deps, &info.sender, epoch_id)?; + + Ok(Response::new().set_data(to_json_binary(&res)?)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::retrieval_limits; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use cosmwasm_std::from_json; + use nym_contracts_common_testing::{AdminExt, ContractOpts}; + use nym_performance_contract_common::RemoveEpochMeasurementsResponse; + + #[cfg(test)] + mod updating_contract_admin { + use super::*; + use crate::testing::init_contract_tester; + use cw_controllers::AdminError; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + use nym_performance_contract_common::ExecuteMsg; + + #[test] + fn can_only_be_performed_by_current_admin() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let random_acc = test.generate_account(); + let new_admin = test.generate_account(); + let res = test + .execute_raw( + random_acc, + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + ) + .unwrap_err(); + + assert_eq!( + res, + NymPerformanceContractError::Admin(AdminError::NotAdmin {}) + ); + + let actual_admin = test.admin_unchecked(); + let res = test.execute_raw( + actual_admin.clone(), + ExecuteMsg::UpdateAdmin { + admin: new_admin.to_string(), + }, + ); + assert!(res.is_ok()); + + let updated_admin = test.admin_unchecked(); + assert_eq!(new_admin, updated_admin); + + Ok(()) + } + + #[test] + fn requires_providing_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_account = "definitely-not-valid-account"; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: bad_account.to_string(), + }, + ); + + assert!(res.is_err()); + + let empty_account = ""; + let res = test.execute_raw( + test.admin_unchecked(), + ExecuteMsg::UpdateAdmin { + admin: empty_account.to_string(), + }, + ); + + assert!(res.is_err()); + + Ok(()) + } + } + + #[cfg(test)] + mod authorising_network_monitor { + use super::*; + use crate::testing::init_contract_tester; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + + #[test] + fn requires_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_address = "foomp".to_string(); + let good_address = test.generate_account(); + + let env = test.env(); + let admin = test.admin_msg(); + + assert!(try_authorise_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err()); + assert!(try_authorise_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + #[cfg(test)] + mod retiring_network_monitor { + use super::*; + use crate::testing::{init_contract_tester, PerformanceContractTesterExt}; + use nym_contracts_common_testing::{AdminExt, ContractOpts, RandExt}; + + #[test] + fn requires_valid_address() -> anyhow::Result<()> { + let mut test = init_contract_tester(); + + let bad_address = "foomp".to_string(); + let good_address = test.generate_account(); + test.authorise_network_monitor(&good_address)?; + + let env = test.env(); + let admin = test.admin_msg(); + + assert!(try_retire_network_monitor( + test.deps_mut(), + env.clone(), + admin.clone(), + bad_address + ) + .is_err()); + assert!(try_retire_network_monitor( + test.deps_mut(), + env, + admin, + good_address.to_string() + ) + .is_ok()); + + Ok(()) + } + } + + // panics in tests are fine... + #[allow(clippy::panic)] + #[test] + fn removing_epoch_measurements_returns_binary_data() -> anyhow::Result<()> { + let mut tester = init_contract_tester(); + + let nm = tester.addr_make("network-monitor"); + tester.authorise_network_monitor(&nm)?; + + tester.advance_mixnet_epoch()?; + for _ in 0..2 * retrieval_limits::EPOCH_PERFORMANCE_PURGE_LIMIT { + let node_id = tester.bond_dummy_nymnode()?; + tester.insert_raw_performance(&nm, node_id, "0.42")?; + } + + let admin = tester.admin_msg(); + let res = try_remove_epoch_measurements(tester.deps_mut(), admin.clone(), 0)?; + + let Some(data) = res.data else { + panic!("missing binary response"); + }; + let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?; + assert!(!deserialised.additional_entries_to_remove_remaining); + + let res = try_remove_epoch_measurements(tester.deps_mut(), admin, 1)?; + + let Some(data) = res.data else { + panic!("missing binary response"); + }; + let deserialised: RemoveEpochMeasurementsResponse = from_json(&data)?; + assert!(deserialised.additional_entries_to_remove_remaining); + + Ok(()) + } +} diff --git a/deny.toml b/deny.toml index 5bd402997e..4809f4e766 100644 --- a/deny.toml +++ b/deny.toml @@ -104,6 +104,7 @@ allow = [ "Unicode-3.0", "OpenSSL", "Zlib", + "CDLA-Permissive-2.0", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the @@ -118,7 +119,6 @@ exceptions = [ #{ allow = ["Zlib"], crate = "adler32" }, { allow = ["GPL-3.0"], crate = "nym-api" }, { allow = ["GPL-3.0"], crate = "nym-gateway" }, - { allow = ["GPL-3.0"], crate = "nym-mixnode" }, { allow = ["GPL-3.0"], crate = "nym-network-requester" }, { allow = ["GPL-3.0"], crate = "nym-node" }, { allow = ["GPL-3.0"], crate = "nym-validator-rewarder" }, diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 7108925d69..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,136 +0,0 @@ -version: "3.7" - -x-network: &NETWORK - BECH32_PREFIX: nymt - DENOM: nymt - STAKE_DENOM: nyxt - WASMD_VERSION: v0.27.0 - -services: - genesis_validator: - build: - context: docker/validator - args: *NETWORK - image: validator:latest - ports: - - "26657:26657" - - "1317:1317" - container_name: genesis_validator - volumes: - - "genesis_volume:/genesis_volume" - - "genesis_nyxd:/root/.nyxd" - environment: *NETWORK - networks: - localnet: - ipv4_address: 172.168.10.2 - command: ["genesis"] - secondary_validator: - build: - context: docker/validator - args: *NETWORK - image: validator:latest - ports: - - "36657:26657" - - "2317:1317" - volumes: - - "genesis_volume:/genesis_volume" - - "secondary_nyxd:/root/.nyxd" - environment: *NETWORK - networks: - localnet: - ipv4_address: 172.168.10.3 - depends_on: - - "genesis_validator" - command: ["secondary"] - # mixnet_contract: - # build: docker/mixnet_contract - # image: contract:latest - # volumes: - # - ".:/nym" - # vesting_contract: - # build: docker/vesting_contract - # image: vesting_contract:latest - # volumes: - # - ".:/nym" - # contract_uploader: - # build: docker/typescript_client - # image: contract_uploader:typescript - # volumes: - # - "genesis_volume:/genesis_volume:ro" - # - "contract_volume:/contract_volume" - # - ".:/nym" - # depends_on: - # - "genesis_validator" - # - "secondary_validator" - # - "mixnet_contract" - # environment: - # BECH32_PREFIX: *BECH32_PREFIX - mnemonic_echo: - build: docker/mnemonic_echo - image: mnemonic_echo:latest - volumes: - - "genesis_volume:/genesis_volume:ro" - depends_on: - - "genesis_validator" - - "secondary_validator" - - # mongo: - # image: mongo:latest - # command: - # - --storageEngine=wiredTiger - # volumes: - # - mongo_data:/data/db - # block_explorer: - # build: - # context: https://github.com/forbole/big-dipper.git#v0.41.x-7 - # image: block_explorer:v0.41.x-7 - # ports: - # - "3080:3000" - # depends_on: - # - "mongo" - # environment: - # ROOT_URL: ${APP_ROOT_URL:-http://localhost} - # MONGO_URL: mongodb://mongo:27017/meteor - # PORT: 3000 - # METEOR_SETTINGS: ${METEOR_SETTINGS} - # explorer: - # build: - # context: docker/explorer - # image: explorer:latest - # ports: - # - "3040:3000" - # depends_on: - # - "genesis_validator" - # - "block_explorer" - - # service to update geoip binary database, for explorer-api - geoipupdate: - container_name: geoipupdate - image: maxmindinc/geoipupdate - restart: unless-stopped - environment: - GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID} - GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY} - GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS} - GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY} - networks: - - geoipupdate - volumes: - - ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP - -volumes: - genesis_volume: - genesis_nyxd: - secondary_nyxd: - -# contract_volume: -# mongo_data: - -networks: - geoipupdate: - localnet: - driver: bridge - ipam: - driver: default - config: - - subnet: 172.168.10.0/25 diff --git a/docker/README.md b/docker/README.md index 3b8899ad55..26d5e46679 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,45 +1,139 @@ -## Build with Docker & Docker Compose +## Nym Validator -Currently you can build and run locally in a Docker containers the following components of the Nym Privacy Platform: +This contains the configuration needed to run a Nym validator using Docker -* One genesis validator -* Any number of secondary validators -* A contract uploader, that uploads the contract built from the local sources -* The web wallet application, accessible on port 3000 -* The block explorer application, accessible on port 3080 -* The network explorer application, accessible on port 3040, for registered users +> **SECURITY**: This runs the validator as the root user inside the container for simplicity and development purposes. This setup should NOT be used in any other fashion. -### Running +### Initial Setup -The following commands need to be run from the root of the Nym git project. - -To build the entire dockerized environment, run the following command: +Before building and running the validator, you'll need to create the required directories: ``` -PERSONAL_TOKEN=[network explorer token] docker-compose build +# Create required directories +mkdir -p data/validator data/addresses +chmod -R 777 data ``` -or build each service separately, as changes are made to their relevant source code. -**Note** network-explorer build time is currently very high, so building it more than once is not advisable. +### Running on Apple (Mx) Macs with Colima -To start the dockerized environment, run the following command: +When running on Apple Silicon Macs, we are using Colima with x86_64 emulation to properly run the validator: +1. Install Colima ``` -METEOR_SETTINGS=$(cat docker/block_explorer/settings.json) docker-compose up -d --scale=secondary_validator=3 +brew install colima ``` -**Note**: The `secondary_validator=3` can take any other number as value, depending on the desired setup. - -The web wallet interface will become available at `localhost:3000`. - -The mnemonic needed to connect to the admin user, which also has a pre-added number of tokens, can be obtained by running: - +2. Set up a Colima VM with x86_64 architecture and Rosetta support: ``` -docker logs nym_mnemonic_echo_1 +# Create a Colima VM with x86_64 architecture +colima start nym-validator --arch x86_64 --cpu 4 --memory 8 --disk 20 --vm-type=vz --vz-rosetta --mount-type=virtiofs ``` -To stop the dockerized environment, run: +3. Build and start the validator: +``` +# Make sure you're using the Colima context +docker context use colima-nym-validator + +# Build the validator +docker-compose build validator + +# Start the validator +docker-compose up -d validator +``` + +### Standard Operation (Intel/AMD x86_64 Systems) + +For standard x86_64 systems: + +``` +# Build the validator +docker-compose build validator + +# Start the validator +docker-compose up -d validator +``` + +The genesis validator will be initialized with the network configuration defined in the `docker-compose.yml` file. + +### Managing the Validator + +To check the validator logs: + +``` +docker logs -f validator +``` + +To get the admin mnemonic: + +``` +docker exec validator cat /root/output/node_admin_mnemonic +``` + +To stop the validator: ``` docker-compose down ``` + +### Terminating and Cleaning Up Validator Data + +If you need to completely terminate your validator and remove all associated data: + +``` +# Stop the containers +docker-compose down + +# Remove the volumes +docker-compose down -v + +# Delete the data directories +rm -rf data + +# If you want to start fresh, recreate the directories +mkdir -p data/validator/config data/addresses +chmod -R 777 data +``` + +This will completely remove all blockchain state, keys, and configuration, allowing you to start with a clean validator instance + +### Using nym-cli for Smart Contract Operations + +The nym-cli utility can be used to manage and execute WASM smart contracts. You can access the CLI from within the validator container: + +``` +docker exec -it validator ./nym-cli cosmwasm --help +``` + +#### Available Commands: + +- **upload**: Upload a smart contract WASM blob +- **init**: Init a WASM smart contract +- **generate-init-message**: Generate an instantiate message +- **migrate**: Migrate a WASM smart contract +- **execute**: Execute a WASM smart contract method +- **raw-contract-state**: Obtain raw contract state of a cosmwasm smart contract + +#### Example Usage: + +To upload a contract: + +``` +docker exec -it validator ./nym-cli cosmwasm upload \ + --mnemonic $(cat /root/output/node_admin_mnemonic) \ + --wasm-file /path/to/contract.wasm +``` + +To initialize a contract: + +``` +docker exec -it validator ./nym-cli cosmwasm init \ + --mnemonic $(cat /root/output/node_admin_mnemonic) \ + --code-id \ + --init-msg '{"key": "value"}' +``` + +For more detailed options, use the help command: + +``` +docker exec -it validator ./nym-cli cosmwasm --help +``` \ No newline at end of file diff --git a/docker/block_explorer/settings.json b/docker/block_explorer/settings.json deleted file mode 100644 index 6b5e8558c0..0000000000 --- a/docker/block_explorer/settings.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "public":{ - "chainName": "Nym", - "chainId": "nymnet", - "slashingWindow": 10000, - "uptimeWindow": 250, - "initialPageSize": 30, - "secp256k1": false, - "bech32PrefixAccAddr": "punk", - "bech32PrefixAccPub": "punkpub", - "bech32PrefixValAddr": "punkvaloper", - "bech32PrefixValPub": "punkvaloperpub", - "bech32PrefixConsAddr": "punkvalcons", - "bech32PrefixConsPub": "punkvalconspub", - "bondDenom": "punk", - "powerReduction": 1000000, - "coins": [ - { - "denom": "upunk", - "displayName": "PUNK", - "fraction": 1000000 - }, - { - "denom": "punk", - "displayName": "PUNK", - "fraction": 1000000 - } - ], - "ledger":{ - "coinType": 118, - "appName": "punk", - "appVersion": "2.16.0", - "gasPrice": 0.025 - }, - "modules": { - "bank": true, - "supply": true, - "minting": false, - "gov": true, - "distribution": false - }, - "coingeckoId": "punk", - "networks": "https://gist.githubusercontent.com/kwunyeung/8be4598c77c61e497dfc7220a678b3ee/raw/bd-networks.json", - "banners": false - }, - "remote":{ - "rpc":"http://genesis_validator:26657", - "api":"http://genesis_validator:1317" - }, - "debug": { - "startTimer": true - }, - "params":{ - "startHeight": 0, - "defaultBlockTime": 5000, - "validatorUpdateWindow": 300, - "blockInterval": 15000, - "transactionsInterval": 18000, - "keybaseFetchingInterval": 18000000, - "consensusInterval": 1000, - "statusInterval":7500, - "signingInfoInterval": 1800000, - "proposalInterval": 5000, - "missedBlocksInterval": 60000, - "delegationInterval": 900000 - } -} - - diff --git a/docker/explorer/Dockerfile b/docker/explorer/Dockerfile deleted file mode 100644 index d6a100419f..0000000000 --- a/docker/explorer/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM node:16 - -COPY ./setup.sh /setup.sh -CMD /setup.sh diff --git a/docker/explorer/setup.sh b/docker/explorer/setup.sh deleted file mode 100755 index 38632b63b8..0000000000 --- a/docker/explorer/setup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -git clone https://github.com/nymtech/nym.git - -mkdir /explorer-copy -cp -r nym/explorer/* /explorer-copy - -cd /explorer-copy - -npm install -#baseUrl -sed -i 's/https:\/\/testnet-milhon-explorer.nymtech.net\//http:\/\/localhost:3080/' ./src/api/constants.ts - -#master validator -sed -i 's/https:\/\/testnet-milhon-validator1.nymtech.net/http:\/\/localhost:26657/' ./src/api/constants.ts - -#big dipper url -sed -i 's/https:\/\/testnet-milhon-blocks.nymtech.net\//http:\/\/localhost:3080/' ./src/api/constants.ts - -nohup npm run start \ No newline at end of file diff --git a/docker/mixnet_contract/Dockerfile b/docker/mixnet_contract/Dockerfile deleted file mode 100644 index 018338fd6d..0000000000 --- a/docker/mixnet_contract/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM rust -RUN rustup target add wasm32-unknown-unknown -CMD cd nym/contracts/mixnet && RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown - diff --git a/docker/mnemonic_echo/Dockerfile b/docker/mnemonic_echo/Dockerfile deleted file mode 100644 index 110a9cdaef..0000000000 --- a/docker/mnemonic_echo/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM alpine -COPY ./entrypoint.sh /entrypoint.sh -CMD /entrypoint.sh diff --git a/docker/mnemonic_echo/entrypoint.sh b/docker/mnemonic_echo/entrypoint.sh deleted file mode 100755 index 8c8a94aec3..0000000000 --- a/docker/mnemonic_echo/entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -# Wait for the mnemonic(s) to be generated -while ! [ -s /genesis_volume/genesis_mnemonic ]; do - sleep 1 -done - -while ! [ -s /genesis_volume/secondary_mnemonic ]; do - sleep 1 -done - -echo "This is the current genesis mnemonic:" -cat /genesis_volume/genesis_mnemonic - -echo "This is the current secondary mnemonic:" -cat /genesis_volume/secondary_mnemonic diff --git a/docker/typescript_client/Dockerfile b/docker/typescript_client/Dockerfile deleted file mode 100644 index a886a06933..0000000000 --- a/docker/typescript_client/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM node -RUN apt update && apt install -y netcat -COPY entrypoint.sh /entrypoint.sh -CMD /entrypoint.sh diff --git a/docker/typescript_client/entrypoint.sh b/docker/typescript_client/entrypoint.sh deleted file mode 100755 index 14de8384db..0000000000 --- a/docker/typescript_client/entrypoint.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -cd /nym/clients/validator -yarn install -yarn build - -cd /nym/docker/typescript_client/upload_contract -npm install - -# Wait until the mnemonic is created -while ! [ -s /genesis_volume/genesis_mnemonic ]; do - sleep 1 -done - -# Wait until the validator opens its port -while ! nc -z genesis_validator 26657; do - sleep 1 -done - -chmod 777 /contract_volume -npx ts-node upload-wasm.ts diff --git a/docker/typescript_client/upload_contract/package-lock.json b/docker/typescript_client/upload_contract/package-lock.json deleted file mode 100644 index 81b35e952b..0000000000 --- a/docker/typescript_client/upload_contract/package-lock.json +++ /dev/null @@ -1,2116 +0,0 @@ -{ - "name": "nym-driver-example", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "nym-driver-example", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@nymproject/nym-validator-client": "0.18.0", - "@types/node": "^14.14.22", - "nodemon": "^2.0.20", - "save-dev": "0.0.1-security", - "tasktimer": "^3.0.0", - "ts-node": "^9.1.1" - }, - "devDependencies": { - "@tsconfig/recommended": "^1.0.1", - "prettier": "^2.8.7", - "typescript": "^4.1.3" - } - }, - "../../../clients/validator": { - "name": "@nymproject/nym-validator-client", - "version": "0.19.0", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "@cosmjs/cosmwasm-stargate": "^0.27.0-rc2", - "@cosmjs/crypto": "^0.27.0-rc2", - "@cosmjs/math": "^0.27.0-rc2", - "@cosmjs/proto-signing": "^0.27.0-rc2", - "@cosmjs/stargate": "^0.27.0-rc2", - "@cosmjs/tendermint-rpc": "^0.27.0-rc2", - "axios": "^0.21.1", - "cosmjs-types": "^0.4.0" - }, - "devDependencies": { - "@types/chai": "^4.2.15", - "@types/expect": "^24.3.0", - "@types/mocha": "^8.2.1", - "@typescript-eslint/eslint-plugin": "^5.7.0", - "@typescript-eslint/parser": "^5.7.0", - "chai": "^4.2.0", - "eslint": "^7.18.0", - "eslint-config-airbnb": "^19.0.2", - "eslint-config-airbnb-typescript": "^16.1.0", - "eslint-config-prettier": "^8.3.0", - "eslint-import-resolver-root-import": "^1.0.4", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-mocha": "^10.0.3", - "eslint-plugin-prettier": "^4.0.0", - "mocha": "^8.2.1", - "moq.ts": "^7.2.0", - "nyc": "^15.1.0", - "prettier": "^2.5.1", - "ts-mocha": "^8.0.0", - "typedoc": "^0.20.27", - "typescript": "^4.1.3" - } - }, - "node_modules/@confio/ics23": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@confio/ics23/-/ics23-0.6.8.tgz", - "integrity": "sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==", - "dependencies": { - "@noble/hashes": "^1.0.0", - "protobufjs": "^6.8.8" - } - }, - "node_modules/@cosmjs/amino": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.25.6.tgz", - "integrity": "sha512-9dXN2W7LHjDtJUGNsQ9ok0DfxeN3ca/TXnxCR3Ikh/5YqBqxI8Gel1J9PQO9L6EheYyh045Wff4bsMaLjyEeqQ==", - "dependencies": { - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6" - } - }, - "node_modules/@cosmjs/cosmwasm-launchpad": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/cosmwasm-launchpad/-/cosmwasm-launchpad-0.25.6.tgz", - "integrity": "sha512-rzpYg/A8tvXbY6p89LabPB1mqCtTUv+33nN+s+VWMH0oOl0LSIgLE0yIT61kwTaf2dWQvRVeFaiRLFC72/w/zw==", - "dependencies": { - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/launchpad": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "pako": "^2.0.2" - } - }, - "node_modules/@cosmjs/cosmwasm-stargate": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.25.6.tgz", - "integrity": "sha512-STaxur5Xk5hqndLEztpa1TqvpdQyg2xD//2ZNFw2fgxf1JQtP0RfFbA3bnBkYwbGARx5cnng8QeZmSUcXnTVxA==", - "dependencies": { - "@cosmjs/amino": "^0.25.6", - "@cosmjs/cosmwasm-launchpad": "^0.25.6", - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/proto-signing": "^0.25.6", - "@cosmjs/stargate": "^0.25.6", - "@cosmjs/tendermint-rpc": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "long": "^4.0.0", - "pako": "^2.0.2", - "protobufjs": "~6.10.2" - } - }, - "node_modules/@cosmjs/crypto": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.25.6.tgz", - "integrity": "sha512-ec+YcQLrg2ibcxtNrh4FqQnG9kG9IE/Aik2NH6+OXQdFU/qFuBTxSFcKDgzzBOChwlkXwydllM9Jjbp+dgIzRw==", - "dependencies": { - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "bip39": "^3.0.2", - "bn.js": "^4.11.8", - "elliptic": "^6.5.3", - "js-sha3": "^0.8.0", - "libsodium-wrappers": "^0.7.6", - "ripemd160": "^2.0.2", - "sha.js": "^2.4.11" - } - }, - "node_modules/@cosmjs/encoding": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.25.6.tgz", - "integrity": "sha512-0imUOB8XkUstI216uznPaX1hqgvLQ2Xso3zJj5IV5oJuNlsfDj9nt/iQxXWbJuettc6gvrFfpf+Vw2vBZSZ75g==", - "dependencies": { - "base64-js": "^1.3.0", - "bech32": "^1.1.4", - "readonly-date": "^1.0.0" - } - }, - "node_modules/@cosmjs/json-rpc": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/json-rpc/-/json-rpc-0.25.6.tgz", - "integrity": "sha512-Mn9og3/IEzC6jWoYXs0ONqFJc8HxVjXzrZPLgaRRdMZEUBvctxdhynau1wbE4LdkYQHbu4aiRu1q1jMYGFAj4A==", - "dependencies": { - "@cosmjs/stream": "^0.25.6", - "xstream": "^11.14.0" - } - }, - "node_modules/@cosmjs/launchpad": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/launchpad/-/launchpad-0.25.6.tgz", - "integrity": "sha512-4Yhn4cX50UE6jZz/hWqKeeCmvrlrz0BBwOdYX/29k25FqP+oLAow1xKm6UxgYuuAq8Pg/bUvswxSqwegZJTb6g==", - "dependencies": { - "@cosmjs/amino": "^0.25.6", - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "axios": "^0.21.1", - "fast-deep-equal": "^3.1.3" - } - }, - "node_modules/@cosmjs/math": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.25.6.tgz", - "integrity": "sha512-Fmyc9FJ8KMU34n7rdapMJrT/8rx5WhMw2F7WLBu7AVLcBh0yWsXIcMSJCoPHTOnMIiABjXsnrrwEaLrOOBfu6A==", - "dependencies": { - "bn.js": "^4.11.8" - } - }, - "node_modules/@cosmjs/proto-signing": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.25.6.tgz", - "integrity": "sha512-JpQ+Vnv9s6i3x8f3Jo0lJZ3VMnj3R5sMgX+8ti1LtB7qEYRR85qbDrEG9hDGIKqJJabvrAuCHnO6hYi0vJEJHA==", - "dependencies": { - "@cosmjs/amino": "^0.25.6", - "long": "^4.0.0", - "protobufjs": "~6.10.2" - } - }, - "node_modules/@cosmjs/socket": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.25.6.tgz", - "integrity": "sha512-hu+pW3Fy0IuhstXgxnZ2Iq0RUnGYoTWfqrxbTsgXBJge4MpEQs2YwGXgJZPMJXedBQivG0tU3r/Wvam0EWuRkQ==", - "dependencies": { - "@cosmjs/stream": "^0.25.6", - "isomorphic-ws": "^4.0.1", - "ws": "^7", - "xstream": "^11.14.0" - } - }, - "node_modules/@cosmjs/stargate": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.25.6.tgz", - "integrity": "sha512-+LM1sK6vGuotJF9fBCBlaDL/yJhfzCV6KbsaWt3teHAKZhKYOd/9mKGiB4H9bfg4h3r+e+FZGiNkH/mdXkcgEw==", - "dependencies": { - "@confio/ics23": "^0.6.3", - "@cosmjs/amino": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/proto-signing": "^0.25.6", - "@cosmjs/stream": "^0.25.6", - "@cosmjs/tendermint-rpc": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "long": "^4.0.0", - "protobufjs": "~6.10.2" - } - }, - "node_modules/@cosmjs/stream": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.25.6.tgz", - "integrity": "sha512-2mXIzf+WaFd+GSrRaJJETVXeZoC5sosuKChFERxSY8zXQ/f3OaG9J6m+quHpPbU3nAIEtnF1jgBVqJiD+NKwGQ==", - "dependencies": { - "xstream": "^11.14.0" - } - }, - "node_modules/@cosmjs/tendermint-rpc": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.25.6.tgz", - "integrity": "sha512-wsvxTI7DReWJu+SVlXLblzh5NJppnh1mljuaTHodMf7HBxrXdbcCcBAO8oMbMgEqOASEY5G+z51wktxhrn9RtA==", - "dependencies": { - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/json-rpc": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/socket": "^0.25.6", - "@cosmjs/stream": "^0.25.6", - "axios": "^0.21.1", - "readonly-date": "^1.0.0", - "xstream": "^11.14.0" - } - }, - "node_modules/@cosmjs/utils": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.25.6.tgz", - "integrity": "sha512-ofOYiuxVKNo238vCPPlaDzqPXy2AQ/5/nashBo5rvPZJkxt9LciGfUEQWPCOb1BIJDNx2Dzu0z4XCf/dwzl0Dg==" - }, - "node_modules/@noble/hashes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.0.0.tgz", - "integrity": "sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg==" - }, - "node_modules/@nymproject/nym-validator-client": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@nymproject/nym-validator-client/-/nym-validator-client-0.18.0.tgz", - "integrity": "sha512-FO1T15S2BJVuMoPA2yOaH40aD3hKJPKJVyX5ix7eJEbBWIdsYNoVeVc/soHhaAU1FIy2uU0G/FZQkaUYJGGb7Q==", - "dependencies": { - "@cosmjs/cosmwasm-stargate": "^0.25.5", - "@cosmjs/math": "^0.25.5", - "@cosmjs/proto-signing": "^0.25.5", - "@cosmjs/stargate": "^0.25.5", - "axios": "^0.21.1" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "node_modules/@tsconfig/recommended": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.1.tgz", - "integrity": "sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==", - "dev": true - }, - "node_modules/@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "node_modules/@types/node": { - "version": "14.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", - "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/bip39": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", - "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", - "dependencies": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" - } - }, - "node_modules/bip39/node_modules/@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dependencies": { - "object-keys": "^1.0.12" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globalthis": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", - "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isomorphic-ws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" - }, - "node_modules/libsodium": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", - "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" - }, - "node_modules/libsodium-wrappers": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", - "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", - "dependencies": { - "libsodium": "^0.7.0" - } - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/nodemon": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", - "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^3.2.7", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/pako": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", - "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/protobufjs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.3.tgz", - "integrity": "sha512-yvAslS0hNdBhlSKckI4R1l7wunVilX66uvrjzE4MimiAt7/qw1nLpMhZrn/ObuUTM/c3Xnfl01LYMdcSJe6dwg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/protobufjs/node_modules/@types/node": { - "version": "13.13.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readonly-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz", - "integrity": "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==" - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/save-dev": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", - "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "dependencies": { - "semver": "~7.0.0" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/symbol-observable": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", - "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/tasktimer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tasktimer/-/tasktimer-3.0.0.tgz", - "integrity": "sha512-Fx2Zw4FFM/gc9cY4Iodo9UwC9yFl7fIU3ay/wqq0+E5WT587aZlmjCW2yLCHRVIwzcSig4tMqIh4vqxOcyXr9A==", - "dependencies": { - "eventemitter3": "^4.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/ts-node": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", - "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", - "dependencies": { - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "typescript": ">=2.7" - } - }, - "node_modules/typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xstream": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/xstream/-/xstream-11.14.0.tgz", - "integrity": "sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==", - "dependencies": { - "globalthis": "^1.0.1", - "symbol-observable": "^2.0.3" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "engines": { - "node": ">=6" - } - } - }, - "dependencies": { - "@confio/ics23": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@confio/ics23/-/ics23-0.6.8.tgz", - "integrity": "sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==", - "requires": { - "@noble/hashes": "^1.0.0", - "protobufjs": "^6.8.8" - } - }, - "@cosmjs/amino": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.25.6.tgz", - "integrity": "sha512-9dXN2W7LHjDtJUGNsQ9ok0DfxeN3ca/TXnxCR3Ikh/5YqBqxI8Gel1J9PQO9L6EheYyh045Wff4bsMaLjyEeqQ==", - "requires": { - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6" - } - }, - "@cosmjs/cosmwasm-launchpad": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/cosmwasm-launchpad/-/cosmwasm-launchpad-0.25.6.tgz", - "integrity": "sha512-rzpYg/A8tvXbY6p89LabPB1mqCtTUv+33nN+s+VWMH0oOl0LSIgLE0yIT61kwTaf2dWQvRVeFaiRLFC72/w/zw==", - "requires": { - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/launchpad": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "pako": "^2.0.2" - } - }, - "@cosmjs/cosmwasm-stargate": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.25.6.tgz", - "integrity": "sha512-STaxur5Xk5hqndLEztpa1TqvpdQyg2xD//2ZNFw2fgxf1JQtP0RfFbA3bnBkYwbGARx5cnng8QeZmSUcXnTVxA==", - "requires": { - "@cosmjs/amino": "^0.25.6", - "@cosmjs/cosmwasm-launchpad": "^0.25.6", - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/proto-signing": "^0.25.6", - "@cosmjs/stargate": "^0.25.6", - "@cosmjs/tendermint-rpc": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "long": "^4.0.0", - "pako": "^2.0.2", - "protobufjs": "~6.10.2" - } - }, - "@cosmjs/crypto": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.25.6.tgz", - "integrity": "sha512-ec+YcQLrg2ibcxtNrh4FqQnG9kG9IE/Aik2NH6+OXQdFU/qFuBTxSFcKDgzzBOChwlkXwydllM9Jjbp+dgIzRw==", - "requires": { - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "bip39": "^3.0.2", - "bn.js": "^4.11.8", - "elliptic": "^6.5.3", - "js-sha3": "^0.8.0", - "libsodium-wrappers": "^0.7.6", - "ripemd160": "^2.0.2", - "sha.js": "^2.4.11" - } - }, - "@cosmjs/encoding": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.25.6.tgz", - "integrity": "sha512-0imUOB8XkUstI216uznPaX1hqgvLQ2Xso3zJj5IV5oJuNlsfDj9nt/iQxXWbJuettc6gvrFfpf+Vw2vBZSZ75g==", - "requires": { - "base64-js": "^1.3.0", - "bech32": "^1.1.4", - "readonly-date": "^1.0.0" - } - }, - "@cosmjs/json-rpc": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/json-rpc/-/json-rpc-0.25.6.tgz", - "integrity": "sha512-Mn9og3/IEzC6jWoYXs0ONqFJc8HxVjXzrZPLgaRRdMZEUBvctxdhynau1wbE4LdkYQHbu4aiRu1q1jMYGFAj4A==", - "requires": { - "@cosmjs/stream": "^0.25.6", - "xstream": "^11.14.0" - } - }, - "@cosmjs/launchpad": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/launchpad/-/launchpad-0.25.6.tgz", - "integrity": "sha512-4Yhn4cX50UE6jZz/hWqKeeCmvrlrz0BBwOdYX/29k25FqP+oLAow1xKm6UxgYuuAq8Pg/bUvswxSqwegZJTb6g==", - "requires": { - "@cosmjs/amino": "^0.25.6", - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "axios": "^0.21.1", - "fast-deep-equal": "^3.1.3" - } - }, - "@cosmjs/math": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/math/-/math-0.25.6.tgz", - "integrity": "sha512-Fmyc9FJ8KMU34n7rdapMJrT/8rx5WhMw2F7WLBu7AVLcBh0yWsXIcMSJCoPHTOnMIiABjXsnrrwEaLrOOBfu6A==", - "requires": { - "bn.js": "^4.11.8" - } - }, - "@cosmjs/proto-signing": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.25.6.tgz", - "integrity": "sha512-JpQ+Vnv9s6i3x8f3Jo0lJZ3VMnj3R5sMgX+8ti1LtB7qEYRR85qbDrEG9hDGIKqJJabvrAuCHnO6hYi0vJEJHA==", - "requires": { - "@cosmjs/amino": "^0.25.6", - "long": "^4.0.0", - "protobufjs": "~6.10.2" - } - }, - "@cosmjs/socket": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.25.6.tgz", - "integrity": "sha512-hu+pW3Fy0IuhstXgxnZ2Iq0RUnGYoTWfqrxbTsgXBJge4MpEQs2YwGXgJZPMJXedBQivG0tU3r/Wvam0EWuRkQ==", - "requires": { - "@cosmjs/stream": "^0.25.6", - "isomorphic-ws": "^4.0.1", - "ws": "^7", - "xstream": "^11.14.0" - } - }, - "@cosmjs/stargate": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.25.6.tgz", - "integrity": "sha512-+LM1sK6vGuotJF9fBCBlaDL/yJhfzCV6KbsaWt3teHAKZhKYOd/9mKGiB4H9bfg4h3r+e+FZGiNkH/mdXkcgEw==", - "requires": { - "@confio/ics23": "^0.6.3", - "@cosmjs/amino": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/proto-signing": "^0.25.6", - "@cosmjs/stream": "^0.25.6", - "@cosmjs/tendermint-rpc": "^0.25.6", - "@cosmjs/utils": "^0.25.6", - "long": "^4.0.0", - "protobufjs": "~6.10.2" - } - }, - "@cosmjs/stream": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.25.6.tgz", - "integrity": "sha512-2mXIzf+WaFd+GSrRaJJETVXeZoC5sosuKChFERxSY8zXQ/f3OaG9J6m+quHpPbU3nAIEtnF1jgBVqJiD+NKwGQ==", - "requires": { - "xstream": "^11.14.0" - } - }, - "@cosmjs/tendermint-rpc": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.25.6.tgz", - "integrity": "sha512-wsvxTI7DReWJu+SVlXLblzh5NJppnh1mljuaTHodMf7HBxrXdbcCcBAO8oMbMgEqOASEY5G+z51wktxhrn9RtA==", - "requires": { - "@cosmjs/crypto": "^0.25.6", - "@cosmjs/encoding": "^0.25.6", - "@cosmjs/json-rpc": "^0.25.6", - "@cosmjs/math": "^0.25.6", - "@cosmjs/socket": "^0.25.6", - "@cosmjs/stream": "^0.25.6", - "axios": "^0.21.1", - "readonly-date": "^1.0.0", - "xstream": "^11.14.0" - } - }, - "@cosmjs/utils": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.25.6.tgz", - "integrity": "sha512-ofOYiuxVKNo238vCPPlaDzqPXy2AQ/5/nashBo5rvPZJkxt9LciGfUEQWPCOb1BIJDNx2Dzu0z4XCf/dwzl0Dg==" - }, - "@noble/hashes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.0.0.tgz", - "integrity": "sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg==" - }, - "@nymproject/nym-validator-client": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@nymproject/nym-validator-client/-/nym-validator-client-0.18.0.tgz", - "integrity": "sha512-FO1T15S2BJVuMoPA2yOaH40aD3hKJPKJVyX5ix7eJEbBWIdsYNoVeVc/soHhaAU1FIy2uU0G/FZQkaUYJGGb7Q==", - "requires": { - "@cosmjs/cosmwasm-stargate": "^0.25.5", - "@cosmjs/math": "^0.25.5", - "@cosmjs/proto-signing": "^0.25.5", - "@cosmjs/stargate": "^0.25.5", - "axios": "^0.21.1" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "@tsconfig/recommended": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.1.tgz", - "integrity": "sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==", - "dev": true - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" - }, - "@types/node": { - "version": "14.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", - "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" - }, - "bip39": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", - "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", - "requires": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" - }, - "dependencies": { - "@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - } - } - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "requires": { - "fill-range": "^7.1.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" - }, - "elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "globalthis": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", - "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", - "requires": { - "define-properties": "^1.1.3" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "isomorphic-ws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", - "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "requires": {} - }, - "js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" - }, - "libsodium": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", - "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==" - }, - "libsodium-wrappers": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", - "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", - "requires": { - "libsodium": "^0.7.0" - } - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "nodemon": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", - "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", - "requires": { - "chokidar": "^3.5.2", - "debug": "^3.2.7", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^5.7.1", - "simple-update-notifier": "^1.0.7", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "pako": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", - "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" - }, - "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true - }, - "protobufjs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.3.tgz", - "integrity": "sha512-yvAslS0hNdBhlSKckI4R1l7wunVilX66uvrjzE4MimiAt7/qw1nLpMhZrn/ObuUTM/c3Xnfl01LYMdcSJe6dwg==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": "^13.7.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "13.13.52", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", - "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" - } - } - }, - "pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "readonly-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz", - "integrity": "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==" - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "save-dev": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/save-dev/-/save-dev-0.0.1-security.tgz", - "integrity": "sha512-k6knZTDNK8PKKbIqnvxiOveJinuw2LcQjqDoaorZWP9M5AR2EPsnpDeSbeoZZ0pHr5ze1uoaKdK8NBGQrJ34Uw==" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "simple-update-notifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", - "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", - "requires": { - "semver": "~7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-observable": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", - "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==" - }, - "tasktimer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tasktimer/-/tasktimer-3.0.0.tgz", - "integrity": "sha512-Fx2Zw4FFM/gc9cY4Iodo9UwC9yFl7fIU3ay/wqq0+E5WT587aZlmjCW2yLCHRVIwzcSig4tMqIh4vqxOcyXr9A==", - "requires": { - "eventemitter3": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "requires": { - "nopt": "~1.0.10" - } - }, - "ts-node": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", - "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", - "requires": { - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.17", - "yn": "3.1.1" - } - }, - "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==" - }, - "undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", - "requires": {} - }, - "xstream": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/xstream/-/xstream-11.14.0.tgz", - "integrity": "sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==", - "requires": { - "globalthis": "^1.0.1", - "symbol-observable": "^2.0.3" - } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" - } - } -} diff --git a/docker/typescript_client/upload_contract/package.json b/docker/typescript_client/upload_contract/package.json deleted file mode 100644 index 8508b2f306..0000000000 --- a/docker/typescript_client/upload_contract/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "nym-driver-example", - "version": "1.0.0", - "main": "./dist/index.js", - "author": "Dave Hrycyszyn", - "license": "MIT", - "description": "The simplest cosmjs typescript client to work as an example driver program", - "repository": "https://github.com/nymtech/nym", - "scripts": { - "build": "tsc" - }, - "devDependencies": { - "@tsconfig/recommended": "^1.0.1", - "prettier": "^2.8.7", - "typescript": "^4.1.3" - }, - "dependencies": { - "@types/node": "^14.14.22", - "nodemon": "^2.0.20", - "@nymproject/nym-validator-client" : "0.18.0", - "save-dev": "0.0.1-security", - "tasktimer": "^3.0.0", - "ts-node": "^9.1.1" - } -} diff --git a/docker/typescript_client/upload_contract/upload-wasm.ts b/docker/typescript_client/upload_contract/upload-wasm.ts deleted file mode 100644 index c525de6f6b..0000000000 --- a/docker/typescript_client/upload_contract/upload-wasm.ts +++ /dev/null @@ -1,43 +0,0 @@ -// npx ts-node upload-wasm.ts - -import ValidatorClient from "@nymproject/nym-validator-client"; -import * as fs from 'fs'; - -async function newClient(): Promise { - let contract = "fakeContractAddress"; // we don't have one yet - let mnemonic = fs.readFileSync("/genesis_volume/genesis_mnemonic", "utf8").slice(0, -1); - let admin = ValidatorClient.connect(contract, mnemonic, "http://genesis_validator:26657", process.env["BECH32_PREFIX"]); - return admin; -} - -async function main() { - let admin = await newClient(); - console.log(`admin address: ${admin.address}`); - - // check that we have actually connected to an account, query it to test - let balance = await admin.getBalance(admin.address); - console.log(`balance of admin account is: ${balance.amount}${balance.denom}`); - - let wasm = fs.readFileSync("/nym/contracts/mixnet/target/wasm32-unknown-unknown/release/mixnet_contracts.wasm"); - console.log("wasm loaded"); - - // dave can upload (note: nobody else can) - const uploadResult = await admin.upload(admin.address, wasm, undefined, "mixnet contract");//.then((uploadResult) => console.log("Upload from dave succeeded, codeId is: " + uploadResult.codeId)).catch((err) => console.log(err)); - - //todo - //add vesting contract? - - // Instantiate the copy of the option contract - const { codeId } = uploadResult; - console.log("code id is", codeId) - const initMsg = {}; - const options = { memo: "v0.1.0", transferAmount: [{ denom: "u" + process.env["BECH32_PREFIX"], amount: "1000000" }], admin: admin.address } - let instantiateResult = await admin.instantiate(admin.address, codeId, initMsg, "mixnet contract", options); - let contractAddress = instantiateResult.contractAddress; - console.log(`mixnet contract ${contractAddress} instantiated successfully`) - fs.writeFileSync("/contract_volume/contract_address", contractAddress); -} - - - -main(); diff --git a/docker/validator/.env.sample b/docker/validator/.env.sample new file mode 100644 index 0000000000..2e1b4c4b1c --- /dev/null +++ b/docker/validator/.env.sample @@ -0,0 +1,25 @@ +CONFIGURED=true + +NETWORK_NAME=nymtestnetwork + +RUST_LOG=info +RUST_BACKTRACE=1 + +BECH32_PREFIX=n +MIX_DENOM=unym +MIX_DENOM_DISPLAY=nym +STAKE_DENOM=unyx +STAKE_DENOM_DISPLAY=nyx +DENOMS_EXPONENT=6 + +MIXNET_CONTRACT_ADDRESS= +VESTING_CONTRACT_ADDRESS= +GROUP_CONTRACT_ADDRESS= +ECASH_CONTRACT_ADDRESS= +MULTISIG_CONTRACT_ADDRESS= +COCONUT_DKG_CONTRACT_ADDRESS= + +REWARDING_VALIDATOR_ADDRESS= +NYXD="https://localhost:26656" +NYM_API="https://insertvalues.io" +NYXD_WS="wss://insertvalues.io" \ No newline at end of file diff --git a/docker/validator/.gitignore b/docker/validator/.gitignore new file mode 100644 index 0000000000..151f3ee8e5 --- /dev/null +++ b/docker/validator/.gitignore @@ -0,0 +1,15 @@ +# data + +data/ +data/validator/ +data/addresses/ + +secrets/ + +# docker +.docker/ + +# access +*_mnemonic +*.key +genesis_mnemonic \ No newline at end of file diff --git a/docker/validator/Dockerfile b/docker/validator/Dockerfile index cfcc007931..5e3039495a 100644 --- a/docker/validator/Dockerfile +++ b/docker/validator/Dockerfile @@ -1,12 +1,42 @@ -FROM golang:buster as go_builder -ARG BECH32_PREFIX -ARG WASMD_VERSION -RUN apt update && apt install -y git build-essential -COPY setup.sh . -RUN ./setup.sh +FROM ubuntu:24.04 -FROM ubuntu:20.04 -COPY --from=go_builder /go/wasmd/build/nymd /root/nymd -COPY --from=go_builder /go/wasmd/build/libwasmvm*.so /root -COPY init_and_start.sh . -ENTRYPOINT ["./init_and_start.sh"] +ARG WASMD_VERSION +ARG NYM_CLI_GIT_TAG +ARG RETAIN_BLOCKS + +RUN apt-get update && apt-get install -y \ + ca-certificates \ + jq \ + curl \ + wget \ + vim \ + openssl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /root + +RUN wget -O nym-cli "https://github.com/nymtech/nym/releases/download/nym-binaries-v${NYM_CLI_GIT_TAG:-2025.8-tourist}/nym-cli" && \ + chmod 755 nym-cli || echo "nym-cli not available" + +RUN wget "https://github.com/nymtech/nyxd/releases/download/${WASMD_VERSION:-v0.54.3}/nyxd-ubuntu-22.04.tar.gz" && \ + tar -zxvf nyxd-ubuntu-22.04.tar.gz && \ + mv libwasmvm.x86_64.so /lib/x86_64-linux-gnu/ && \ + chmod 755 nyxd && \ + rm nyxd-ubuntu-22.04.tar.gz + +RUN mkdir -p "/root/.nyxd/config" "/root/output" +RUN chmod 755 "/root/.nyxd" "/root/.nyxd/config" "/root/output" + +VOLUME /root/.nyxd +VOLUME /root/output + +RUN touch .env + +COPY start.sh . +RUN chmod 755 *.sh + +ENV RETAIN_BLOCKS=${RETAIN_BLOCKS} + +ENTRYPOINT ["./start.sh"] +CMD ["genesis"] \ No newline at end of file diff --git a/docker/validator/docker-compose.yml b/docker/validator/docker-compose.yml new file mode 100644 index 0000000000..182f4f7e86 --- /dev/null +++ b/docker/validator/docker-compose.yml @@ -0,0 +1,30 @@ +services: + validator: + build: + context: ./ + args: + NYM_CLI_GIT_TAG: "2025.8-tourist" + WASMD_VERSION: "v0.54.3" + image: validator:latest + container_name: validator + ports: + - "127.0.0.1:26657:26657" + - "127.0.0.1:26656:26656" + - "127.0.0.1:1317:1317" + restart: unless-stopped + deploy: + resources: + limits: + cpus: '2' + memory: 4G + volumes: + - ./data/addresses:/root/output + - ./data/validator:/root/.nyxd + environment: + BECH32_PREFIX: "n" + DENOM: "nym" + STAKE_DENOM: "nyx" + WASMD_VERSION: "v0.54.3" + CHAIN_ID: "nymtestnetwork" + NYM_CLI_GIT_TAG: "2025.8-tourist" + RETAIN_BLOCKS: "no" \ No newline at end of file diff --git a/docker/validator/init_and_start.sh b/docker/validator/init_and_start.sh deleted file mode 100755 index a1e8820fbc..0000000000 --- a/docker/validator/init_and_start.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/sh - -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root -PASSPHRASE=passphrase - -cd /root - -if [ "$1" = "genesis" ]; then - if [ ! -f "/root/.nymd/config/genesis.json" ]; then - ./nyxd init nymnet --chain-id nymnet 2> /dev/null - # staking/governance token is hardcoded in config, change this - sed -i "s/\"stake\"/\"u${STAKE_DENOM}\"/" /root/.nyxd/config/genesis.json - sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${DENOM}"'"/' /root/.nyxd/config/app.toml - sed -i '0,/enable = false/s//enable = true/g' /root/.nyxd/config/app.toml - sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nyxd/config/config.toml - sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nyxd/config/config.toml - sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nyxd/config/config.toml - -# create accounts - yes "${PASSPHRASE}" | ./nyxd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /root/.nyxd/mnemonic - yes "${PASSPHRASE}" | ./nyxd keys add secondary 2>&1 >/dev/null | tail -n 1 > /root/.nyxd/secondary_mnemonic - cp /root/.nyxd/mnemonic /genesis_volume/genesis_mnemonic - cp /root/.nyxd/secondary_mnemonic /genesis_volume/secondary_mnemonic - -# add genesis accounts with some initial tokens - GENESIS_ADDRESS=$(yes "${PASSPHRASE}" | ./nyxd keys show node_admin -a) - SECONDARY_ADDRESS=$(yes "${PASSPHRASE}" | ./nyxd keys show secondary -a) - yes "${PASSPHRASE}" | ./nyxd add-genesis-account "${GENESIS_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}" - yes "${PASSPHRASE}" | ./nyxd add-genesis-account "${SECONDARY_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}" - - yes "${PASSPHRASE}" | ./nyxd gentx node_admin 1000000000u"${STAKE_DENOM}" --chain-id nymnet 2> /dev/null - ./nyxd collect-gentxs 2> /dev/null - ./nyxd validate-genesis > /dev/null - cp /root/.nyxd/config/genesis.json /genesis_volume/genesis.json - else - echo "Validator already initialized, starting with the existing configuration." - echo "If you want to re-init the validator, destroy the existing container" - fi - ./nyxd start -elif [ "$1" = "secondary" ]; then - if [ ! -f "/root/.nymd/config/genesis.json" ]; then - ./nyxd init nymnet --chain-id nym-secondary 2> /dev/null - - # Wait until the genesis node writes the genesis.json to the shared volume - while ! [ -s /genesis_volume/genesis.json ]; do - sleep 1 - done - -# wait for the actual validator to start up - sleep 5 - - cp /genesis_volume/genesis.json /root/.nyxd/config/genesis.json - GENESIS_PEER=$(cat /root/.nyxd/config/genesis.json | grep '"memo"' | cut -d'"' -f 4) - GENESIS_IP=$(cat /root/.nyxd/config/genesis.json | grep '"memo"' | cut -d'@' -f2 | cut -d: -f1) - sed -i 's/persistent_peers = ""/persistent_peers = "'"${GENESIS_PEER}"'"/' /root/.nyxd/config/config.toml - sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nyxd/config/app.toml - sed -i '0,/enable = false/s//enable = true/g' /root/.nyxd/config/app.toml - sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nyxd/config/config.toml - sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nyxd/config/config.toml - sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nyxd/config/config.toml - -# import mnemonic generated by the genesis validator (have a local copy for ease of use) - cp /genesis_volume/secondary_mnemonic /root/.nyxd/mnemonic - { cat /root/.nyxd/mnemonic; echo "${PASSPHRASE}"; echo "${PASSPHRASE}"; } | ./nyxd keys add node_admin --recover #> /dev/null - ./nyxd validate-genesis > /dev/null - -# create validator -# don't even ask about those sleeps... - { echo "${PASSPHRASE}"; sleep 10; yes; sleep 10; } | ./nyxd tx staking create-validator --amount=10000000u"${STAKE_DENOM}" --fees 100000u"${DENOM}" --pubkey="$(./nyxd tendermint show-validator)" --moniker="secondary" --commission-rate="0.10" --commission-max-rate="0.20" --commission-max-change-rate="0.01" --min-self-delegation="1" --chain-id=nymnet --from=node_admin -b async --node http://"${GENESIS_IP}":26657 - else - echo "Validator already initialized, starting with the existing configuration." - echo "If you want to re-init the validator, destroy the existing container" - fi - ./nyxd start -else - echo "Wrong command. Usage: ./$0 [genesis/secondary]" -fi diff --git a/docker/validator/setup.sh b/docker/validator/setup.sh deleted file mode 100755 index 4b176fb04e..0000000000 --- a/docker/validator/setup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -set -ue - -git clone https://github.com/CosmWasm/wasmd.git -cd wasmd -git checkout "${WASMD_VERSION}" -WASMD_COMMIT_HASH=$(git rev-parse HEAD) -mkdir build -go build \ - -o build/nyxd -mod=readonly -tags "netgo,ledger" \ - -ldflags "-X github.com/cosmos/cosmos-sdk/version.Name=nymd \ - -X github.com/cosmos/cosmos-sdk/version.AppName=nymd \ - -X github.com/CosmWasm/wasmd/app.NodeDir=.nymd \ - -X github.com/cosmos/cosmos-sdk/version.Version=${WASMD_VERSION} \ - -X github.com/cosmos/cosmos-sdk/version.Commit=${WASMD_COMMIT_HASH} \ - -X github.com/CosmWasm/wasmd/app.Bech32Prefix=${BECH32_PREFIX} \ - -X 'github.com/cosmos/cosmos-sdk/version.BuildTags=netgo,ledger'" \ - -trimpath ./cmd/wasmd -find .. -type f -name 'libwasm*.so' -exec cp {} build \; diff --git a/docker/validator/start.sh b/docker/validator/start.sh new file mode 100755 index 0000000000..d9cfab7d23 --- /dev/null +++ b/docker/validator/start.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH":/root +PASSPHRASE=passphrase +APP_NAME=nyxd +OUTPUT_DIRECTORY="/root/output" +VALIDATOR_DATA_DIRECTORY="/root/.${APP_NAME}" + +mkdir -p "${VALIDATOR_DATA_DIRECTORY}/config" +mkdir -p "${OUTPUT_DIRECTORY}" + +if [ ! -f "${VALIDATOR_DATA_DIRECTORY}/config/genesis.json" ]; then + # initialise the validator + ./${APP_NAME} init "${CHAIN_ID}" --chain-id "${CHAIN_ID}" 2>/dev/null + + echo "init chain successful" + sleep 5 + + echo "checking config files:" + ls -la ${VALIDATOR_DATA_DIRECTORY}/config/ + + echo "changing params" + sed -i "s/\"stake\"/\"u${STAKE_DENOM}\"/" "${VALIDATOR_DATA_DIRECTORY}/config/genesis.json" + sed -i 's/minimum-gas-prices = "0stake"/minimum-gas-prices = "0.025u'"${DENOM}"'"/' "${VALIDATOR_DATA_DIRECTORY}/config/app.toml" + sed -i '0,/enable = false/s//enable = true/g' "${VALIDATOR_DATA_DIRECTORY}/config/app.toml" + if [ "$RETAIN_BLOCKS" = "no" ]; then + # amending to say if min retain blocks should be set yes or no... + sed -i 's/min-retain-blocks = 0/min-retain-blocks = 70000/' "${VALIDATOR_DATA_DIRECTORY}/config/app.toml" + fi + sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' "${VALIDATOR_DATA_DIRECTORY}/config/config.toml" + sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' "${VALIDATOR_DATA_DIRECTORY}/config/config.toml" + sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' "${VALIDATOR_DATA_DIRECTORY}/config/config.toml" + sed -i 's/address = "tcp:\/\/localhost:1317"/address = "tcp:\/\/0.0.0.0:1317"/' "${VALIDATOR_DATA_DIRECTORY}/config/app.toml" + + echo "params changed" + + echo "adding parent mnemonic account details" + yes "${PASSPHRASE}" | ./${APP_NAME} keys add node_admin 2>&1 >/dev/null | tail -n 1 >${OUTPUT_DIRECTORY}/node_admin_mnemonic + + # add genesis accounts with some initial tokens + echo "adding genesis account details" + GENESIS_ADDRESS=$(yes "${PASSPHRASE}" | ./${APP_NAME} keys show node_admin -a) + yes "${PASSPHRASE}" | ./${APP_NAME} genesis add-genesis-account "${GENESIS_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}" + + echo "adding gentx time :)" + yes "${PASSPHRASE}" | ./${APP_NAME} genesis gentx node_admin 100000000000u"${STAKE_DENOM}" --chain-id "${CHAIN_ID}" 2>/dev/null + ./${APP_NAME} genesis collect-gentxs 2>/dev/null + ./${APP_NAME} genesis validate-genesis >/dev/null + + # make a copy of the genesis file to the output directory + cp "${VALIDATOR_DATA_DIRECTORY}/config/genesis.json" "${OUTPUT_DIRECTORY}/genesis.json" +fi + +./${APP_NAME} start & +sleep 10 + +sleep infinity \ No newline at end of file diff --git a/docker/vesting_contract/Dockerfile b/docker/vesting_contract/Dockerfile deleted file mode 100644 index eddf9f7004..0000000000 --- a/docker/vesting_contract/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM rust -RUN rustup target add wasm32-unknown-unknown -CMD cd nym/contracts/vesting && RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown \ No newline at end of file diff --git a/documentation/README.md b/documentation/README.md index de968a158c..776db65a21 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -21,6 +21,15 @@ Our `prebuild` script relies on the following: Otherwise make sure to have `node` installed. +#### Binary dependencies +If you don't want/need build our binaries in Rust localy you can download them from latest Release page on [github](https://github.com/nymtech/nym/releases) into target/release in root of this repo. +{'nym-node', 'nym-api', 'nymvisor'} + +Make them executable +```bash +chmod +x target/release/* +``` + ### Serve Local (Hot Reload) ```sh pnpm i diff --git a/documentation/autodoc/src/main.rs b/documentation/autodoc/src/main.rs index 7f9513097c..812677fb99 100644 --- a/documentation/autodoc/src/main.rs +++ b/documentation/autodoc/src/main.rs @@ -163,7 +163,7 @@ fn main() -> io::Result<()> { write_output_to_file(&mut file, output)?; for (subcommand, subsubcommands) in subcommands { - writeln!(file, "\n## `{}` ", subcommand)?; + writeln!(file, "\n## `{subcommand}` ")?; let output = Command::new(main_command) .arg(subcommand) .arg("--help") @@ -195,12 +195,12 @@ fn execute_command_own_file(main_command: &str, subcommand: &str) -> io::Result< || get_last_word_from_filepath(main_command).unwrap() == "nym-node") && subcommand == "run" { - info!("SKIPPING {} {}", main_command, subcommand); + info!("SKIPPING {main_command} {subcommand}"); } else { let last_word = get_last_word_from_filepath(main_command); let output = Command::new(main_command).arg(subcommand).output()?; if !output.stdout.is_empty() { - info!("creating own file for {} {}", main_command, subcommand,); + info!("creating own file for {main_command} {subcommand}",); if !fs::metadata(WRITE_PATH) .map(|metadata| metadata.is_dir()) .unwrap_or(false) @@ -216,10 +216,7 @@ fn execute_command_own_file(main_command: &str, subcommand: &str) -> io::Result< write_output_to_file(&mut file, output)?; // execute help - info!( - "creating own file for {} {} --help", - main_command, subcommand, - ); + info!("creating own file for {main_command} {subcommand} --help",); if !fs::metadata(COMMAND_PATH) .map(|metadata| metadata.is_dir()) .unwrap_or(false) @@ -243,10 +240,7 @@ fn execute_command_own_file(main_command: &str, subcommand: &str) -> io::Result< debug!("empty stdout - nothing to write"); } } else { - info!( - "creating own file for {} {} --help", - main_command, subcommand, - ); + info!("creating own file for {main_command} {subcommand} --help",); if !fs::metadata(COMMAND_PATH) .map(|metadata| metadata.is_dir()) .unwrap_or(false) @@ -281,7 +275,7 @@ fn execute_command( if subsubcommand.is_some() { writeln!(file, "\n## `{} {}`", subcommand, subsubcommand.unwrap())?; - info!("executing {} {} --help ", main_command, subcommand); + info!("executing {main_command} {subcommand} --help "); let output = Command::new(main_command) .arg(subcommand) .arg(subsubcommand.unwrap()) @@ -294,7 +288,7 @@ fn execute_command( } // just subcommands } else { - writeln!(file, "\n## `{}`", subcommand)?; + writeln!(file, "\n## `{subcommand}`")?; // execute help let output = Command::new(main_command) @@ -318,9 +312,9 @@ fn execute_command( || get_last_word_from_filepath(main_command).unwrap() == "nymvisor" && subcommand == "run" { - info!("SKIPPING {} {}", main_command, subcommand); + info!("SKIPPING {main_command} {subcommand}"); } else { - info!("executing {} {}", main_command, subcommand); + info!("executing {main_command} {subcommand}"); let output = Command::new(main_command).arg(subcommand).output()?; if !output.stdout.is_empty() { writeln!(file, "Example output:")?; diff --git a/documentation/docs/components/operators/interactive/calculators/reward-calculator.jsx b/documentation/docs/components/operators/interactive/calculators/reward-calculator.jsx new file mode 100644 index 0000000000..b97de4dca9 --- /dev/null +++ b/documentation/docs/components/operators/interactive/calculators/reward-calculator.jsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react' +import CirculatingSupply from 'components/outputs/api-scraping-outputs/circulating-supply.json' +import RewardParams from 'components/outputs/api-scraping-outputs/reward-params.json' + + +export default function RewardsCalculator() { + const [a, setA] = useState( + Number( + (Number(RewardParams.interval.epoch_reward_budget) / 1_000_000).toFixed(6) + ) + ) + const [b, setB] = useState(0) + const [c, setC] = useState(0) + const [d, setD] = useState(0) + const [e, setE] = useState( + Number( + (Number(RewardParams.interval.stake_saturation_point) / 1_000_000).toFixed(6) + ) + ) + const result = + e !== 0 + ? `${( + a * b * c * ((1 / 240) + 0.3 * ((d / e) / 240)) * 1 / (1 + 0.3) + ).toFixed(6)} NYM` + : '—' + + return ( + + ) +} diff --git a/documentation/docs/components/operators/snippets/stake-saturation.mdx b/documentation/docs/components/operators/snippets/stake-saturation.mdx new file mode 100644 index 0000000000..b17b73ac10 --- /dev/null +++ b/documentation/docs/components/operators/snippets/stake-saturation.mdx @@ -0,0 +1,34 @@ +import StakingTarget from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md'; +import StakingScaleFactor from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md'; +import StakeSaturation from 'components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md'; +import CirculatingSupply from 'components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md'; +import { Callout } from 'nextra/components'; +import StakingSupply from 'components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md'; + +Stake saturation is a node reputation done in a form of self bond or stakers delegation. Optimal stake saturation level is calculated as: + + +> **stake_saturation_level = staking_target / rewarded_set_size** +> +> **rewarded_set_size = active_set_size + standby_set_size** + + +With current circulating supply of NYM, staking target of NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), the stake saturation level is NYM per node. + +Node stake saturation is a value between `0` and `1` following this logic. + +**Node stake saturation formula:** + + +> **node_stake_saturation = node_total_stake / stake_saturation_level** + + +There is a caveat that the maximum value can be `1`. In practice it means that: + +1. If `node_total_stake < stake_saturation_level` then `node_stake_saturation` will be a float between `0` and `1` + +2. If `node_total_stake = stake_saturation_level` then `node_stake_saturation` will be `1` + +3. If `node_total_stake > stake_saturation_level` then `node_stake_saturation` will be `1` due the capping function working as anti-whale prevention. +- This results in a smaller ROI per every staked (self bond or delegation) NYM token on that node, as the maximum rewards is capped and in this case distributed in between more staked tokens. +- For example if `node_total_stake = 2 * stake_saturation_level` then the reward per staked token will be 50% in comparison to a case where `node_total_stake = stake_saturation_level`, in other words with 100% *over-saturation*, ROI is half the maximum. diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-install-output.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-install-output.mdx index 487e4027bd..ae026dc659 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-install-output.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-install-output.mdx @@ -7,20 +7,32 @@ net.ipv6.conf.all.forwarding = 1 net.ipv4.ip_forward = 1 IP forwarding configured successfully. Creating Nym exit policy chain... -Creating chain NYM-EXIT... -Creating chain NYM-EXIT in ip6tables... -Linking NYM-EXIT to FORWARD chain... -Linking NYM-EXIT to IPv6 FORWARD chain... +Chain NYM-EXIT already exists. Flushing it... +Chain NYM-EXIT already exists in ip6tables. Flushing it... +NYM-EXIT all opt -- in * out nymwg 0.0.0.0/0 -> 0.0.0.0/0 +NYM-EXIT all opt in * out nymwg ::/0 -> ::/0 Setting up NAT rules... +MASQUERADE all opt -- in * out ens3 0.0.0.0/0 -> 0.0.0.0/0 IPv4 NAT rule already exists. +MASQUERADE all opt in * out ens3 ::/0 -> ::/0 IPv6 NAT rule already exists. +ACCEPT all opt -- in nymwg out ens3 0.0.0.0/0 -> 0.0.0.0/0 +ACCEPT all opt -- in ens3 out nymwg 0.0.0.0/0 -> 0.0.0.0/0 state RELATED,ESTABLISHED +ACCEPT all opt in nymwg out ens3 ::/0 -> ::/0 +ACCEPT all opt in ens3 out nymwg ::/0 -> ::/0 state RELATED,ESTABLISHED Configuring DNS and ICMP rules... -Added IPv6 ICMP rule (allow ping6). -Added IPv6 DNS rule (UDP). -Added IPv6 DNS rule (TCP). +ACCEPT icmp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 icmptype 8 +ACCEPT icmp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 icmptype 0 +ACCEPT icmpv6 opt in * out * ::/0 -> ::/0 +ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:53 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:53 +ACCEPT udp opt in * out * ::/0 -> ::/0 udp dpt:53 +ACCEPT tcp opt in * out * ::/0 -> ::/0 tcp dpt:53 Applying Spamhaus blocklist... Downloading exit policy from https://nymtech.net/.wellknown/network-requester/exit-policy.txt Processing 429 blocklist rules... +REJECT all opt -- in * out * 0.0.0.0/0 -> 205.189.71.0/24 reject-with icmp-port-unreachable +REJECT all opt -- in * out * 0.0.0.0/0 -> 205.189.72.0/23 reject-with icmp-port-unreachable Blocklist applied successfully. Applying allowed ports... Adding rules for SILC (Port: 706) @@ -103,6 +115,11 @@ Adding rules for POP3OverTLS (Port: 995) Added: NYM-EXIT tcp port 995 Added: NYM-EXIT udp port 995 Added: NYM-EXIT udp port 995 +Adding rules for DarkFiTor (Port: 25551) + Added: NYM-EXIT tcp port 25551 + Added: NYM-EXIT tcp port 25551 + Added: NYM-EXIT udp port 25551 + Added: NYM-EXIT udp port 25551 Adding rules for MMCC (Port: 5050) Added: NYM-EXIT tcp port 5050 Added: NYM-EXIT tcp port 5050 @@ -268,6 +285,11 @@ Adding rules for Mumble (Port: 64738) Added: NYM-EXIT tcp port 64738 Added: NYM-EXIT udp port 64738 Added: NYM-EXIT udp port 64738 +Adding rules for DarkFi (Port: 26661) + Added: NYM-EXIT tcp port 26661 + Added: NYM-EXIT tcp port 26661 + Added: NYM-EXIT udp port 26661 + Added: NYM-EXIT udp port 26661 Adding rules for PPTP (Port: 1723) Added: NYM-EXIT tcp port 1723 Added: NYM-EXIT tcp port 1723 @@ -333,6 +355,11 @@ Adding rules for Kpasswd (Port: 464) Added: NYM-EXIT tcp port 464 Added: NYM-EXIT udp port 464 Added: NYM-EXIT udp port 464 +Adding rules for MoneroRPC (Port: 18089) + Added: NYM-EXIT tcp port 18089 + Added: NYM-EXIT tcp port 18089 + Added: NYM-EXIT udp port 18089 + Added: NYM-EXIT udp port 18089 Adding rules for RemoteHTTPS (Port: 981) Added: NYM-EXIT tcp port 981 Added: NYM-EXIT tcp port 981 @@ -398,6 +425,11 @@ Adding rules for GroupWise (Port: 1677) Added: NYM-EXIT tcp port 1677 Added: NYM-EXIT udp port 1677 Added: NYM-EXIT udp port 1677 +Adding rules for Monero (Port: 18080-18081) + Added: NYM-EXIT tcp port range 18080:18081 + Added: NYM-EXIT tcp port range 18080:18081 + Added: NYM-EXIT udp port range 18080:18081 + Added: NYM-EXIT udp port range 18080:18081 Adding rules for EnsimControlPanel (Port: 19638) Added: NYM-EXIT tcp port 19638 Added: NYM-EXIT tcp port 19638 diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-status-output.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-status-output.mdx index 7a1eaa5acf..8e8293a865 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-status-output.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-status-output.mdx @@ -1,807 +1,795 @@ ```console -ESC[0;33mNym Exit Policy Status:ESC[0m -ESC[0;33m----------------------ESC[0m -ESC[0;32mNetwork Device:ESC[0m ens3 -ESC[0;32mWireguard Interface:ESC[0m nymwg +Nym Exit Policy Status: +---------------------- +Network Device: eth0 +Wireguard Interface: nymwg -ESC[0;33mInterface Details:ESC[0m -12: nymwg: mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 - link/none +Interface Details: +87: nymwg: mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 + link/none -ESC[0;33mIP Addresses:ESC[0m -12: nymwg: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 +IP Addresses: +87: nymwg: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 inet 10.1.0.1/32 brd 10.1.0.1 scope global nymwg valid_lft forever preferred_lft forever -12: nymwg: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 - inet6 fc01::1/112 scope global +87: nymwg: mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 + inet6 fc01::1/112 scope global valid_lft forever preferred_lft forever -ESC[0;33mIptables Chains:ESC[0m +Iptables Chains: IPv4 Chain: Chain NYM-EXIT (1 references) - pkts bytes target prot opt in out source destination - 0 0 REJECT 0 -- * * 0.0.0.0/0 5.188.10.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 5.188.11.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 31.132.36.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 31.184.237.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 37.9.42.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 43.229.52.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 45.9.148.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 45.43.128.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 45.142.120.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.148.112.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.148.120.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.148.127.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.173.208.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 79.110.22.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.121.39.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.193.75.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.200.12.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.200.81.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.200.82.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.200.83.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.200.164.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.216.3.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.220.163.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.200.248.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.243.90.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.243.91.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.243.93.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.234.99.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 103.99.0.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 103.215.80.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 103.239.28.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.166.96.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.207.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.233.0.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.239.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.243.192.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.247.96.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.250.192.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.250.224.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 107.182.112.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 107.190.160.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 141.136.22.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 150.129.40.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 159.174.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 162.222.128.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 162.249.20.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 163.53.247.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 166.117.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 167.74.0.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 167.160.96.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 168.64.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 168.76.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 168.129.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 169.239.152.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 170.114.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 172.98.0.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 174.136.192.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 176.121.14.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.159.97.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.159.100.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.159.107.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.14.192.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.14.193.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.14.195.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.21.8.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.39.8.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.71.0.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.77.248.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.116.172.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.116.175.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.124.56.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.129.8.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.130.36.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.130.40.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.140.53.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.143.220.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.143.222.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.143.223.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.146.168.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.165.153.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.193.90.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.244.29.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.244.30.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.244.31.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 188.247.230.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.26.25.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.31.212.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.43.175.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.43.176.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.43.184.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.161.80.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.251.231.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 193.228.91.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 194.5.97.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 194.5.98.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 194.5.99.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 195.182.57.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 196.45.120.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 196.61.192.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 196.196.8.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 196.199.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 197.231.208.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.20.16.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.45.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.56.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.151.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.151.152.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.178.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.183.32.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.186.25.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.187.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.200.0.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.200.8.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 198.206.140.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.5.152.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.34.128.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.84.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.89.16.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.120.163.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.166.200.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.185.192.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.196.192.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.198.160.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.212.96.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.223.0.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.241.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.249.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.253.224.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 199.254.32.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.19.38.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.44.224.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.52.96.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.87.199.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.107.208.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.126.244.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.130.16.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.147.64.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.232.0.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.144.0.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.148.192.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.151.128.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.159.45.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.172.244.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.189.71.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.189.72.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.203.0.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.233.224.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 205.236.189.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.124.104.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.195.224.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.197.165.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.209.80.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.224.160.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.226.0.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.226.32.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 206.227.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 207.22.192.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 207.45.224.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 207.110.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 207.110.128.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 209.66.128.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 216.179.128.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 217.8.116.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 217.8.117.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 223.169.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 223.254.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 42.4.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 68.119.232.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 68.215.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 69.244.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 70.111.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 70.126.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 112.78.2.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 195.20.40.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 14.160.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 27.2.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 27.106.108.128/25 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 37.236.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.100.21.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 39.32.0.0/11 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 41.190.2.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 41.190.30.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 45.116.232.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.118.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.161.9.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 60.184.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 62.44.134.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 78.85.40.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 79.11.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 79.108.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 81.93.93.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 83.24.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 83.149.19.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 84.18.126.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.198.140.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.116.176.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.227.224.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.241.88.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 89.114.108.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.196.250.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 93.122.192.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 95.0.60.160/27 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 95.110.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 89.189.152.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 103.26.246.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 106.51.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 109.124.0.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 109.124.16.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 109.126.128.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 109.175.6.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 111.125.108.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 112.101.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.80.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.128.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 114.96.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 114.115.128.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 115.72.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 118.20.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.24.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 125.167.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 139.5.157.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 146.185.223.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 154.68.4.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 177.55.154.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 177.125.30.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 177.224.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.135.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 179.5.103.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 181.67.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 181.174.101.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 182.69.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 182.160.100.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 182.184.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 182.253.162.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 183.82.128.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 183.128.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.36.88.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.150.15.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 185.172.86.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 186.179.100.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 189.216.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 190.235.110.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 190.239.190.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.64.121.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 193.34.141.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 193.188.254.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 197.229.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 201.148.126.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 202.136.88.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 200.121.192.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 220.164.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 221.228.192.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 24.0.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 27.184.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.0.0.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 58.53.128.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 59.92.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 60.52.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 60.176.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 60.215.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 61.163.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 62.194.131.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 64.175.32.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 67.116.236.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 67.121.120.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 67.124.36.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 68.62.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 69.112.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 71.56.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 76.112.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 78.97.32.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 80.108.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 81.240.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 82.72.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 82.169.28.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 84.127.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 84.144.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 84.220.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.48.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.54.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.85.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.86.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.176.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 89.217.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.176.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.182.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 92.0.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 92.112.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 92.128.0.0/12 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 109.128.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 110.212.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 111.85.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 112.224.0.0/11 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.70.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.89.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.111.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.224.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 113.240.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 114.246.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 114.248.80.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 115.60.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 115.213.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 116.238.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 117.22.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 117.136.0.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 118.80.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 118.168.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 120.0.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 120.68.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 121.29.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 122.169.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 122.173.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.67.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.101.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.112.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.134.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.161.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.174.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.188.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 123.244.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 124.89.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 124.128.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 124.134.0.0/17 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 124.94.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 125.93.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 125.125.176.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 125.224.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 150.70.75.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 166.204.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.191.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.125.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 183.91.2.0/23 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 183.184.0.0/13 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 188.23.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 188.98.0.0/15 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 189.64.0.0/14 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 201.53.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 201.82.64.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 212.56.64.0/18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 218.202.219.0/24 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 220.152.128.0/22 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 220.178.0.0/19 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 221.11.32.0/20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 222.183.0.0/16 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 222.240.216.0/21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 82.165.159.132/31 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 91.208.144.164 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 209.182.193.155 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 213.205.38.29 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 5.79.71.205 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 5.79.71.225 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.129.41 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.129.213 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.144.42 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.147.11 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.151.95 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.153.71 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.153.115 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.168.194 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.169.101 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.170.84 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.174.35 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.179.9 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.182.164 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.184.75 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.186.110 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.186.114 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.188.186 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 38.229.191.189 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 46.244.21.4 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 50.21.181.152 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 50.63.202.35 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 52.5.245.208 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 64.71.166.50 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 64.71.188.178 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 67.215.255.139 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 74.200.48.169 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 74.208.153.9 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 74.208.164.166 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 74.208.64.191 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.17.31.122 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 85.17.31.82 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.18.146 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.149.145 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.149.153 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.18.112 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.18.141 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.190.153 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.190.154 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.190.157 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.20.192 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.24.200 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.253.18 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 87.106.26.9 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 95.211.230.75 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.42.225.122 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 104.244.14.252 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 109.70.26.37 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 144.217.74.156 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 146.148.124.166 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 148.81.111.111 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 151.80.148.103 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 176.58.104.168 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.162.203.202 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.162.203.211 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.162.203.226 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 178.162.217.107 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 184.105.76.250 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 184.105.192.2 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.0.72.20 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.0.72.21 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.169.69.25 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.42.116.41 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 192.42.119.41 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 193.166.255.170 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 193.166.255.171 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 204.11.56.48 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 208.91.197.46 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 212.227.20.93 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 212.227.20.116 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 212.227.20.164 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 213.165.83.176 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 216.218.135.114 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 216.218.185.162 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 216.218.208.114 reject-with icmp-port-unreachable - 0 0 REJECT 0 -- * * 0.0.0.0/0 216.66.15.109 reject-with icmp-port-unreachable - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:706 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:706 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5432 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5432 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2082:2083 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2082:2083 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:8232:8233 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:8232:8233 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1500 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1500 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:123 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:123 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1293 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1293 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:11371 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:11371 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:443 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:110 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:110 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1194 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1194 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3074 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:3074 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1521 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1521 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:2049 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:2049 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:88 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:88 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:995 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:995 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5050 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5050 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:43 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:991 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:991 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:143 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:143 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5228 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5228 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:445 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:445 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1755 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1755 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:993 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:993 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9001 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9001 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:5222:5223 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:5222:5223 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:20:21 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:20:21 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:60000:61000 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:60000:61000 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2102:2104 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2102:2104 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:873 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:873 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:27000:27050 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:27000:27050 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9418 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9418 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1863 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1863 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:8087:8088 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:8087:8088 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9030 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9030 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:4643 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:4643 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9339 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9339 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:902:904 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:902:904 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1533 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1533 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2095:2096 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2095:2096 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5190 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5190 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:749 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:749 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:4321 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:4321 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10000 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:10000 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:53 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:53 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:19294 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:19294 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:220 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:220 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:8332:8333 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:8332:8333 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:64738 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:64738 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1723 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1723 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8443 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8443 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8888 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8888 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2086:2087 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2086:2087 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9735 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9735 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:554 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:554 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:853 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:853 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:22 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8082 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8082 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:992 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:992 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:25565 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:25565 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3690 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:3690 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:464 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:464 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:981 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:981 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9053 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9053 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:50002 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:50002 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9443 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9443 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:389 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:389 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:80:81 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:80:81 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:27017 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:27017 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:5000:5005 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:5000:5005 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1433 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1433 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8883 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8883 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3306 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:3306 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8767 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8767 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1677 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1677 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:19638 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:19638 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1220 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1220 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:79 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:79 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:989:990 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:989:990 - 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:636 - 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:636 - 0 0 REJECT 0 -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable + pkts bytes target prot opt in out source destination + 0 0 REJECT all -- * * 0.0.0.0/0 5.188.10.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 5.188.11.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 31.132.36.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 31.184.237.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 37.9.42.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 43.229.52.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 45.9.148.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 45.43.128.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 45.142.120.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.148.112.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.148.120.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.148.127.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.173.208.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 79.110.22.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.121.39.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.193.75.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.200.12.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.200.81.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.200.82.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.200.83.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.200.164.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.216.3.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.220.163.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.200.248.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.243.90.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.243.91.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.243.93.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.234.99.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 103.99.0.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 103.215.80.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 103.239.28.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.166.96.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.207.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.233.0.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.239.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.243.192.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.247.96.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.250.192.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.250.224.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 107.182.112.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 107.190.160.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 141.136.22.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 150.129.40.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 159.174.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 162.222.128.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 162.249.20.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 163.53.247.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 166.117.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 167.74.0.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 167.160.96.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 168.64.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 168.76.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 168.129.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 169.239.152.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 170.114.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 172.98.0.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 174.136.192.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 176.121.14.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.159.97.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.159.100.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.159.107.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.14.192.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.14.193.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.14.195.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.21.8.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.39.8.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.71.0.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.77.248.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.116.172.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.116.175.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.124.56.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.129.8.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.130.36.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.130.40.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.140.53.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.143.220.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.143.222.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.143.223.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.146.168.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.165.153.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.193.90.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.244.29.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.244.30.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.244.31.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 188.247.230.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.26.25.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.31.212.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.43.175.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.43.176.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.43.184.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.161.80.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.251.231.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 193.228.91.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 194.5.97.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 194.5.98.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 194.5.99.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 195.182.57.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 196.45.120.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 196.61.192.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 196.196.8.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 196.199.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 197.231.208.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.20.16.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.45.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.56.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.151.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.151.152.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.178.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.183.32.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.186.25.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.187.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.200.0.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.200.8.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 198.206.140.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.5.152.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.34.128.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.84.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.89.16.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.120.163.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.166.200.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.185.192.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.196.192.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.198.160.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.212.96.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.223.0.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.241.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.249.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.253.224.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 199.254.32.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.19.38.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.44.224.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.52.96.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.87.199.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.107.208.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.126.244.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.130.16.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.147.64.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.232.0.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.144.0.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.148.192.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.151.128.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.159.45.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.172.244.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.189.71.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.189.72.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.203.0.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.233.224.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 205.236.189.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.124.104.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.195.224.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.197.165.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.209.80.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.224.160.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.226.0.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.226.32.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 206.227.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 207.22.192.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 207.45.224.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 207.110.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 207.110.128.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 209.66.128.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 216.179.128.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 217.8.116.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 217.8.117.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 223.169.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 223.254.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 42.4.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 68.119.232.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 68.215.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 69.244.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 70.111.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 70.126.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 112.78.2.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 195.20.40.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 14.160.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 27.2.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 27.106.108.128/25 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 37.236.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.100.21.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 39.32.0.0/11 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 41.190.2.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 41.190.30.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 45.116.232.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.118.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.161.9.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 60.184.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 62.44.134.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 78.85.40.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 79.11.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 79.108.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 81.93.93.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 83.24.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 83.149.19.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 84.18.126.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.198.140.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.116.176.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.227.224.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.241.88.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 89.114.108.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.196.250.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 93.122.192.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 95.0.60.160/27 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 95.110.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 89.189.152.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 103.26.246.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 106.51.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 109.124.0.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 109.124.16.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 109.126.128.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 109.175.6.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 111.125.108.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 112.101.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.80.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.128.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 114.96.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 114.115.128.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 115.72.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 118.20.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.24.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 125.167.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 139.5.157.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 146.185.223.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 154.68.4.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 177.55.154.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 177.125.30.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 177.224.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.135.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 179.5.103.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 181.67.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 181.174.101.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 182.69.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 182.160.100.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 182.184.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 182.253.162.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 183.82.128.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 183.128.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.36.88.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.150.15.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 185.172.86.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 186.179.100.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 189.216.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 190.235.110.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 190.239.190.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.64.121.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 193.34.141.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 193.188.254.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 197.229.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 201.148.126.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 202.136.88.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 200.121.192.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 220.164.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 221.228.192.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 24.0.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 27.184.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.0.0.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 58.53.128.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 59.92.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 60.52.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 60.176.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 60.215.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 61.163.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 62.194.131.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 64.175.32.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 67.116.236.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 67.121.120.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 67.124.36.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 68.62.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 69.112.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 71.56.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 76.112.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 78.97.32.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 80.108.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 81.240.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 82.72.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 82.169.28.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 84.127.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 84.144.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 84.220.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.48.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.54.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.85.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.86.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.176.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 89.217.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.176.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.182.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 92.0.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 92.112.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 92.128.0.0/12 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 109.128.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 110.212.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 111.85.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 112.224.0.0/11 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.70.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.89.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.111.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.224.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 113.240.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 114.246.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 114.248.80.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 115.60.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 115.213.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 116.238.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 117.22.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 117.136.0.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 118.80.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 118.168.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 120.0.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 120.68.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 121.29.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 122.169.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 122.173.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.67.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.101.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.112.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.134.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.161.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.174.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.188.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 123.244.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 124.89.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 124.128.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 124.134.0.0/17 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 124.94.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 125.93.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 125.125.176.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 125.224.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 150.70.75.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 166.204.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.191.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.125.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 183.91.2.0/23 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 183.184.0.0/13 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 188.23.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 188.98.0.0/15 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 189.64.0.0/14 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 201.53.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 201.82.64.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 212.56.64.0/18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 218.202.219.0/24 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 220.152.128.0/22 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 220.178.0.0/19 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 221.11.32.0/20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 222.183.0.0/16 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 222.240.216.0/21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 82.165.159.132/31 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 91.208.144.164 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 209.182.193.155 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 213.205.38.29 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 5.79.71.205 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 5.79.71.225 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.129.41 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.129.213 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.144.42 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.147.11 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.151.95 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.153.71 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.153.115 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.168.194 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.169.101 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.170.84 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.174.35 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.179.9 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.182.164 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.184.75 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.186.110 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.186.114 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.188.186 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 38.229.191.189 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 46.244.21.4 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 50.21.181.152 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 50.63.202.35 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 52.5.245.208 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 64.71.166.50 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 64.71.188.178 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 67.215.255.139 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 74.200.48.169 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 74.208.153.9 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 74.208.164.166 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 74.208.64.191 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.17.31.122 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 85.17.31.82 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.18.146 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.149.145 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.149.153 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.18.112 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.18.141 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.190.153 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.190.154 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.190.157 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.20.192 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.24.200 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.253.18 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 87.106.26.9 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 95.211.230.75 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.42.225.122 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 104.244.14.252 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 109.70.26.37 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 144.217.74.156 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 146.148.124.166 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 148.81.111.111 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 151.80.148.103 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 176.58.104.168 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.162.203.202 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.162.203.211 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.162.203.226 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 178.162.217.107 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 184.105.76.250 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 184.105.192.2 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.0.72.20 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.0.72.21 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.169.69.25 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.42.116.41 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 192.42.119.41 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 193.166.255.170 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 193.166.255.171 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 204.11.56.48 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 208.91.197.46 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 212.227.20.93 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 212.227.20.116 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 212.227.20.164 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 213.165.83.176 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 216.218.135.114 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 216.218.185.162 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 216.218.208.114 reject-with icmp-port-unreachable + 0 0 REJECT all -- * * 0.0.0.0/0 216.66.15.109 reject-with icmp-port-unreachable + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:706 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:706 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5432 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5432 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2082:2083 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2082:2083 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:8232:8233 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:8232:8233 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1500 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1500 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:123 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:123 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1293 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1293 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:11371 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:11371 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:443 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:110 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:110 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1194 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1194 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3074 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:3074 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1521 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1521 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:2049 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:2049 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:88 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:88 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:995 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:995 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:25551 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:25551 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5050 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5050 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:43 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:991 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:991 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:143 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:143 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5228 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5228 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:445 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:445 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1755 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1755 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:993 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:993 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9001 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9001 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:5222:5223 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:5222:5223 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:20:21 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:20:21 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:60000:61000 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:60000:61000 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2102:2104 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2102:2104 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:873 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:873 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:27000:27050 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:27000:27050 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9418 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9418 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1863 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1863 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:8087:8088 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:8087:8088 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9030 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9030 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:4643 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:4643 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9339 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9339 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:902:904 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:902:904 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1533 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1533 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2095:2096 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2095:2096 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5190 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:5190 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:749 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:749 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:4321 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:4321 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:10000 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:10000 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:53 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:53 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:19294 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:19294 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:220 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:220 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:8332:8333 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:8332:8333 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:64738 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:64738 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:26661 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:26661 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1723 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1723 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8443 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8443 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8888 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8888 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:2086:2087 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:2086:2087 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9735 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9735 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:554 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:554 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:853 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:853 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:22 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8082 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8082 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:992 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:992 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:25565 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:25565 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3690 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:3690 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:464 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:464 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:18089 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:18089 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:981 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:981 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9053 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9053 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:50002 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:50002 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9443 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:9443 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:389 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:389 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:80:81 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:80:81 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:27017 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:27017 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:5000:5005 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:5000:5005 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1433 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1433 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8883 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8883 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3306 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:3306 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8767 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:8767 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1677 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1677 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:18080:18081 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:18080:18081 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:19638 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:19638 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:1220 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:1220 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:79 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:79 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpts:989:990 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpts:989:990 + 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:636 + 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:636 + 0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable IPv6 Chain: Chain NYM-EXIT (1 references) - pkts bytes target prot opt in out source destination - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:706 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:706 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:5432 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:5432 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:2082:2083 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:2082:2083 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:8232:8233 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:8232:8233 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1500 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1500 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:123 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:123 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1293 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1293 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:11371 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:11371 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:443 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:443 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:110 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:110 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1194 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1194 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:3074 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:3074 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1521 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1521 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:2049 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:2049 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:88 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:88 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:995 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:995 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:5050 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:5050 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:43 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:43 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:991 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:991 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:143 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:143 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:5228 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:5228 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:445 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:445 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1755 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1755 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:993 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:993 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9001 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9001 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:5222:5223 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:5222:5223 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:20:21 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:20:21 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:60000:61000 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:60000:61000 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:2102:2104 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:2102:2104 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:873 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:873 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:27000:27050 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:27000:27050 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9418 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9418 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1863 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1863 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:8087:8088 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:8087:8088 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9030 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9030 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:4643 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:4643 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9339 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9339 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:902:904 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:902:904 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1533 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1533 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:5222:5223 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:5222:5223 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:20:21 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:20:21 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:60000:61000 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:60000:61000 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:2102:2104 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:2102:2104 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:873 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:873 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:27000:27050 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:27000:27050 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9418 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9418 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1863 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1863 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:8087:8088 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:8087:8088 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9030 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9030 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:4643 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:4643 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9339 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9339 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:902:904 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:902:904 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1533 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1533 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:2095:2096 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:2095:2096 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:5190 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:5190 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:749 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:749 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:4321 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:4321 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:10000 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:10000 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:53 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:53 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:19294 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:19294 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:220 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:220 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:8332:8333 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:8332:8333 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:64738 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:64738 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1723 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1723 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:8443 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:8443 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:8888 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:8888 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:2086:2087 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:2086:2087 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9735 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9735 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:554 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:554 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:853 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:853 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:22 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:22 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:8082 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:8082 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:992 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:992 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:25565 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:25565 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:3690 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:3690 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:464 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:464 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:981 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:981 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9053 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9053 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:50002 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:50002 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:9443 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:9443 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:389 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:389 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:80:81 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:80:81 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:27017 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:27017 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:5000:5005 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:5000:5005 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1433 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1433 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:8883 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:8883 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:3306 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:3306 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:8767 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:8767 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1677 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1677 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:19638 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:19638 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:1220 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:1220 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:79 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:79 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpts:989:990 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpts:989:990 - 0 0 ACCEPT 6 -- * * ::/0 ::/0 tcp dpt:636 - 0 0 ACCEPT 17 -- * * ::/0 ::/0 udp dpt:636 - 0 0 REJECT 0 -- * * ::/0 ::/0 reject-with icmp6-port-unreachable + pkts bytes target prot opt in out source destination + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:706 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:706 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:5432 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:5432 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:2082:2083 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:2082:2083 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:8232:8233 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:8232:8233 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1500 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1500 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:123 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:123 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1293 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1293 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:11371 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:11371 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:443 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:443 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:110 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:110 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1194 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1194 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:3074 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:3074 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1521 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1521 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:2049 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:2049 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:88 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:88 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:995 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:995 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:25551 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:25551 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:5050 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:5050 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:43 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:43 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:991 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:991 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:143 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:143 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:5228 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:5228 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:445 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:445 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1755 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1755 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:993 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:993 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9001 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9001 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:5222:5223 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:5222:5223 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:20:21 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:20:21 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:60000:61000 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:60000:61000 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:2102:2104 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:2102:2104 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:873 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:873 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:27000:27050 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:27000:27050 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9418 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9418 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1863 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1863 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:8087:8088 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:8087:8088 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9030 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9030 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:4643 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:4643 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9339 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9339 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:902:904 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:902:904 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1533 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1533 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:2095:2096 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:2095:2096 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:5190 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:5190 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:749 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:749 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:4321 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:4321 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:10000 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:10000 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:53 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:53 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:19294 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:19294 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:220 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:220 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:8332:8333 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:8332:8333 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:64738 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:64738 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:26661 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:26661 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1723 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1723 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:8443 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:8443 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:8888 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:8888 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:2086:2087 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:2086:2087 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9735 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9735 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:554 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:554 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:853 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:853 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:22 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:22 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:8082 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:8082 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:992 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:992 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:25565 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:25565 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:3690 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:3690 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:464 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:464 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:18089 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:18089 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:981 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:981 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9053 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9053 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:50002 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:50002 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:9443 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:9443 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:389 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:389 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:80:81 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:80:81 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:27017 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:27017 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:5000:5005 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:5000:5005 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1433 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1433 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:8883 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:8883 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:3306 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:3306 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:8767 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:8767 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1677 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1677 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:18080:18081 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:18080:18081 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:19638 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:19638 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:1220 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:1220 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:79 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:79 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpts:989:990 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpts:989:990 + 0 0 ACCEPT tcp * * ::/0 ::/0 tcp dpt:636 + 0 0 ACCEPT udp * * ::/0 ::/0 udp dpt:636 + 0 0 REJECT all * * ::/0 ::/0 reject-with icmp6-port-unreachable -ESC[0;33mIP Forwarding:ESC[0m +IP Forwarding: IPv4: 1 IPv6: 1 ``` diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-test-output.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-test-output.mdx index c7ee424647..184f858c83 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-test-output.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-test-output.mdx @@ -2,41 +2,61 @@ Running Nym Exit Policy Verification Tests... Testing Port Range Rules... Testing FTP tcp port range 20-21 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:20:21 ✓ Rule exists: NYM-EXIT tcp port range 20:21 Testing HTTP tcp port range 80-81 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:80:81 ✓ Rule exists: NYM-EXIT tcp port range 80:81 Testing CPanel tcp port range 2082-2083 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:2082:2083 ✓ Rule exists: NYM-EXIT tcp port range 2082:2083 Testing XMPP tcp port range 5222-5223 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:5222:5223 ✓ Rule exists: NYM-EXIT tcp port range 5222:5223 Testing Steam (sampling) tcp port range 27000-27050 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:27000:27050 ✓ Rule exists: NYM-EXIT tcp port range 27000:27050 Testing FTP over TLS tcp port range 989-990 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:989:990 ✓ Rule exists: NYM-EXIT tcp port range 989:990 Testing RTP/VoIP tcp port range 5000-5005 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:5000:5005 ✓ Rule exists: NYM-EXIT tcp port range 5000:5005 Testing Simplify Media tcp port range 8087-8088 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8087:8088 ✓ Rule exists: NYM-EXIT tcp port range 8087:8088 Testing Zcash tcp port range 8232-8233 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8232:8233 ✓ Rule exists: NYM-EXIT tcp port range 8232:8233 Testing Bitcoin tcp port range 8332-8333 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8332:8333 ✓ Rule exists: NYM-EXIT tcp port range 8332:8333 +Testing Monero tcp port range 18080-18081 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:18080:18081 +✓ Rule exists: NYM-EXIT tcp port range 18080:18081 Test test_port_range_rules PASSED Testing Critical Service Rules... +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:22 ✓ Rule exists: NYM-EXIT tcp port 22 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:53 ✓ Rule exists: NYM-EXIT tcp port 53 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:443 ✓ Rule exists: NYM-EXIT tcp port 443 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:853 ✓ Rule exists: NYM-EXIT tcp port 853 +ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:1194 ✓ Rule exists: NYM-EXIT tcp port 1194 +ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:53 ✓ Rule exists: NYM-EXIT udp port 53 +ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:123 ✓ Rule exists: NYM-EXIT udp port 123 +ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:1194 ✓ Rule exists: NYM-EXIT udp port 1194 Relevant existing rules for HTTP (port 80): Test test_critical_services PASSED This test takes some time, do not quit the process Testing Default Reject Rule... - ✓ Default REJECT rule exists Test test_default_reject_rule PASSED diff --git a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json new file mode 100644 index 0000000000..83ee4980db --- /dev/null +++ b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json @@ -0,0 +1,18 @@ +{ + "total_supply": { + "denom": "unym", + "amount": "1000000000000000" + }, + "mixmining_reserve": { + "denom": "unym", + "amount": "182883243257647" + }, + "vesting_tokens": { + "denom": "unym", + "amount": "0" + }, + "circulating_supply": { + "denom": "unym", + "amount": "817116756742353" + } +} diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index f71e380d34..3762743930 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -808_623_916 +817_116_756 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md new file mode 100644 index 0000000000..2a89edffe7 --- /dev/null +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md @@ -0,0 +1 @@ +5_080 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 39bbb86bce..069e6eb141 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.64% +0.77% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index e36dbca0e2..486924e832 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -44.811 +37.305 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index 6a6f41769e..7dce4fcd05 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -1_028_488 +250_000 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md index 602604b10d..1fc1b2163f 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md @@ -1 +1 @@ -50.0% +7.34% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index dd8fe066d0..a4417fced3 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -404_311_958 +60_000_000 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md new file mode 100644 index 0000000000..a4417fced3 --- /dev/null +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md @@ -0,0 +1 @@ +60_000_000 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index d4a66cde69..ea63891258 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -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 | 191_376_083 | +| Mixmining Reserve | Tokens releasing for operators rewards | 182_883_243 | | Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 808_623_916 | -| Stake Saturation | Optimal size of node self-bond + delegation | 1_028_488 | +| Circulating Supply | Amount of unlocked tokens | 817_116_756 | +| Stake Saturation | Optimal size of node self-bond + delegation | 250_000 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json new file mode 100644 index 0000000000..cd175a8719 --- /dev/null +++ b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json @@ -0,0 +1,18 @@ +{ + "interval": { + "reward_pool": "182883243257647.891553460395608456", + "staking_supply": "60000000000000", + "staking_supply_scale_factor": "0.07342892", + "epoch_reward_budget": "5080090090.490219209818344322", + "stake_saturation_point": "250000000000", + "sybil_resistance": "0.3", + "active_set_work_factor": "10", + "interval_pool_emission": "0.02" + }, + "rewarded_set": { + "entry_gateways": 80, + "exit_gateways": 100, + "mixnodes": 60, + "standby": 0 + } +} diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index f2de5fca15..f42ee9d860 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Thursday, April 3rd 2025, 13:40:32 UTC +Thursday, September 4th 2025, 09:12:25 UTC diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-help.md index 62836e167d..f1af88721d 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-help.md @@ -2,13 +2,14 @@ Usage: nym-node [OPTIONS] Commands: - build-info Show build information of this binary - bonding-information Show bonding information of this node depending on its currently selected mode - node-details Show details of this node - migrate Attempt to migrate an existing mixnode or gateway into a nym-node - run Start this nym-node - sign Use identity key of this node to sign provided message - help Print this message or the help of the given subcommand(s) + build-info Show build information of this binary + bonding-information Show bonding information of this node depending on its currently selected mode + node-details Show details of this node + migrate Attempt to migrate an existing mixnode or gateway into a nym-node + run Start this nym-node + sign Use identity key of this node to sign provided message + unsafe-reset-sphinx-keys UNSAFE: reset existing sphinx keys and attempt to generate fresh one for the current network state + help Print this message or the help of the given subcommand(s) Options: -c, --config-env-file diff --git a/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md b/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md index b1d4cec752..bbc71cfd4b 100644 --- a/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md +++ b/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md @@ -1,21 +1,45 @@ -| **ISP** | **Locations** | **Public IPv6** | **Crypto Payments** | **Comments** | **Last Updated** | -|:------------------------------------------------|:---------------------------------------------------------------------------------------------------------|:-------------------------------------|:---------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------| -| [AlexHost](https://alexhost.com) | Moldova, Bulgaria, Sweden, Netherlands | Yes, on by default | Yes | They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) | 07/2024 | -| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 | -| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesn’t match the location offered by the provider. | 05/2024 | -| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 | -| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 | -| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 | -| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Crypto payments must be done per each server monthly or annually. | 05/2024 | -| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 | -| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 | -| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 | -| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 | -| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 | -| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 | -| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 | -| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 | -| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 | -| [TerraHost](https://terrahost.no) | Norway | Yes, on by default | Yes | Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser | 07/2024 | -| [iHostArt](https://ihostart.com) | Romania | Yes, on by default | Yes | Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. | 07/2024 | -| [vSys Host](https://vsys.host) | Ukraine, Netherlands, USA | Yes, on by default | Yes | Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. | 07/2024 | +| **ISP** | **Locations** | **Public IPv6** | **Crypto Payments** | **Comments** | **Last Updated** | +|:---------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------|:---------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------| +| [AlexHost](https://alexhost.com) | Moldova, Bulgaria, Sweden, Netherlands | Yes, on by default | Yes | They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) | 07/2024 | +| [AmeriNoc](https://www.amerinoc.com) | USA | Yes | nan | nan | 07/2025 | +| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 | +| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesn’t match the location offered by the provider. | 05/2024 | +| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | nan | 07/2025 | +| [DataPacket](https://www.datapacket.com/pricing) | NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP | Yes | nan | nan | 07/2025 | +| [Dataclub](https://www.dataclub.eu/) | Latvia, Sweden, Netherlands | Yes | nan | nan | 07/2027 | +| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 | +| [FranTech](https://my.frantech.ca) | USA | Yes | nan | nan | 07/2025 | +| [Fsit](https://www.fsit.com/server/vps-vserver-kvm) | Swiss | Yes | Yes | nan | 07/2025 | +| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 | +| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 | +| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Not fast enough, Crypto payments must be done per each server monthly or annually. | 07/2025 | +| [Hostraha](https://hostraha.com) | Kenya and other African countries | No, but advertised otherwise | Yes, USDT TRC20 | Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times. | 08/2025 | +| [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) | Various countries with different pricing | nan | Yes | nan | 07/2025 | +| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 | +| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 | +| [Incognet](https://incognet.io/kansas-city-dedicated-servers) | USA, Netherlands | Yes | nan | nan | 07/2025 | +| [Ionos](https://www.ionos.com/servers/amd-servers) | US, DE, UK, ESP, FR | nan | No | nan | 07/2025 | +| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 | +| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | US, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 | +| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 | +| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 | +| [Lowendbox](https://lowendbox.com/category/dedicated-servers) | | | | Just an aggregator with good offers | 07/2025 | +| [M247](https://m247.com/eu/services/host/dedicated-servers/) | UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands | Yes | No | nan | 07/2025 | +| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 | +| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 | +| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 | +| [NiceVPS](https://nicevps.net/) | Netherlands | Yes | nan | nan | 07/2025 | +| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 | +| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Not all locations always available | 07/2025 | +| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 | +| [PrivateLayer](https://privatelayer.com) | Swiss | Yes | Yes | Slow customer response | 07/2025 | +| [Privex](https://www.privex.io/tor-exit-policy/) | USA, Germany, Sweden | Yes | Yes | nan | 07/2025 | +| [Psychz](https://www.psychz.net) | US, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 | +| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 | +| [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) | USA, Canada | nan | No | nan | 07/2025 | +| [Svea](https://svea.net/vps) | Sweden | Yes | nan | nan | 07/2025 | +| [TerraHost](https://terrahost.no) | Norway | Yes, on by default | Yes | Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser | 07/2024 | +| [Thundervm](https://thundervm.com/en/hosting/dedicated-server) | USA, UK, France, Italy, Switzerland, Netherlands | nan | Yes | | 07/2025 | +| [Zenlayer](https://www.zenlayer.com/bare-metal/) | [advertised over 50 locations](50+ https://www.zenlayer.com/global-network) | nan | nan | nan | 07/2025 | +| [iHostArt](https://ihostart.com) | Romania | Yes, on by default | Yes | Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. | 07/2024 | +| [vSys Host](https://vsys.host) | Ukraine, Netherlands, USA | Yes, on by default | Yes | Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. | 07/2024 | diff --git a/documentation/docs/data/csv/isp-sheet.csv b/documentation/docs/data/csv/isp-sheet.csv index f72973f924..aba01a1c8d 100644 --- a/documentation/docs/data/csv/isp-sheet.csv +++ b/documentation/docs/data/csv/isp-sheet.csv @@ -1,26 +1,44 @@ **ISP**,**Locations**,**Public IPv6**,**Crypto Payments**,**Comments**,**Last Updated** -[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR","Very slow customer support","05/2024" -[BitLaunch](https://bitlaunch.io),"Canada, USA, UK","No","Yes","Expensive. Digial Ocean through BitLanch has IPv6","05/2024" -[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box","Yes","Crypto payments must be done per each server monthly or annually.","05/2024" -[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy","Yes out of the box","No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed","05/2024" -[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore","No","Yes","Issued IP doesn’t match the location offered by the provider.","05/2024" -[Njalla](https://nja.la),"Sweden","Yes","Yes","Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.","05/2024" -[HostSailor](https://hostsailor.com),"USA","Yes, based on ticket","Yes","The IPv6 setup needs custom research and is not documented","05/2024" -[Misaka](https://www.misaka.io/),"South Africa","Yes, native support","No","Very Expensive","05/2024" -[IsHosting](https://ishosting.com/en),"Brazil, Netherlands","Yes, based on ticket","Yes","Expensive","05/2024" -[AlexHost](https://alexhost.com),"Moldova, Bulgaria, Sweden, Netherlands","Yes, on by default","Yes","They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR)","07/2024" -[iHostArt](https://ihostart.com),"Romania","Yes, on by default","Yes","Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware.","07/2024" -[Incognet](https://incognet.io),"Netherlands and USA","Yes, on by default","Yes","They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits","07/2024" -[vSys Host](https://vsys.host),"Ukraine, Netherlands, USA","Yes, on by default","Yes","Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check.","07/2024" -[LiteServer](https://liteserver.nl),"Netherlands","Yes, on by default","Yes","Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal","07/2024" -[TerraHost](https://terrahost.no),"Norway","Yes, on by default","Yes","Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser","07/2024" -[Mevspace](https://mevspace.com),"Poland","Yes, on by default","Yes","Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff","07/2024" -[Hostiko](https://hostiko.com.ua),"Ukraine, Germany","Yes, on by default","Yes","Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket.","07/2024" -[Hostslick](https://hostslick.com),"Netherlands, Germany","Yes, on by default","Yes","Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node","07/2024" -[RDP](https://rdp.sh),"Netherlands, USA, Poland","Yes, on by default","Yes","German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node.","07/2024" - - - - - - +[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR",Very slow customer support,05/2024 +[BitLaunch](https://bitlaunch.io),"Canada, USA, UK",No,Yes,Expensive. Digial Ocean through BitLanch has IPv6,05/2024 +[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box",Yes,"Not fast enough, Crypto payments must be done per each server monthly or annually.",07/2025 +[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy",Yes out of the box,"No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed",05/2024 +[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore",No,Yes,Issued IP doesn’t match the location offered by the provider.,05/2024 +[Njalla](https://nja.la),Sweden,Yes,Yes,"Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.",05/2024 +[HostSailor](https://hostsailor.com),USA,"Yes, based on ticket",Yes,The IPv6 setup needs custom research and is not documented,05/2024 +[Misaka](https://www.misaka.io/),South Africa,"Yes, native support",No,Very Expensive,05/2024 +[IsHosting](https://ishosting.com/en),"Brazil, Netherlands","Yes, based on ticket",Yes,Expensive,05/2024 +[AlexHost](https://alexhost.com),"Moldova, Bulgaria, Sweden, Netherlands","Yes, on by default",Yes,"They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR)",07/2024 +[iHostArt](https://ihostart.com),Romania,"Yes, on by default",Yes,"Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware.",07/2024 +[Incognet](https://incognet.io),Netherlands and USA,"Yes, on by default",Yes,They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits,07/2024 +[vSys Host](https://vsys.host),"Ukraine, Netherlands, USA","Yes, on by default",Yes,"Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check.",07/2024 +[LiteServer](https://liteserver.nl),Netherlands,"Yes, on by default",Yes,Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal,07/2024 +[TerraHost](https://terrahost.no),Norway,"Yes, on by default",Yes,Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser,07/2024 +[Mevspace](https://mevspace.com),Poland,"Yes, on by default",Yes,"Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff",07/2024 +[Hostiko](https://hostiko.com.ua),"Ukraine, Germany","Yes, on by default",Yes,"Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket.",07/2024 +[Hostslick](https://hostslick.com),"Netherlands, Germany","Yes, on by default",Yes,Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node,07/2024 +[RDP](https://rdp.sh),"Netherlands, USA, Poland","Yes, on by default",Yes,"German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node.",07/2024 +[Lowendbox](https://lowendbox.com/category/dedicated-servers), , , ,Just an aggregator with good offers,07/2025 +[Thundervm](https://thundervm.com/en/hosting/dedicated-server),"USA, UK, France, Italy, Switzerland, Netherlands",,Yes, ,07/2025 +[OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/),"USA, DE, FR, UK, PL, CA", ,No,Not all locations always available,07/2025 +[Mebilcom](https://www.melbicom.net/dedicatedserver/),"NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL",,No,,07/2025 +[Servermania](https://www.servermania.com/dedicated-servers-hosting.htm),"USA, Canada",,No,,07/2025 +[Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6),"PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR",Yes,No,,07/2025 +[Ionos](https://www.ionos.com/servers/amd-servers),"US, DE, UK, ESP, FR",,No,,07/2025 +[Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134),"US, NL, DE, UK, CA, SG, JP, AUS, HK",,No,KYC mandatory,07/2025 +[M247](https://m247.com/eu/services/host/dedicated-servers/),"UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands",Yes,No,,07/2025 +[Hostroyale](https://hostroyale.com/hosting/dedicated-server/),Various countries with different pricing,, Yes,,07/2025 +[DataPacket](https://www.datapacket.com/pricing),"NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP",Yes,,,07/2025 +[Zenlayer](https://www.zenlayer.com/bare-metal/), [advertised over 50 locations](50+ https://www.zenlayer.com/global-network),,,,07/2025 +[PrivateLayer](https://privatelayer.com),Swiss,Yes,Yes,Slow customer response,07/2025 +[AmeriNoc](https://www.amerinoc.com),USA,Yes,,,07/2025 +[Colocall](https://www.colocall.net/),Ukraine,Yes,,,07/2025 +[Incognet](https://incognet.io/kansas-city-dedicated-servers),"USA, Netherlands",Yes,,,07/2025 +[FranTech](https://my.frantech.ca),USA,Yes,,,07/2025 +[Psychz](https://www.psychz.net),"US, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025 +[Fsit](https://www.fsit.com/server/vps-vserver-kvm),Swiss,Yes,Yes,,07/2025 +[NiceVPS](https://nicevps.net/),Netherlands,Yes,,,07/2025 +[Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027 +[Privex](https://www.privex.io/tor-exit-policy/),"USA, Germany, Sweden",Yes,Yes,,07/2025 +[Svea](https://svea.net/vps),Sweden,Yes,,,07/2025 +[Hostraha](https://hostraha.com),Kenya and other African countries,"No, but advertised otherwise","Yes, USDT TRC20","Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times.",08/2025 diff --git a/documentation/docs/next.config.js b/documentation/docs/next.config.js index 0f9f7feee9..6d6a79257e 100644 --- a/documentation/docs/next.config.js +++ b/documentation/docs/next.config.js @@ -360,6 +360,12 @@ const config = { permanent: true, basePath: false, }, + { + source: "/developers/tutorials/rust-sdk.html", + destination: "/docs/developers/rust/mixnet/examples", + permanent: true, + basePath: false, + }, { source: "/developers/integrations/integration-options.html", destination: "/docs/developers/integrations", @@ -566,6 +572,12 @@ const config = { permanent: true, basePath: false, }, + { + source: "/docs/sdk/rust.html", + destination: "/docs/developers/rust", + permanent: true, + basePath: false, + }, { source: "/docs/sdk/typescript.html", destination: "/docs/developers/typescript", diff --git a/documentation/docs/package.json b/documentation/docs/package.json index 172d7c85da..6be16afb45 100644 --- a/documentation/docs/package.json +++ b/documentation/docs/package.json @@ -45,7 +45,7 @@ "chain-registry": "^1.19.0", "cosmjs-types": "^0.9.0", "lucide-react": "^0.438.0", - "next": "^14.2.15", + "next": "^15.2.4", "nextra": "2", "nextra-theme-docs": "2", "react": "^18.2.0", diff --git a/documentation/docs/pages/apis/ns-api/ns-api-run-deploy.mdx b/documentation/docs/pages/apis/ns-api/ns-api-run-deploy.mdx index 9d787172d3..4df6a92c8d 100644 --- a/documentation/docs/pages/apis/ns-api/ns-api-run-deploy.mdx +++ b/documentation/docs/pages/apis/ns-api/ns-api-run-deploy.mdx @@ -306,6 +306,7 @@ export RUST_LOG="info" export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" export NODE_STATUS_AGENT_SERVER_PORT="8000" export NODE_STATUS_AGENT_AUTH_KEY= +export NODE_STATUS_AGENT_PROBE_MNEMONIC= export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1" workers=${1:-1} diff --git a/documentation/docs/pages/developers/rust.md b/documentation/docs/pages/developers/rust.md deleted file mode 100644 index e396395bfa..0000000000 --- a/documentation/docs/pages/developers/rust.md +++ /dev/null @@ -1,4 +0,0 @@ -# Introduction -The Rust SDK allows developers building applications in Rust to import and interact with Nym clients as they would any other dependency, instead of running the client as a separate process on their machine. - -Check the [development status](./rust/development-status) page to see the various modules that make up the SDK, and the [FFI](./rust/ffi) page for Go/C++ developers. diff --git a/documentation/docs/pages/developers/rust.mdx b/documentation/docs/pages/developers/rust.mdx new file mode 100644 index 0000000000..db5aa11b61 --- /dev/null +++ b/documentation/docs/pages/developers/rust.mdx @@ -0,0 +1,16 @@ +# Introduction + + +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + +The Rust SDK allows developers building applications in Rust to import and interact with Nym clients as they would any other dependency, instead of running the client as a separate process on their machine. + +Check the [development status](./rust/development-status) page to see the various modules that make up the SDK, and the [FFI](./rust/ffi) page for Go/C++ developers. diff --git a/documentation/docs/pages/developers/rust/_meta.json b/documentation/docs/pages/developers/rust/_meta.json index d193319720..dda7e68e20 100644 --- a/documentation/docs/pages/developers/rust/_meta.json +++ b/documentation/docs/pages/developers/rust/_meta.json @@ -4,6 +4,5 @@ "mixnet": "Mixnet Module", "tcpproxy": "TcpProxy Module", "client-pool": "Client Pool", - "ffi": "FFI", - "tutorials": "Tutorials (Coming Soon)" + "ffi": "FFI" } diff --git a/documentation/docs/pages/developers/rust/client-pool.mdx b/documentation/docs/pages/developers/rust/client-pool.mdx index 584b642f71..6c444ad9e8 100644 --- a/documentation/docs/pages/developers/rust/client-pool.mdx +++ b/documentation/docs/pages/developers/rust/client-pool.mdx @@ -1,5 +1,16 @@ # Client Pool +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + We have a configurable-size Client Pool for processes that require multiple clients in quick succession (this is used by default by the [`TcpProxyClient`](./tcpproxy) for instance) This will be useful for developers looking to build connection logic, or just are using raw SDK clients in a sitatuation where there are multiple connections with a lot of churn. diff --git a/documentation/docs/pages/developers/rust/client-pool/architecture.mdx b/documentation/docs/pages/developers/rust/client-pool/architecture.mdx index 4159796c1e..415ad51ce1 100644 --- a/documentation/docs/pages/developers/rust/client-pool/architecture.mdx +++ b/documentation/docs/pages/developers/rust/client-pool/architecture.mdx @@ -1,5 +1,17 @@ # Client Pool Architecture + +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + + ## Motivations In situations where multiple connections are expected, and the number of connections can vary greatly, the Client Pool reduces time spent waiting for the creation of a Mixnet Client blocking your code sending traffic through the Mixnet. Instead, a configurable number of Clients can be generated and run in the background which can be very quickly grabbed, used, and disconnected. diff --git a/documentation/docs/pages/developers/rust/client-pool/example.md b/documentation/docs/pages/developers/rust/client-pool/example.mdx similarity index 88% rename from documentation/docs/pages/developers/rust/client-pool/example.md rename to documentation/docs/pages/developers/rust/client-pool/example.mdx index 1125865895..90c2e5f331 100644 --- a/documentation/docs/pages/developers/rust/client-pool/example.md +++ b/documentation/docs/pages/developers/rust/client-pool/example.mdx @@ -1,5 +1,15 @@ # Client Pool Example +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + > You can find this code [here](https://github.com/nymtech/nym/blob/develop/sdk/rust/nym-sdk/examples/client_pool.rs) ```rust diff --git a/documentation/docs/pages/developers/rust/development-status.md b/documentation/docs/pages/developers/rust/development-status.mdx similarity index 75% rename from documentation/docs/pages/developers/rust/development-status.md rename to documentation/docs/pages/developers/rust/development-status.mdx index cde8d04d72..bc19997015 100644 --- a/documentation/docs/pages/developers/rust/development-status.md +++ b/documentation/docs/pages/developers/rust/development-status.mdx @@ -1,4 +1,16 @@ # Development status + + +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + The SDK is still somewhat a work in progress: interfaces are fairly stable but still may change in subsequent releases. In the future the SDK will be made up of several modules, each of which will allow developers to interact with different parts of Nym infrastructure. diff --git a/documentation/docs/pages/developers/rust/ffi.mdx b/documentation/docs/pages/developers/rust/ffi.mdx index 848ca1755e..69ccfe3b75 100644 --- a/documentation/docs/pages/developers/rust/ffi.mdx +++ b/documentation/docs/pages/developers/rust/ffi.mdx @@ -1,7 +1,14 @@ -import { Callout } from 'nextra/components' # FFI Bindings +import { Callout } from 'nextra/components'; + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + We are working on the intitial versions of the FFI code to allow developers to experiment and get feedback. Please get in touch if you think the FFI bindings are lacking certain functionality. diff --git a/documentation/docs/pages/developers/rust/importing.md b/documentation/docs/pages/developers/rust/importing.mdx similarity index 62% rename from documentation/docs/pages/developers/rust/importing.md rename to documentation/docs/pages/developers/rust/importing.mdx index 7a4c9cf918..fb487d9048 100644 --- a/documentation/docs/pages/developers/rust/importing.md +++ b/documentation/docs/pages/developers/rust/importing.mdx @@ -1,4 +1,14 @@ # Installation +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + The `nym-sdk` crate is **not yet available via [crates.io](https://crates.io)**. As such, in order to import the crate you must specify the Nym monorepo in your `Cargo.toml` file. Since the `HEAD` of `master` is always the most recent release, we recommend developers use that for their imports, unless they have a reason to pull in a specific historic version of the code. ```toml diff --git a/documentation/docs/pages/developers/rust/mixnet.mdx b/documentation/docs/pages/developers/rust/mixnet.mdx index a6e6621413..c6173329f1 100644 --- a/documentation/docs/pages/developers/rust/mixnet.mdx +++ b/documentation/docs/pages/developers/rust/mixnet.mdx @@ -1,4 +1,13 @@ # Mixnet Module +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + This module exposes the logic of creating and interacting with clients and Mixnet messages. This is recommended for those wanting to either start playing around with the Mixnet and how it works, or build connection logic. diff --git a/documentation/docs/pages/developers/rust/mixnet/examples.md b/documentation/docs/pages/developers/rust/mixnet/examples.mdx similarity index 51% rename from documentation/docs/pages/developers/rust/mixnet/examples.md rename to documentation/docs/pages/developers/rust/mixnet/examples.mdx index 9bac32f1b1..5e21e1883a 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples.mdx @@ -1,5 +1,15 @@ # Examples +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + All the following examples can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/master/sdk/rust/nym-sdk/examples) in the monorepo. Just navigate to `nym/sdk/rust/nym-sdk/examples/` and run the files from there with: ```sh diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/builders.md b/documentation/docs/pages/developers/rust/mixnet/examples/builders.md deleted file mode 100644 index ff837d33f1..0000000000 --- a/documentation/docs/pages/developers/rust/mixnet/examples/builders.md +++ /dev/null @@ -1,3 +0,0 @@ -# Builder Patterns - -Since there are two ways of creating an SDK client - ephemeral and with-storage - then there are two ways of applying the Builder Pattern to client creation. diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/builders.mdx b/documentation/docs/pages/developers/rust/mixnet/examples/builders.mdx new file mode 100644 index 0000000000..fed758990b --- /dev/null +++ b/documentation/docs/pages/developers/rust/mixnet/examples/builders.mdx @@ -0,0 +1,11 @@ +# Builder Patterns +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + +Since there are two ways of creating an SDK client - ephemeral and with-storage - then there are two ways of applying the Builder Pattern to client creation. diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder-with-storage.md b/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder-with-storage.mdx similarity index 83% rename from documentation/docs/pages/developers/rust/mixnet/examples/builders/builder-with-storage.md rename to documentation/docs/pages/developers/rust/mixnet/examples/builders/builder-with-storage.mdx index 88ecf42085..0d86bea5dc 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder-with-storage.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder-with-storage.mdx @@ -1,5 +1,13 @@ # Mixnet Client Builder with Storage +import { Callout } from 'nextra/components'; + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + The previous example involves ephemeral keys - if we want to create and then maintain a client identity over time, our code becomes a little more complex as we need to create, store, and conditionally load these keys. > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/builder_with_storage.rs). diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder.md b/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder.mdx similarity index 76% rename from documentation/docs/pages/developers/rust/mixnet/examples/builders/builder.md rename to documentation/docs/pages/developers/rust/mixnet/examples/builders/builder.mdx index 1dc8da6701..d94dfae86a 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/builders/builder.mdx @@ -1,5 +1,13 @@ # Mixnet Client Builder +import { Callout } from 'nextra/components'; + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + You can spin up an ephemeral client like so. This client will not have a persistent identity and its keys will be dropped on restart. Since there is currently no way of reconnecting a client that has been disconnected after use, then treat disconnecting a client the same as dropping its keys entirely. > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/builder.rs). diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology.mdx b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology.mdx index 1985900b06..e26b346976 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology.mdx +++ b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology.mdx @@ -2,6 +2,14 @@ import { Callout } from 'nextra/components' + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + These examples are **not** the same as using a configurable network: these functions define a subset of nodes to use on a given network, whereas the [testnet](./testnet) example is an example of switching to use a different network entirely. The two can be combined, but if you are looking for how to connect your client to a testnet, see the `testnet` file. diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.md b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx similarity index 86% rename from documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.md rename to documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx index 40b4a87f91..3f833616be 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx @@ -1,5 +1,13 @@ # Custom Topology Provider +import { Callout } from 'nextra/components'; + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + If you are also running a Validator and Nym API for your network, you can specify that endpoint as such and interact with it as clients usually do (under the hood). > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/custom_topology_provider.rs) diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/manually-overwrite-topology.md b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/manually-overwrite-topology.mdx similarity index 88% rename from documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/manually-overwrite-topology.md rename to documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/manually-overwrite-topology.mdx index dd150ad35b..e3c6d1fec2 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/manually-overwrite-topology.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/manually-overwrite-topology.mdx @@ -1,5 +1,13 @@ # Manually Overwrite Topology +import { Callout } from 'nextra/components'; + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + If you aren't running a Validator and Nym API, and just want to import a specific sub-set of mix nodes, you can simply overwrite the grabbed topology manually. > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs) diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/simple.md b/documentation/docs/pages/developers/rust/mixnet/examples/simple.mdx similarity index 70% rename from documentation/docs/pages/developers/rust/mixnet/examples/simple.md rename to documentation/docs/pages/developers/rust/mixnet/examples/simple.mdx index 9df473686f..c6065d6231 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/simple.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/simple.mdx @@ -1,4 +1,14 @@ # Simple Send + +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + Lets look at a very simple example of how you can import and use the websocket client in a piece of Rust code. Simply importing the `nym_sdk` crate into your project allows you to create a client and send traffic through the mixnet. diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/socks.md b/documentation/docs/pages/developers/rust/mixnet/examples/socks.mdx similarity index 78% rename from documentation/docs/pages/developers/rust/mixnet/examples/socks.md rename to documentation/docs/pages/developers/rust/mixnet/examples/socks.mdx index 6daf6e5c07..03089679b7 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/socks.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/socks.mdx @@ -1,5 +1,14 @@ # Socks Proxy +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + If you are looking at implementing Nym as a transport layer for a crypto wallet or desktop app, this is probably the best place to start if they can speak SOCKS5, 4a, or 4. > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/socks5.rs) diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/split-send.md b/documentation/docs/pages/developers/rust/mixnet/examples/split-send.mdx similarity index 78% rename from documentation/docs/pages/developers/rust/mixnet/examples/split-send.md rename to documentation/docs/pages/developers/rust/mixnet/examples/split-send.mdx index b01e3a133b..6648e1956f 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/split-send.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/split-send.mdx @@ -1,4 +1,14 @@ # Send and Receive in Different Tasks + +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + If you need to split the different actions of your client across different tasks, you can do so like this. You can think of this analogously to spliting a Tcp Stream into read/write. This functionality is also useful for embedding a sending and receiving client into different tasks. > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs) diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/storage.md b/documentation/docs/pages/developers/rust/mixnet/examples/storage.mdx similarity index 92% rename from documentation/docs/pages/developers/rust/mixnet/examples/storage.md rename to documentation/docs/pages/developers/rust/mixnet/examples/storage.mdx index 7d82ce2993..3a103c78f0 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/storage.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/storage.mdx @@ -1,4 +1,14 @@ # Manually Handle Storage + +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + If you're integrating mixnet functionality into an existing app and want to integrate saving client configs and keys into your existing storage logic, you can manually perform these actions. > You can find this code [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/examples/manually_handle_storage.rs) diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/surbs.md b/documentation/docs/pages/developers/rust/mixnet/examples/surbs.mdx similarity index 87% rename from documentation/docs/pages/developers/rust/mixnet/examples/surbs.md rename to documentation/docs/pages/developers/rust/mixnet/examples/surbs.mdx index ab8d9abb79..1543102a63 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/surbs.md +++ b/documentation/docs/pages/developers/rust/mixnet/examples/surbs.mdx @@ -1,4 +1,14 @@ # Anonymous Replies with SURBs (Single Use Reply Blocks) + +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + Both functions used to send messages through the mixnet (`send_message` and `send_plain_message`) send a pre-determined number of SURBs along with their messages by default. You can read more about how SURBs function under the hood [here](../../../../network/traffic/anonymous-replies). diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/testnet.mdx b/documentation/docs/pages/developers/rust/mixnet/examples/testnet.mdx index 78112c7b54..606fcc8ead 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/testnet.mdx +++ b/documentation/docs/pages/developers/rust/mixnet/examples/testnet.mdx @@ -1,5 +1,13 @@ # Configurable Network +import { Callout } from 'nextra/components' + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + If you want to connect your Mixnet client to a different network than Mainnet, simply pull in a file from [`nym/envs`](https://github.com/nymtech/nym/tree/master/envs) as such: ```rust diff --git a/documentation/docs/pages/developers/rust/mixnet/message-helpers.md b/documentation/docs/pages/developers/rust/mixnet/message-helpers.mdx similarity index 86% rename from documentation/docs/pages/developers/rust/mixnet/message-helpers.md rename to documentation/docs/pages/developers/rust/mixnet/message-helpers.mdx index 2d2b550f86..b398bdd554 100644 --- a/documentation/docs/pages/developers/rust/mixnet/message-helpers.md +++ b/documentation/docs/pages/developers/rust/mixnet/message-helpers.mdx @@ -1,5 +1,15 @@ # Message Helpers +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + ## Handling incoming messages When listening out for a response to a sent message (e.g. if you have sent a request to a service, and are awaiting the response) you will want to await [non-empty messages (if you don't know why, read the info on this here)](./troubleshooting#client-receives-empty-messages-when-listening-for-response). This can be done with something like the helper functions here: diff --git a/documentation/docs/pages/developers/rust/mixnet/message-types.md b/documentation/docs/pages/developers/rust/mixnet/message-types.mdx similarity index 74% rename from documentation/docs/pages/developers/rust/mixnet/message-types.md rename to documentation/docs/pages/developers/rust/mixnet/message-types.mdx index 833b1cc3fc..80502b77e6 100644 --- a/documentation/docs/pages/developers/rust/mixnet/message-types.md +++ b/documentation/docs/pages/developers/rust/mixnet/message-types.mdx @@ -1,5 +1,15 @@ # Message Types +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + There are several functions used to send outgoing messages through the Mixnet, each with a different level of customisation: - `send(&self, message: InputMessage) -> Result<()>` diff --git a/documentation/docs/pages/developers/rust/mixnet/troubleshooting.md b/documentation/docs/pages/developers/rust/mixnet/troubleshooting.mdx similarity index 94% rename from documentation/docs/pages/developers/rust/mixnet/troubleshooting.md rename to documentation/docs/pages/developers/rust/mixnet/troubleshooting.mdx index ff7b10577a..719cda58f3 100644 --- a/documentation/docs/pages/developers/rust/mixnet/troubleshooting.md +++ b/documentation/docs/pages/developers/rust/mixnet/troubleshooting.mdx @@ -1,4 +1,17 @@ # Troubleshooting + +import { Callout } from 'nextra/components'; + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + + Below are several common issues or questions you may have. If you come across something that isn't explained here, [PRs are welcome](https://github.com/nymtech/nym/issues/new/choose). diff --git a/documentation/docs/pages/developers/rust/tcpproxy.mdx b/documentation/docs/pages/developers/rust/tcpproxy.mdx index e4bfac03b1..ee7836d175 100644 --- a/documentation/docs/pages/developers/rust/tcpproxy.mdx +++ b/documentation/docs/pages/developers/rust/tcpproxy.mdx @@ -1,5 +1,13 @@ # TcpProxy Module +import { Callout } from 'nextra/components'; + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + This module exposes the `TcpProxyClient` and the `TcpProxyServer` which can be used to proxy traffic through the Mixnet in a way that is more familiar to developers than the methods exposed by the [`Mixnet` module](./mixnet). Both `Client` and `Server` are intended to be initialised and then run in a background thread, exposing a configurable `localhost` socket which developers can read/write/stream to without having to worry about the [message-based](../concepts/messages) nature of sending and receiving traffic to/from the Mixnet. diff --git a/documentation/docs/pages/developers/rust/tcpproxy/architecture.mdx b/documentation/docs/pages/developers/rust/tcpproxy/architecture.mdx index 006cd1ce6a..b9be09ecdc 100644 --- a/documentation/docs/pages/developers/rust/tcpproxy/architecture.mdx +++ b/documentation/docs/pages/developers/rust/tcpproxy/architecture.mdx @@ -1,7 +1,16 @@ -import { Callout } from 'nextra/components' # Architecture +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + + This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + + It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + + ## Motivations The motivation behind the creation of the `TcpProxy` module is to allow developers to interact with the Mixnet in a way that is far more familiar to them: simply setting up a connection with a transport, being returned a socket, and then being able to stream data to/from it, similar to something like the Tor [`arti`](https://gitlab.torproject.org/tpo/core/arti/-/tree/main/crates/arti-client) client. diff --git a/documentation/docs/pages/developers/rust/tcpproxy/examples/multiconn.mdx b/documentation/docs/pages/developers/rust/tcpproxy/examples/multiconn.mdx index a003b1bee8..6f3bcffd38 100644 --- a/documentation/docs/pages/developers/rust/tcpproxy/examples/multiconn.mdx +++ b/documentation/docs/pages/developers/rust/tcpproxy/examples/multiconn.mdx @@ -1,5 +1,13 @@ # Multi Connection Example +import { Callout } from 'nextra/components' + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + This example starts off several Tcp connections on a loop to a remote endpoint: in this case the `TcpListener` behind the `NymProxyServer` instance on the echo server found in [`nym/tools/echo-server/`](https://github.com/nymtech/nym/tree/develop/tools/echo-server). It pipes a few messages to it, logs the replies, and keeps track of the number of replies received per connection. diff --git a/documentation/docs/pages/developers/rust/tcpproxy/examples/singleconn.mdx b/documentation/docs/pages/developers/rust/tcpproxy/examples/singleconn.mdx index f5f1ce8c7f..a0c1a73cfe 100644 --- a/documentation/docs/pages/developers/rust/tcpproxy/examples/singleconn.mdx +++ b/documentation/docs/pages/developers/rust/tcpproxy/examples/singleconn.mdx @@ -1,5 +1,13 @@ # Single Connection Example +import { Callout } from 'nextra/components' + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + This is a basic example which opens a single TCP connection and writes a bunch of messages between a client and some 'echo server' logic, so only uses a single session under the hood and doesn't really show off the message ordering capabilities; this is mainly just a quick introductory illustration on how: - the mixnet does message ordering - the NymProxyClient and NymProxyServer can be hooked into and used to communicate between two otherwise pretty vanilla TcpStreams diff --git a/documentation/docs/pages/developers/rust/tcpproxy/troubleshooting.md b/documentation/docs/pages/developers/rust/tcpproxy/troubleshooting.md deleted file mode 100644 index 8a3d6d5c23..0000000000 --- a/documentation/docs/pages/developers/rust/tcpproxy/troubleshooting.md +++ /dev/null @@ -1,4 +0,0 @@ -# Troubleshooting - -## Lots of `duplicate fragment received` messages -You might see a lot of `WARN` level logs about duplicate fragments in your logs, depending on the log level you're using. This occurs when a packet is retransmitted somewhere in the Mixnet, but then the original makes it to the destination client as well. This is not something to do with your client logic, but instead the state of the Mixnet. diff --git a/documentation/docs/pages/developers/rust/tcpproxy/troubleshooting.mdx b/documentation/docs/pages/developers/rust/tcpproxy/troubleshooting.mdx new file mode 100644 index 0000000000..7634e94ac4 --- /dev/null +++ b/documentation/docs/pages/developers/rust/tcpproxy/troubleshooting.mdx @@ -0,0 +1,12 @@ +# Troubleshooting +import { Callout } from 'nextra/components' + + + There will be a breaking SDK upgrade in the coming months. This upgrade will make the SDK a lot easier to build with. + +This upgrade will affect the interface of the SDK dramatically, and will be coupled with a protocol change - stay tuned for information on early access to the new protocol testnet. + +It will also be coupled with the documentation of the SDK on [crates.io](https://crates.io/). + +## Lots of `duplicate fragment received` messages +You might see a lot of `WARN` level logs about duplicate fragments in your logs, depending on the log level you're using. This occurs when a packet is retransmitted somewhere in the Mixnet, but then the original makes it to the destination client as well. This is not something to do with your client logic, but instead the state of the Mixnet. diff --git a/documentation/docs/pages/developers/tools/echo-server.mdx b/documentation/docs/pages/developers/tools/echo-server.mdx index 334c14daf0..920a1f4bdd 100644 --- a/documentation/docs/pages/developers/tools/echo-server.mdx +++ b/documentation/docs/pages/developers/tools/echo-server.mdx @@ -10,11 +10,15 @@ This server was initially built for the `TcpProxy` tests, but can be useful for Run `cargo build --release` from `nym/tools/echo-server`. The binary will be in the main workspace `target/release` dir. ## Run -All the server requires is a port to connect the TCP listener to. If no `` is supplied then the server defaults to using mainnet. - ```sh -echo-server -# e.g. ../../target/release/echo-server 9000 ../../envs/canary.env +Usage: echo-server [OPTIONS] + +Options: + -g, --gateway Optional gateway to use + -c, --config-path Optional config path to specify + -e, --env Optional env file - defaults to Mainnet if None + -l, --listen-port Listen port [default: 8080] + -h, --help Print help ``` ## Logging diff --git a/documentation/docs/pages/developers/typescript.mdx b/documentation/docs/pages/developers/typescript.mdx index 913df2c189..93a95b4c6f 100644 --- a/documentation/docs/pages/developers/typescript.mdx +++ b/documentation/docs/pages/developers/typescript.mdx @@ -1,5 +1,16 @@ + # Introduction +import { Callout } from 'nextra/components'; + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + Welcome to the documentation for Nym's TypeScript SDK! -This comprehensive guide contains information about the various TypeScript SDK modules that facilitate interaction with different components of the Nym stack, including the Nym mixnet, the Nyx blockchain, and Coconut credentials. +This guide contains information about the various TypeScript SDK modules that facilitate interaction with different components of the Nym stack, including the Nym mixnet, the Nyx blockchain, and Coconut credentials. diff --git a/documentation/docs/pages/developers/typescript/FAQ.mdx b/documentation/docs/pages/developers/typescript/FAQ.mdx index 5998a1e0a8..f226a9466a 100644 --- a/documentation/docs/pages/developers/typescript/FAQ.mdx +++ b/documentation/docs/pages/developers/typescript/FAQ.mdx @@ -1,4 +1,13 @@ # TS SDK FAQ +import { Callout } from 'nextra/components' + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + ## Why and when does the mixnet client complain about insufficient topology? diff --git a/documentation/docs/pages/developers/typescript/bundling/bundling.mdx b/documentation/docs/pages/developers/typescript/bundling/bundling.mdx index 667eea07ad..0bb48dc0e9 100644 --- a/documentation/docs/pages/developers/typescript/bundling/bundling.mdx +++ b/documentation/docs/pages/developers/typescript/bundling/bundling.mdx @@ -1,5 +1,15 @@ # Troubleshooting bundling +import { Callout } from 'nextra/components'; + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + You might need some help bundling packages from the Nym Typescript SDK into your package. Here are some things that could go wrong: diff --git a/documentation/docs/pages/developers/typescript/bundling/esbuild.mdx b/documentation/docs/pages/developers/typescript/bundling/esbuild.mdx index c794cf7ce6..9f225a512c 100644 --- a/documentation/docs/pages/developers/typescript/bundling/esbuild.mdx +++ b/documentation/docs/pages/developers/typescript/bundling/esbuild.mdx @@ -2,6 +2,14 @@ import { Callout } from 'nextra/components'; # Troubleshooting bundling with ESbuild + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + If you've been following the steps outlined in the Examples section, your development environment should be configured as follows: #### Environment Setup @@ -14,7 +22,7 @@ npm create vite@latest During the environment setup, choose React and subsequently opt for Typescript if you want your application to function smoothly following this tutorial. Next, navigate to your application directory and run the following commands: ```bash cd < YOUR_APP > -npm i +npm i npm run dev ``` @@ -29,4 +37,3 @@ npm install @nymproject/< PACKAGE_NAME > By implementing the provided code for the various components in the step-by-step examples section, you should be able to set-up and run your application without encountering any bundling challenges! - diff --git a/documentation/docs/pages/developers/typescript/bundling/webpack.mdx b/documentation/docs/pages/developers/typescript/bundling/webpack.mdx index 12878675e9..80b688d44a 100644 --- a/documentation/docs/pages/developers/typescript/bundling/webpack.mdx +++ b/documentation/docs/pages/developers/typescript/bundling/webpack.mdx @@ -2,6 +2,15 @@ import { Callout } from 'nextra/components'; # Troubleshooting bundling with Webpack + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + ## Webpack > 5 ESM For any project using Webpack, you´ll need the following rule in your `webpack.config.js` above version 5: @@ -24,7 +33,7 @@ If you wish to use Webpack for your app with the code provided in the step-by-st npx create-react-app nymapp --template typescript cd nymapp ``` -You'll then need to install the needed dependencies, head to your app's `App.tsx` file and paste the code provided in the step-by-step section. +You'll then need to install the needed dependencies, head to your app's `App.tsx` file and paste the code provided in the step-by-step section. #### Contract client @@ -34,7 +43,7 @@ You'll then need to install the needed dependencies, head to your app's `App.tsx ##### Install contract-clients dependencies ```bash -npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate @cosmjs/proto-signing +npm install @nymproject/contract-clients @cosmjs/cosmwasm-stargate @cosmjs/proto-signing ``` Head to you app's `App.tsx` file and replace the code by the one provided in the step-by-step examples section. @@ -76,7 +85,7 @@ module.exports = function override(config) { } } ]) - return config; + return config; } EOF ``` @@ -90,4 +99,4 @@ EOF "test": "react-app-rewired test", "eject": "react-scripts eject" }, -``` \ No newline at end of file +``` diff --git a/documentation/docs/pages/developers/typescript/examples/cosmos-kit.mdx b/documentation/docs/pages/developers/typescript/examples/cosmos-kit.mdx index 1df3855ffa..5f220bbb0d 100644 --- a/documentation/docs/pages/developers/typescript/examples/cosmos-kit.mdx +++ b/documentation/docs/pages/developers/typescript/examples/cosmos-kit.mdx @@ -2,8 +2,12 @@ import { Callout } from 'nextra/components' # Cosmos Kit - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + The TypeScript SDK is currently not avaliable: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! The wonderful people of Cosmology have made some [fantastic components](https://cosmoskit.com/) that can be used with diff --git a/documentation/docs/pages/developers/typescript/examples/mix-fetch.mdx b/documentation/docs/pages/developers/typescript/examples/mix-fetch.mdx index 267f8d3788..0c49da6181 100644 --- a/documentation/docs/pages/developers/typescript/examples/mix-fetch.mdx +++ b/documentation/docs/pages/developers/typescript/examples/mix-fetch.mdx @@ -1,9 +1,14 @@ import { Callout } from 'nextra/components'; # `mixFetch` - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + An easy way to secure parts or all of your web app is to replace calls to [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) with `mixFetch`: MixFetch works the same as vanilla `fetch` as it's a proxied wrapper around the original function. diff --git a/documentation/docs/pages/developers/typescript/examples/mixnet.mdx b/documentation/docs/pages/developers/typescript/examples/mixnet.mdx index e5210a1780..db1ec6c59a 100644 --- a/documentation/docs/pages/developers/typescript/examples/mixnet.mdx +++ b/documentation/docs/pages/developers/typescript/examples/mixnet.mdx @@ -1,8 +1,12 @@ import { Callout } from 'nextra/components' # Mixnet Client - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! As you know by now, in order to send or receive messages over the mixnet, you'll need to use the [`SDK Client`](https://www.npmjs.com/package/@nymproject/sdk), which will allow you to create apps that can use the Nym mixnet and Coconut credentials. diff --git a/documentation/docs/pages/developers/typescript/examples/nym-smart-contracts.mdx b/documentation/docs/pages/developers/typescript/examples/nym-smart-contracts.mdx index d0e4ca7faf..0e982dd1e9 100644 --- a/documentation/docs/pages/developers/typescript/examples/nym-smart-contracts.mdx +++ b/documentation/docs/pages/developers/typescript/examples/nym-smart-contracts.mdx @@ -1,8 +1,12 @@ import { Callout } from 'nextra/components' # Nym Smart Contract Clients - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! As previously mentioned, to query or execute on any of the Nym contracts, you'll need to use one of the [`Contract Clients`](https://www.npmjs.com/package/@nymproject/contract-clients), which contains read-only query and signing clients for all of Nym's smart contracts. diff --git a/documentation/docs/pages/developers/typescript/installation.mdx b/documentation/docs/pages/developers/typescript/installation.mdx index 0596ef0828..cc8367d787 100644 --- a/documentation/docs/pages/developers/typescript/installation.mdx +++ b/documentation/docs/pages/developers/typescript/installation.mdx @@ -4,6 +4,14 @@ import { Callout } from 'nextra/components' The different modules in the Typescript SDK allow developers to start building browser-based applications quickly. Simply import the SDK module of your choice – depending on the component from the Nym architecture you want to use – into your code via NPM, as you would any other TypeScript library. + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + Other than the `Contract Clients`, SDK modules come in four different flavours (ESM, CJS and full-fat for ESM and CJS). This documentation focuses on examples using the `full-fat` versions. diff --git a/documentation/docs/pages/developers/typescript/overview.mdx b/documentation/docs/pages/developers/typescript/overview.mdx index 1a163b232a..82bf983f28 100644 --- a/documentation/docs/pages/developers/typescript/overview.mdx +++ b/documentation/docs/pages/developers/typescript/overview.mdx @@ -3,6 +3,16 @@ import { TableContainer, Table, TableBody, TableCell, TableRow, Paper } from '@m import { NPMLink } from '../../../components/npm'; ## SDK overview + + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + The Typescript SDK allows developers to start building browser-based Nym-based applications quickly, by simply importing the SDK modules into their code via NPM as they would any other Typescript library. Currently developers can use different packages from the Typescript SDK to run the following entirely in browser: diff --git a/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx b/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx index f1dfb89ea3..5852480206 100644 --- a/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx +++ b/documentation/docs/pages/developers/typescript/playground/cosmos-kit.mdx @@ -6,8 +6,12 @@ import FormattedCosmoskitExampleCode from '../../../../code-examples/sdk/typescr import { Callout } from 'nextra/components' - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! Below is an example that uses [CosmosKit](https://cosmoskit.com/) to connect and sign a fake transaction with your [Keplr wallet](https://www.keplr.app/) or diff --git a/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx b/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx index b1dccf5ae2..6f3612b1c5 100644 --- a/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx +++ b/documentation/docs/pages/developers/typescript/playground/mixfetch.mdx @@ -5,23 +5,16 @@ import Box from '@mui/material/Box'; import FormattedMixFetchExampleCode from '../../../../code-examples/sdk/typescript/mixfetch-example-code.mdx'; import { Callout } from 'nextra/components' - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! - - Right now Gateways are not required to run a Secure Websocket (WSS) listener, so only a subset of nodes running in Gateway mode have configured their nodes to do so. - For the moment you have to select a Gateway that has WSS enabled from [this list](https://harbourmaster.nymtech.net/v1/services?wss=true). - - You can also find WSS-enabled nodes by querying the `gateways/described` endpoint on the Nym API and filtering for `wss_port`, either via the [Swagger webpage](https://validator.nymtech.net/api/swagger/index.html) or with `curl`: - - ``` -curl -X 'GET' \ - 'https://validator.nymtech.net/api/v1/gateways/described' \ - -H 'accept: application/json' - ``` - diff --git a/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx b/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx index 4937051fb2..b5114ef3ee 100644 --- a/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx +++ b/documentation/docs/pages/developers/typescript/playground/mixnodes.mdx @@ -5,8 +5,14 @@ import Box from '@mui/material/Box'; import FormattedExampleCode from '../../../../code-examples/sdk/typescript/mixnodes-example-code.mdx'; import { Callout } from 'nextra/components' - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! The Nym Mixnet contract keeps a directory of all mixnodes that can be used to mix traffic. diff --git a/documentation/docs/pages/developers/typescript/playground/traffic.mdx b/documentation/docs/pages/developers/typescript/playground/traffic.mdx index d1851b878b..6dcc16b335 100644 --- a/documentation/docs/pages/developers/typescript/playground/traffic.mdx +++ b/documentation/docs/pages/developers/typescript/playground/traffic.mdx @@ -4,10 +4,14 @@ import { Traffic } from '../../../../components/traffic'; import Box from '@mui/material/Box'; import FormattedTrafficExampleCode from '../../../../code-examples/sdk/typescript/traffic-example-code.mdx'; - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. - + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + Use this tool to experiment with the mixnet: send and receive messages! diff --git a/documentation/docs/pages/developers/typescript/playground/wallet.mdx b/documentation/docs/pages/developers/typescript/playground/wallet.mdx index 99f39816cc..20b97911b6 100644 --- a/documentation/docs/pages/developers/typescript/playground/wallet.mdx +++ b/documentation/docs/pages/developers/typescript/playground/wallet.mdx @@ -13,8 +13,12 @@ import FormattedWalletDelegationsCode from '../../../../code-examples/sdk/typesc import { Callout } from 'nextra/components' - -The Typescript SDK is currently undergoing maintenance: a network upgrade elsewhere has temporarily caused a problem. These docs are likely to change after we upgrade the SDK. + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! Here's a small wallet example using testnet for you to test out! diff --git a/documentation/docs/pages/developers/typescript/start.mdx b/documentation/docs/pages/developers/typescript/start.mdx index 0d3c5b4c0f..59c636eb21 100644 --- a/documentation/docs/pages/developers/typescript/start.mdx +++ b/documentation/docs/pages/developers/typescript/start.mdx @@ -3,6 +3,14 @@ import { Callout } from 'nextra/components' ## MixFetch + + The TypeScript SDK is currently not avaliable to use: a network upgrade elsewhere has caused a problem which is not currently fixed. TS SDK Clients are not able to connect to the network. + + When the issue is resolved, this will be reflected in the documentation. + + Thanks for your patience! + + Use the [`mixFetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) package as a drop-in replacement for `fetch`to send HTTP requests over the Nym mixnet: ```ts diff --git a/documentation/docs/pages/index.mdx b/documentation/docs/pages/index.mdx index 36243839d2..cac9844c41 100644 --- a/documentation/docs/pages/index.mdx +++ b/documentation/docs/pages/index.mdx @@ -1,5 +1,3 @@ import { LandingPage } from '../components/landing-page.tsx' - - diff --git a/documentation/docs/pages/operators/binaries/pre-built-binaries.mdx b/documentation/docs/pages/operators/binaries/pre-built-binaries.mdx index e8cea401e3..425eafcde5 100644 --- a/documentation/docs/pages/operators/binaries/pre-built-binaries.mdx +++ b/documentation/docs/pages/operators/binaries/pre-built-binaries.mdx @@ -2,6 +2,7 @@ import { Steps } from 'nextra/components'; import { VarInfo } from 'components/variable-info.tsx'; import { Tabs } from 'nextra/components'; import { MyTab } from 'components/generic-tabs.tsx'; +import {Callout} from 'nextra/components'; # Pre-built Binaries @@ -9,6 +10,10 @@ This page is for operators who prefer to download ready made binaries. The [Gith If the pre-built binaries don't work or are unavailable for your system, you will need to build the platform yourself. + +**[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions based on Debian bullseye/sid (11) like Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) or Ubuntu 22.04 (and newer)!** Alternatively [compile the binaries from source](building-nym.mdx). + + ## Setup Binaries @@ -35,12 +40,12 @@ In case you want to download binary to your current working directory, drop `One-liner, ]} defaultIndex="0"> -To see your binary `sha256sum` hash, run: +To see your binary `sha256sum` hash, run: ```sh sha256sum -``` +``` ```sh -# for example +# for example # sha256sum ./nym-wallet_1.2.15_amd64.AppImage # or # sha256sum ./nym-node @@ -49,7 +54,7 @@ sha256sum - Open it with your text editor or print its content with `cat hashes.json` - Check it if your binary `sha256sum` output is in `hashes.json` by using the `sha256sum` and searching for it or using `grep` command: ``` -grep -i +grep -i ``` diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 11aa6b8231..b0dcb2ba61 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -39,6 +39,8 @@ export const LoadEndpointInfo = () => ( ); +{/* + # Changelog This page displays a full list of all the changes during our release cycle from `v2024.3-eclipse` onward. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first. @@ -47,6 +49,567 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.15-gruyere` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.15-gruyere) +- [`nym-node`](nodes/nym-node.mdx) version `1.17.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-08-20T10:37:05.965300480Z +Build Version: 1.17.0 +Commit SHA: 40e1cbc7a9f518eafbb5649c383626b096dd167d +Commit Date: 2025-08-20T12:34:04.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- [**NIP-3: Nym Exit Policy Update**](https://forum.nym.com/t/nip-3-nym-exit-policy-update/1462/2) resulted by operators [governance](https://governator.nym.com/proposal/prop-d0c0d398-43bd-4a6f-b008-1921b64ae4ed) is implemented. + +- If you operate a node routing wireguard, please re-run [these steps](nodes/nym-node/configuration#wireguard-exit-policy-configuration) to update your wireguard exit policy through IP tables rules. + +- [**NIP-2: Changing stake saturation**](https://governator.nym.com/proposal/prop-8dfa4a28-55b1-45a5-968f-f2897c2670df) will be implemented soon, with that we are going to adjust rules of Delegation Program. **Join [Community call ](https://www.youtube.com/watch?v=KaO6-WLWzMo) on Thursday August 21st, 14:00UTC** to find out more info. + +### Features + +- [Remove old free credential handle](https://github.com/nymtech/nym/pull/5864): And create some traits for unit testing purposes. + +- [Ecash liveness check](https://github.com/nymtech/nym/pull/5890) + +- [Basic zulip client for sending messages](https://github.com/nymtech/nym/pull/5913): In order to be able to send zulip notifications about *emergency* upgrade mode being activated, we need some sort of client. Unfortunately there isn't any rust library that's maintained (the only one had last commit 4 years ago). This simple thing now currently only supports message sending + +- [`nym-node` debug command to reset providers db](https://github.com/nymtech/nym/pull/5914) + +- [Make DNS Resolver fallback optional](https://github.com/nymtech/nym/pull/5920): Default to no dns system fallback, but keep support in the custom hickory dns resolver used for resolving internal domains. + +- [WG exit policy scripts update](https://github.com/nymtech/nym/pull/5921): This PR modifies the scripts `wireguard-exit-policy-manager.sh` and `exit-policy-tests.sh` supporting operators to easily configure and test their IP tables rules in order to have same exit policy for WG as the one for NR, adding the ports decided to be enabled in [NIP-3](https://governator.nym.com/proposal/prop-d0c0d398-43bd-4a6f-b008-1921b64ae4ed) protocol upgrade. + +### Refactors & Maintenance + +- [Allow compatibility with 'CDLA-Permissive-2.0'](https://github.com/nymtech/nym/pull/5910): This license is present in the included `webpki-roots` + +- [Migrate strum to `0.27.2`](https://github.com/nymtech/nym/pull/5960): This PR migrates strum to the latest. Notably all macros' were moved into `strum_macros`. The rest stays the same. + +*/} + +## `v2025.14-feta` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.14-feta) +- [`nym-node`](nodes/nym-node.mdx) version `1.16.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-08-05T09:14:30.322593213Z +Build Version: 1.16.0 +Commit SHA: 7f97f13799342f864e1b106e8cafc9f6d6c24c0f +Commit Date: 2025-07-24T11:00:58.000000000+01:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- Stark Industries is on a sanction list by EU. IP addresses managed by Stark Ind. and their subsidies (ASN 44477 / ASN 33993) had been put on [spamhaus.org](http://spamhaus.org/) [list](https://www.spamhaus.org/drop/asndrop.json). The effect on NymVPN user experience is that Exit Gateways IPs hosted on Stark Ind. are seen as a spam proxies by many online services. + +- We ask operators - especially Exit Gateways - to consider moving to another ISP. Visit an updated [ISP list](community-counsel/isp-list) and feel free to add more providers, following [these steps](community-counsel/add-content). + +### Features + +- [Allow PG database backend](https://github.com/nymtech/nym/pull/5880): + - Added PostgreSQL database support alongside existing SQLite through Cargo feature flags + - Implemented runtime query conversion from SQLite `?` placeholders to PostgreSQL `$1`, `$2`, ... format + - Single codebase now supports both databases without query duplication + +- [Support mnemonic in the NS agent](https://github.com/nymtech/nym/pull/5883) + +- [`sqlx-pool-guard`: allocate more memory on windows](https://github.com/nymtech/nym/pull/5896): + - Allocate 1.5x more memory than reported by the system to provide a safety margin + - Increase number of retry attempts to 5 + + +- [dkg epoch dealers query](https://github.com/nymtech/nym/pull/5899) + +- [dkg snapshot epoch](https://github.com/nymtech/nym/pull/5900): In order to determine if signer quorum has been down at particular height, we need to know with certainty the dkg epoch id corresponding to given block height. This PR makes it possible. Every time epoch state is changed (due to DKG progress), snapshot is saved and can be queried. This doesn't work for past data, but given mainnet has only had a single DKG instance, that's not an issue. + +- [`sqlx-pool-guard`: obtain filename from connect options](https://github.com/nymtech/nym/pull/5905): + +### Refactors & Maintenance + +- [Nym node tokio console](https://github.com/nymtech/nym/pull/5909) + +## `v2025.13-emmental` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.13-emmental) +- [`nym-node`](nodes/nym-node.mdx) version `1.15.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-07-22T09:24:35.790560275Z +Build Version: 1.15.0 +Commit SHA: 578c9b0567656d86812aa21eb0b4c93b5a7235bd +Commit Date: 2025-07-22T11:09:35.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- [**NIP-3: Nym Exit Policy Update**](https://forum.nym.com/t/nip-3-nym-exit-policy-update/1462/2) is out - **VOTE [HERE](https://governator.nym.com/proposal/prop-d0c0d398-43bd-4a6f-b008-1921b64ae4ed)!** + +- `nym-node` can only run one `--mode` at a time. Multiple mode nodes will fail to start. + +### Features + +- [Initial performance contract](https://github.com/nymtech/nym/pull/5833): Introduces the foundational structure of the performance contract. Not yet integrated into any system. + +- [Basic performance contract integration within Nym API](https://github.com/nymtech/nym/pull/5871): Integrates the performance contract into `nym-api`, enabling node score source selection from the contract (disabled by default). + +- [Forbid running `mixnode` + `entry-gateway` on the same node](https://github.com/nymtech/nym/pull/5878): Prevents configuration of `mixnode` and `entry-gateway` node on the same host. + +- [HTTP Discovery objects & network defaults](https://github.com/nymtech/nym/pull/5814): Adds config options for expanded API URLs via discovery environment objects. + +- [Add build info endpoints](https://github.com/nymtech/nym/pull/5857) + +- [Batch SQL writes for packet stats](https://github.com/nymtech/nym/pull/5874) + +- [Update push-node-status-agent.yaml](https://github.com/nymtech/nym/pull/5882): Adds input for setting release tag and prefixes image tag with `golden-`. + +- [Make Mix hops optional for Mixnet Client SURBs](https://github.com/nymtech/nym/pull/5861): Allows SURBs to be configured to only use gateways in the mixnet return path. + +- [Check gateway supported versions](https://github.com/nymtech/nym/pull/5860) + +- [Clear out screaming logs](https://github.com/nymtech/nym/pull/5856): Removes unnecessary alarming log messages. + +- [Use display when printing paths](https://github.com/nymtech/nym/pull/5853): Uses Rust’s `display()` method for better path output. + +- [Remove old explorer references](https://github.com/nymtech/nym/pull/5846) + +- [Listen for shutdown signals during nym-node startup](https://github.com/nymtech/nym/pull/5879): This is to avoid situation where the process can't be killed without 'kill -9' because the logic to listen to shutdown signals hasn't been hit yet + +### Bugfixes + +- [Don't allow mixnode running in exit mode](https://github.com/nymtech/nym/pull/5898) + +- [Fix contract build process in Makefile](https://github.com/nymtech/nym/pull/5892) + +- [Ignore 'Send' responses when claiming bandwidth](https://github.com/nymtech/nym/pull/5884) + +- [Fix the broken link](https://github.com/nymtech/nym/pull/5873) + +- [Scraper bugfix: ignore precommits from missing validators](https://github.com/nymtech/nym/pull/5867) + +- [Fix removal of qa env](https://github.com/nymtech/nym/pull/5855) + +- [Return true remaining](https://github.com/nymtech/nym/pull/5866) + +### Refactors & Maintenance + +- [chore: 1.88 clippy](https://github.com/nymtech/nym/pull/5877): Applies clippy fixes based on recent compiler version update. + +- [Security patches for the `dkg` crate](https://github.com/nymtech/nym/pull/5828): Addresses overflows, unsafe RNGs, and integrity checks as per Cryspen audit. + + +## `v2025.12-dolcelatte` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.12-dolcelatte) +- [`nym-node`](nodes/nym-node.mdx) version `1.14.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-07-09T12:50:32.123212812Z +Build Version: 1.14.0 +Commit SHA: 089c47cce70ef8ea5ecfe3d00b52e510d006e120 +Commit Date: 2025-07-07T15:44:15.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- **[Nym Governator](https://governator.nym.com) is out!** Make sure to check out this new operators governance tool and vote on: + - [**NIP-1:** Proposing NIP as process to propose and log changes to the Nym network](https://governator.nym.com/proposal/prop-9719838a-e86d-4227-8141-dfd5c651ed92) + - [**NIP-2:** Change stake saturation and active set to improve network decentralisation and efficiency](https://governator.nym.com/proposal/prop-8dfa4a28-55b1-45a5-968f-f2897c2670df) + +### Features + +- [Nympool contract](https://github.com/nymtech/nym/pull/5464): This is the first iteration of the nympool contract. It's currently **not yet** integrated with the mixnet contract. + +- [Noise XKpsk3 integration (2025 version)](https://github.com/nymtech/nym/pull/5692) + +- [Key rotation](https://github.com/nymtech/nym/pull/5777) + +- [HTTP Client Retries, Fallbacks, and Redirects](https://github.com/nymtech/nym/pull/5789) + +- [Close sqlite pool before moving or reopening databases](https://github.com/nymtech/nym/pull/5796): Our sqlite db recovery and backup strategies don't work on Windows because the OS prevents moving the already open file. This PR attempts to address this and in principle it attempts to close all connections to the database and ensure that sqlx relinquishes the access to db file(s). Short summary of what this PR introduces: + + 1. Close sqlx connection pools in order to close/release the db file + 2. Poll until there are no more db file descriptors left open. + +- [Replace chrono with time in NS API](https://github.com/nymtech/nym/pull/5811) + +- [Use the same client bandwidth for top up](https://github.com/nymtech/nym/pull/5813) + +- [ Node status dvpn directory](https://github.com/nymtech/nym/pull/5829) + +- [Removing test-net faucet](https://github.com/nymtech/nym/pull/5840) + +- [Amended the buy section](https://github.com/nymtech/nym/pull/5841): Change the wallet to include the buy options for NYM + +- [Remove bity dir](https://github.com/nymtech/nym/pull/5844) + +- [Remove not used old `mock-api`](https://github.com/nymtech/nym/pull/5845) + +- [Remove qa env](https://github.com/nymtech/nym/pull/5847) + +- [Remove/old env references](https://github.com/nymtech/nym/pull/5848) + +- [Updated browser extension piece removal](https://github.com/nymtech/nym/pull/5849): Keep all the `internal-dev wasm` pieces as future examples + - everything previously was going to be removed + - shows functioning `wasm` interaction with the `js` + +### Bugfix + +- [Fixed typo in API endpoint parameter](https://github.com/nymtech/nym/pull/5449) + +- [Adjust heuristic for wireguard peer activity](https://github.com/nymtech/nym/pull/5818) + +- [Url scheme warning log](https://github.com/nymtech/nym/pull/5819): Fix conditions for logging about url scheme. Warns only if the provided urls are not HTTP or HTTPS, as intended. + +- [Bug Fix for Wallet build](https://github.com/nymtech/nym/pull/5820): Revert url used for `connection-tester` to default `url::Url`. + +- [Fix swapped total and circulating supplies](https://github.com/nymtech/nym/pull/5822) + +- [Fixed client route for obtaining v2 list of gateways](https://github.com/nymtech/nym/pull/5859) + +- [Allow gateways to permit authentication from v4 clients](https://github.com/nymtech/nym/pull/5862) + +- [Backwards compat](https://github.com/nymtech/nym/pull/5865) + +## `v2025.11-cheddar` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar) +- [`nym-node`](nodes/nym-node.mdx) version `1.13.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-06-10T09:41:31.291089877Z +Build Version: 1.13.0 +Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51 +Commit Date: 2025-06-10T11:39:20.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` +### Operators Updates & Tools + +- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance +- **NIP1** will outline the governance process: Its draft will be published soon for discussion +- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable +- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz) +- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!) + +### Features + +- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783) + +- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there + +- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787) + +- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility. + +- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800): + - Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API` + - Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code). + +- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802) + +- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804) + +- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812) + +- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831) + +## `v2025.10-brie` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie) +- [`nym-node`](nodes/nym-node.mdx) version `1.12.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-05-27T10:13:18.511075453Z +Build Version: 1.12.0 +Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0 +Commit Date: 2025-05-27T12:11:13.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- [**Reward calculator**](tokenomics/mixnet-rewards#rewards-logic--calculation): A quick app to calculate rewards for any node included in the Mixnet active set +- **Backup your nodes: Stark Industries Solutions and its subsidy PQ.Hosting were added to EU's sanction list**. Everyone running a node on PQ or Stark in Europe should get additional service free of charge and back up their node there. +- **We recommend operators using Stark Industries Solutions and PQ.Hosting to start looking for alternatives.** Here are some useful pages: + - [Backup a node](nodes/maintenance#backup-a-node) + - [Backup proxy conf](nodes/maintenance#backup-proxy-configuration) + - [Restore a node](nodes/maintenance#restoring-a-node) + - [Restore proxy conf](nodes/maintenance#restoring-proxy-configuration) + - [Move a node](nodes/maintenance#moving-a-node) + +- **Nym Squad League (NSL) now supports monthly payments.** Report your monthly contributions before the end of the week (May 30, Friday) to get your tokens next week. Alternatively, you can report everything all at once at the end Spring Season (Deadline: Wednesday, June 18). Read more about NSL at [forum.nym.com](https://forum.nym.com/). + +### Features + +- [`RememberMe` is the new don't `ForgetMe`](https://github.com/nymtech/nym/pull/5742): Sessions stats collected from gateway proved themselves to be incredibly noisy. In an effort to reduce the noise, `ForgetMe` was introduced at the end of sessions to prompt gateways to forget a specific session. This flag helped reduce the amount of noise, but alone it's not enough (e.g. `ForgetMe` can't be sent by non-sdk client). Therefore we're turning the whole thing and around and introducing a `RememberMe` flag, to prompt the gateways to actually remember said session. Read more in the [dev description of the PR](https://github.com/nymtech/nym/pull/5742). + +- [Remove old test directory - Update validator docker](https://github.com/nymtech/nym/pull/5743): The docker directory was extremely old - replaced it with a validator start up with docker compose + +- [`nym-api` `bincode` and `yaml` support](https://github.com/nymtech/nym/pull/5745): Support for `nym-api` to return responses as either `bincode` or `yaml` (on top of existing, default, `json`). Furthermore, initial client support for nodes-related queries is introduced. further endpoints can be enhanced by simply pushing `("output", "bincode")` params. Here is the sample response size difference when using `json` vs `bincode` (with and without `gzip` enabled on top of it): + +| Endpoint | `json` | `json` + `gzip` | `bincode` | `bincode` + `gzip` | +| --------------------------------------------------- | ------- | ----------- | ------- | -------------- | +| `/v1/nym-nodes/described` | 882,684 | 204,265 | 437,204 | 192,752 | +| `/v1/unstable/nym-nodes/skimmed/entry-gateways/all` | 116,268 | 27,991 | 39,755 | 25,589 | +| `/v1/unstable/nym-nodes/skimmed/mixnodes/active` | 44,646 | 11,371 | 14,021 | 10,235 | +| `/v1/ecash/aggregated-coin-indices-signatures` | 11,669 | 6,037 | 4,952 | 4,980 | +| `/v1/network/chain-status` | 1,523 | 828 | 1,004 | 585 | +| `/v1/nym-nodes/rewarded-set` | 1,221 | 616 | 679 | 529 | + +- [Decrease default average packet delay to 15 ms](https://github.com/nymtech/nym/pull/5754) + +- [Expires header for `/active` `nym-api` responses](https://github.com/nymtech/nym/pull/5755) + +- [Raw route submissions](https://github.com/nymtech/nym/pull/5756) + +- [Add `node_bonded` field to delegations](https://github.com/nymtech/nym/pull/5759): Clarifies whether the delegation is to a bonded or un-bonded node and include delegations to unbonded nodes in the returned list + +- [Instrument `create_request`](https://github.com/nymtech/nym/pull/5760): In the `vpn-api` client we create requests directly, so let's instrument them as well as the currently instrumented top-level function `get_json` doesn't capture that + +- [Use bincode by default in NymApiClient + remove feature-lock](https://github.com/nymtech/nym/pull/5761) + +- [Fetch the topology from the `nym-api` concurrently](https://github.com/nymtech/nym/pull/5767): When fetching the topology from the `nym-api`, multiple `GET` are done. We can do these concurrently as a simple way to speed things up. Some basic testing locally shows about 30% less time for the mixnet client to connect. + +- [Skip refreshing the topology on startup as we already have an initial set](https://github.com/nymtech/nym/pull/5768) + +- [Teach `HttpClientError` how to report its status code and timeout](https://github.com/nymtech/nym/pull/5770): Provide two delegated methods so that we don't have to use `reqwest` in the vpn client repo to access this information + +- [Expanded `Accept Encoding` for `reqwest`](https://github.com/nymtech/nym/pull/5779): The accept encoding that we are sending has a logic fallacy in that it is supposed to prefer `gzip`, and fall back to plaintext. What it actually requests is `gzip`, but if that isn't available, anything else is fine. This is an issue because some cloud cached API endpoints are optimistic about support for `br` and the `reqwest` client isn't actually configured to handle decompressing that so it returns the still encoded value to the calling application instead of the expected json. Enable support for more content encoding types through `reqwest`. This gives the server side more flexibility, which it seems to want, and we now only request the specific `content-encodings` that we will definitely be able to parse. Applications using the api client should no longer receive data still encoded as `reqwest` should manage decompressing it for us. + +### Bugfix + +- [Fix parallel feature in ecash crate with `Send + Sync`](https://github.com/nymtech/nym/pull/5744): In the current state, the `nym-compact-ecash` crate doesn't compile with the feature flag `par_verify` and `par_signing` because generic type `B` is not `Send + Sync` and so `par_iter` can't be called on it. + +- [Downgrade deranged crate to `0.4.0`](https://github.com/nymtech/nym/pull/5746): Downgrade the crate `deranged` from `0.4.1` to `0.4.0`, as `0.4.1` was yanked + and is flagged by `cargo audit` + +- [Upgrade prometheus crate to fix security warning](https://github.com/nymtech/nym/pull/5747): Upgrade the `prometheus` crate to bump the version of the `protobuf` + crate, which is flagged by `cargo audit` as having a security issue: + +```bash + Crate: protobuf + Version: 2.28.0 + Title: Crash due to uncontrolled recursion in protobuf crate + Date: 2024-12-12 + ID: RUSTSEC-2024-0437 + URL: https://rustsec.org/advisories/RUSTSEC-2024-0437 + Solution: Upgrade to >=3.7.2 + Dependency tree: + protobuf 2.28.0 + └── prometheus 0.13.4 +``` +- [Update `pretty_env_logger` to latest to not depend on unmaintained crate atty](https://github.com/nymtech/nym/pull/5748): The crate `atty` is flagged to be unmaintained and also having some security issues. + +- [Remove `pretty_env_logger` and switch remaining crates to use tracing](https://github.com/nymtech/nym/pull/5749) + +## `v2025.9-appenzeller` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.9-appenzeller) +- [`nym-node`](nodes/nym-node.mdx) version `1.11.0` + +```shell +nym-node +Binary Name: nym-node +Build Timestamp: 2025-05-13T14:48:01.050934949Z +Build Version: 1.11.0 +Commit SHA: 3f6acbfd6658cb8ada8db218ba037c3c2b744b9d +Commit Date: 2025-05-13T11:42:50.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- [Reward page updated](tokenomics/mixnet-rewards#rewards-logic--calculation) with [detailed explanation of rewards calculation](tokenomics/mixnet-rewards#rewards-calculation) +- [Nym Explorer v2 upgraded](https://nym.com/explorer) with new search, interactive map and advanced filters + +### Features + +- [Add contains ticketbook data db query](https://github.com/nymtech/nym/pull/5670) +- [Add `/account/{address}`](https://github.com/nymtech/nym/pull/5673): Meant to replace `https://explorer.nymtech.net/api/v1/tmp/unstable/account/{address}` for Explorer v2 + +### Bugfix + +- [Use node saturation instead of its stake for selection weight](https://github.com/nymtech/nym/pull/5717) + +## `v2025.8-tourist` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.8-tourist) +- [`nym-node`](nodes/nym-node.mdx) version `1.10.0` + +```shell +nym-node +Binary Name: nym-node +Build Timestamp: 2025-04-29T11:36:50.614557168Z +Build Version: 1.10.0 +Commit SHA: e594630314d4676cbe9bba9ab07bd405a9cf679a +Commit Date: 2025-04-29T12:19:54.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` +### Operators Updates & Tools + +- [New Nym wallet](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.2.18) is out: Release version `1.2.18` + +**Documentation Updates** + +- [Tokenomics page](tokenomics.mdx) updated +- [Node operators rewards page](tokenomics/mixnet-rewards.mdx) updated with new parts added: + - [Node performance calculation](tokenomics/mixnet-rewards#node-performance-calculation) + - [Stake saturation calculation](tokenomics/mixnet-rewards#stake-saturation) + - [Rewards calculation](tokenomics/mixnet-rewards#rewards-calculation) + - [Rewards distribution](tokenomics/mixnet-rewards#rewards-distribution) + +### Features + +- [Adding fresh nym-api tests and workflow](https://github.com/nymtech/nym/pull/5659) + +- [Replay protection](https://github.com/nymtech/nym/pull/5682): Introduces replay detection into a `nym-node`. Currently it uses a bloomfilter that's reset every 25h. In the future this will be controlled by the key rotation. The bloomfilter is also periodically flushed to disk in order to be able to recover from a crash/shutdown. + +- [Tauri V2 - Wallet Migration](https://github.com/nymtech/nym/pull/5687): + - The core of the lifting was done via the migrate command + - A lot of API's changed + - Improved styling + - Pipelines are fixed for all platforms + +- [Make mix hops optional for Mixnet Client](https://github.com/nymtech/nym/pull/5696): As is the route selection for packets, reply SURBs, and acknowledgements are selected deep withing the `MixnetClient` machinery. This makes custom route selection (i.e. for authenticating / registering wiregaurd mode vpn clients) is not supported. This PR adds configuration toggle that allows `MixnetClient` to be built where packets are sent direct through entry to Exit Gateway nodes -- no mix hops. + +- [Bump the `nym-vpn deb` metapackage to `1.0`](https://github.com/nymtech/nym/pull/5697) + +- [Updated sphinx payload keys](https://github.com/nymtech/nym/pull/5698): This PR uses the new version of the `sphinx-packet` that allows us to derive payload encryption keys from the seed rather than having to attach the keys themselves. In practice each reply surb is now ~60% smaller. However, currently all of those functionalities are **DISABLED** by default. It is because it required nodes to actually understand the new scheme. it shouldn't be enabled until sufficient number of nodes, particularly mixnodes, had upgraded. + +- [Allow copy and paste on logins fields for the wallet](https://github.com/nymtech/nym/pull/5699): Allow shell open for url links (some platforms it's not working as expected) + +- [Removed old explorer-api](https://github.com/nymtech/nym/pull/5701) + +- [Peer handle should die more gracefully](https://github.com/nymtech/nym/pull/5704) + +- [Update `hickory` DNS `0.24.4` to `0.25`](https://github.com/nymtech/nym/pull/5709): Update the dependency on `hickory` DNS to the latest minor version + +- [Remove inactive peers](https://github.com/nymtech/nym/pull/5721) + +- [Add reserved byte to reply surb serialisation](https://github.com/nymtech/nym/pull/5731) + + +## `v2025.7-tex` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.7-tex) +- [`nym-node`](nodes/nym-node.mdx) version `1.9.0` + +```shell +nym-node +Binary Name: nym-node +Build Timestamp: 2025-04-15T14:36:52.729991996Z +Build Version: 1.9.0 +Commit SHA: 08b6be93c49e8c225e74ffabb5529493bd4b13b6 +Commit Date: 2025-04-15T15:29:46.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.86.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + +- **[Nym release binaries](https://github.com/nymtech/nym/releases) no longer work on distributions Debian bullseye/sid (11) / Ubuntu 20.04 LTS and older! Please upgrade your sever to Debian bookworm (Debian 12) / Ubuntu 22.04 (and newer)!** Alternatively compile the binaries from source. + +- The Nym Squad League Winter Season has concluded. Changes are coming to NSL, including the new NSL council, monthly payments for contributions, revamped reports, and more. Details available in the [Winter Season report](https://forum.nym.com/t/nym-squad-league-winter-season-report/1265). + +- [New scripts to inititialise and configure multiple VMs](nodes/preliminary-steps/vps-setup/advanced#setting-up-virtual-machines): KVM setup for virtualising nodes proved to be a good approach for admins orchestrating multiple nodes on a dedicated or bare metal machines. Now there is an option of running bash scripts speeding up the process of VM initialisation and configuration, while leaving some flexibility via interactive prompts. + +### Features + +- [Move all workflows on ubuntu-20 to ubuntu-22](https://github.com/nymtech/nym/pull/5455): Ubuntu 20.04 runners are scheduled to be removed from set of github hosted runners. Now is as good a time as any to finally take the plunge to switch away from ubuntu-20.04 to the shiny oh so modern ubuntu-22.04 runners + +- [Bump `elliptic` from `6.5.5` to `6.6.1`](https://github.com/nymtech/nym/pull/5483): Bumps [`elliptic`](https://github.com/indutny/elliptic) + +- [Bump `blake3` from `1.6.1` to `1.7.0`](https://github.com/nymtech/nym/pull/5658): Bumps [`blake3`](https://github.com/BLAKE3-team/BLAKE3) + +- [Mix throughput tester](https://github.com/nymtech/nym/pull/5661): A utility to measure mixing throughput of a node. Especially since there are some design choices with non-obvious performance penalties. For example 1 bloomfilter per connection vs 1 global bloomfilter with additional locking for replay protection. + +- [Update log crate](https://github.com/nymtech/nym/pull/5667): Update `log crate` to latest, fix `format_in_format_args`, fix `to_string_in_format_args` + +- [Bump the patch-updates group across 1 directory with 7 updates](https://github.com/nymtech/nym/pull/5668): + +| Package | From | To | +| --- | --- | --- | +| [`clap`](https://github.com/clap-rs/clap) | `4.5.32` | `4.5.34` | +| [`clap_complete`](https://github.com/clap-rs/clap) | `4.5.46` | `4.5.47` | +| [`once_cell`](https://github.com/matklad/once_cell) | `1.21.1` | `1.21.3` | +| [`reqwest`](https://github.com/seanmonstar/reqwest) | `0.12.4` | `0.12.15` | +| [`tempfile`](https://github.com/Stebalien/tempfile) | `3.19.0` | `3.19.1` | +| [`time`](https://github.com/time-rs/time) | `0.3.39` | `0.3.41` | +| [`uniffi`](https://github.com/mozilla/uniffi-rs) | `0.29.0` | `0.29.1` | + +- [Improve explorer caching](https://github.com/nymtech/nym/pull/5669): Fix: + - `tanstack` caching + - graphs data bug + - "auto" gas fee for redeem rewards + +- [Update node versions in CI](https://github.com/nymtech/nym/pull/5677) + +- [Bash scripts to init and configure VMs conveniently and update docs](https://github.com/nymtech/nym/pull/5681): KVM setup for virtualising nodes proved to be a good approach for admins orchestrating multiple nodes on a dedicated/bare machines. This PR adds an option of running bash scripts speeding up the process of VM initialisation and configuration, while leaving some flexibility to the users via interactive prompts. + - Script to initialise new VM on host machine and prompting user for needed vars + - Script to config the VM from within and prompting user for needed vars + - Documentation of the above + - Lift headers so the main chapters come out in the side menu for easier navigation + +- [`clippy` for `1.86`](https://github.com/nymtech/nym/pull/5685) + +- [Expand `/v3/nym-nodes` with geodata](https://github.com/nymtech/nym/pull/5686): Includes node description and geodata. + +### Bugfix + +- [Minor fixes involving key cloning and hashing](https://github.com/nymtech/nym/pull/5664): Adds fixes for several minor key related issues: + - Copying a private keys to perform a DH + - Perform DH against the reference to the static key to prevent memory copy of secret key material + - Hashing an `x25519` public key without first converting to a canonical representation + - Use derived `Hash` implementation which internally [converts to a canonical representation](https://github.com/dalek-cryptography/curve25519-dalek/blob/fbf1fb5339b22eaf9925e520c90f1821f79ef5db/curve25519-dalek/src/montgomery.rs#L103) before hashing + +- [Fix the mac build of the wallet](https://github.com/nymtech/nym/pull/5684) + ## `v2025.6-chuckles` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.8-chuckles) @@ -71,11 +634,11 @@ cargo Profile: release - [**Nym Explorer v2.1.0**](https://nym.com/explorer) is out as a beta release -- Since last release [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration). Make sure to apply it on the servers hosting `nym-node` in `exit-gateway` mode. +- Since last release [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration). Make sure to apply it on the servers hosting `nym-node` in `exit-gateway` mode. #### Community Tools -Nym operators are coming with diverse backgrounds. While for some running a `nym-node` is an introduction to sys-administration, others are well seasoned builders and hackers. It's amazing to see the effort people put in order to lift each other by day-to-day mutual problem solving support in [Matrix channel](https://matrix.to/#/#operators:nymtech.chat). More and more people take it further and build tools to improve network monitoring and node administration. +Nym operators are coming with diverse backgrounds. While for some running a `nym-node` is an introduction to sys-administration, others are well seasoned builders and hackers. It's amazing to see the effort people put in order to lift each other by day-to-day mutual problem solving support in [Matrix channel](https://matrix.to/#/#operators:nymtech.chat). More and more people take it further and build tools to improve network monitoring and node administration. While we are planning to make a page aggregating these tools together, here are some tools released lately by the technical community: @@ -99,7 +662,7 @@ While we are planning to make a page aggregating these tools together, here are - [Bump `nanoid` from `3.3.7` to `3.3.8` in `/documentation/docs`](https://github.com/nymtech/nym/pull/5335): Bumps [`nanoid`](https://github.com/ai/nanoid) -- [Bump `store2` from `2.14.3` to `2.14.4`](https://github.com/nymtech/nym/pull/5391): Bumps [`store2`](https://github.com/nbubna/store) +- [Bump `store2` from `2.14.3` to `2.14.4`](https://github.com/nymtech/nym/pull/5391): Bumps [`store2`](https://github.com/nbubna/store) - [Bump `@octokit/plugin-paginate-rest` and `@actions/github` in `/.github/actions/nym-hash-releases/src`](https://github.com/nymtech/nym/pull/5488): Bumps [`@octokit/plugin-paginate-rest`](https://github.com/octokit/plugin-paginate-rest.js) @@ -124,16 +687,16 @@ While we are planning to make a page aggregating these tools together, here are - `/issued-ticketbooks-on-count/:issuance_date` - `/issued-ticketbooks-challenge-commitment` - `/issued-ticketbooks-data` - + - removed: - `/issued-ticketbooks-challenge` }> Testing Steps Performed: -New issue ticketbook related endpoints work - tickets are issued and consumed as before. +New issue ticketbook related endpoints work - tickets are issued and consumed as before. Redeeming tickets is happening as normal on the gateway side. - + Notes (if any): Chuckles will not require the rewarder binary to be updated; as there will be additional changes to rewarding, this will be extensively tested in the future. @@ -168,15 +731,15 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Bump `tempfile` from `3.18.0` to `3.19.0`](https://github.com/nymtech/nym/pull/5631): Bumps [`tempfile`](https://github.com/Stebalien/tempfile) - [Rework IPR codec to extract out timer and implement AsyncWrite](https://github.com/nymtech/nym/pull/5632): The goal is to be able to handle back pressure in the vpn client. For that we need to extract out the timer from the codec since we want to move control over that to the mixnet processor. Once the timer is removed we can reformulate the mixnet sender as a `Sink` wrapped in a `FramedWrite` - + - Extract out timer from IPR codec - Implement AsyncWrite on mixnet client sender - Add functions to wait for lanes to clear on `LaneQueueLenghts` - + - [Remove `explorer-api` from the main workspace](https://github.com/nymtech/nym/pull/5635) - [Bump `dtolnay/rust-toolchain` from `1.90.0` to `1.100.0`](https://github.com/nymtech/nym/pull/5638): Bumps [`dtolnay/rust-toolchain`](https://github.com/dtolnay/rust-toolchain) - + - [Bump `zip` from `2.2.2` to `2.4.1`](https://github.com/nymtech/nym/pull/5639): Bumps [`zip`](https://github.com/zip-rs/zip2) - [Add `max_retransmissions` flag on each message](https://github.com/nymtech/nym/pull/5642): Add an optional `max_retransmissions` field on `InputMessage`, to be able to selectively disable or turn down the number of `retransmissions` done for specific packets @@ -192,7 +755,7 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Update wallet to include Interval Operator Cost and Profit Margin](https://github.com/nymtech/nym/pull/5652): Created a new modal for Interval Operator Cost and Profit Margin changes - Uses existing update node operating cost functionality for old mixnodes (If it works it works) - Prompts a warning that a user "Can send the request as many times as they want but it will only update the last message when the interval changes" - + - [Wallet-revamp to be in line with new nym-theming](https://github.com/nymtech/nym/pull/5653): Updating colour palette to match the [nym.com](https://nym.com) sites: - Used the same font too - Updated icons @@ -201,7 +764,7 @@ Chuckles will not require the rewarder binary to be updated; as there will be ad - [Revert using `AsyncWrite` sink in IPR](https://github.com/nymtech/nym/pull/5656): Revert the use of the `AsyncWrite` sink in the IPR until it's fully ready. -- [Remove Google public DNS](https://github.com/nymtech/nym/pull/5660): We had a lot of complaints about Google, so we're removing this in VPN client, Nym should follow to avoid issues with firewall. +- [Remove Google public DNS](https://github.com/nymtech/nym/pull/5660): We had a lot of complaints about Google, so we're removing this in VPN client, Nym should follow to avoid issues with firewall. ### Bugfix @@ -232,14 +795,14 @@ cargo Profile: release 2. Security Fixes in Dependencies: Address known security vulnerabilities, making updating highly recommended 3. Authorisation & Timestamp Changes: Modifies how timestamps are validated in Auth v2 - this is going to be what the clients use. For example: the clients will start to use v2 variants of everything (prepping the network for the breaking change to come on April 1st which will force clients to only use v2). - + 4. Wireguard exit policy [scripts](https://github.com/nymtech/nym/tree/develop/scripts/wireguard-exit-policy) ### Operators Updates & Tools - [**Wireguard exit policy is out!**](nodes/nym-node/configuration#wireguard-exit-policy-configuration) Operators can use a new guide with our scripts to setup wireguard exit policy using IP tables rules. -- [Setup commands for `nym-node` simplified](nodes/nym-node/setup#setup--run): `config.toml` defaults to correct binding addresses and ports, no need to specify unless operators want to change those values. +- [Setup commands for `nym-node` simplified](nodes/nym-node/setup#setup--run): `config.toml` defaults to correct binding addresses and ports, no need to specify unless operators want to change those values. - [Virtualising dedicated server with KVM now includes IPv6 setup](nodes/preliminary-steps/vps-setup/advanced#installing-kvm-on-a-server-with-ubuntu-2204): Thanks to [LunarDAO](https://lunardao.net) squad we caught a missing IPv6 configuration in KVM installation guide. Operators can now run a bare metal or dedicated server and virtualise it into VMs each with a fully functional node, having all specs fully under control. @@ -308,17 +871,17 @@ cargo Profile: release - Add `bond_info` & `self_described` to DB `nym_nodes` - Update mixnode & gateway bond status on data refresh - Add `active` column to DB `nym_nodes` - - Use only active & bonded nodes in scraping/testrun tasks + - Use only active & bonded nodes in scraping/testrun tasks - [Rust SDK SURB example: change hardcoded file to tempdir](https://github.com/nymtech/nym/pull/5576): Change hardcoded filepath for client to tempdir -- [Server Side internal DoT/DoH opt out](https://github.com/nymtech/nym/pull/5577): Allow server side (nodes and nym-api) to use nym-http-api-client without DoH/DoT resolver. +- [Server Side internal DoT/DoH opt out](https://github.com/nymtech/nym/pull/5577): Allow server side (nodes and nym-api) to use nym-http-api-client without DoH/DoT resolver. - [Delete double memo field in send modal](https://github.com/nymtech/nym/pull/5578) - [Bump ring from `0.17.3` to `0.17.13` in /nym-wallet](https://github.com/nymtech/nym/pull/5582): Bumps [ring](https://github.com/briansmith/ring) -- [Bump the patch-updates group with 8 updates](https://github.com/nymtech/nym/pull/5585): +- [Bump the patch-updates group with 8 updates](https://github.com/nymtech/nym/pull/5585): | Package | From | To | | --- | --- | --- | @@ -385,7 +948,7 @@ cargo Profile: release ## `v2025.4-dorina-patched` -Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. +Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina-patched) - [`nym-node`](nodes/nym-node.mdx) version `1.6.2` diff --git a/documentation/docs/pages/operators/community-counsel/legal.mdx b/documentation/docs/pages/operators/community-counsel/legal.mdx index c916a899d6..f2f0d114d9 100644 --- a/documentation/docs/pages/operators/community-counsel/legal.mdx +++ b/documentation/docs/pages/operators/community-counsel/legal.mdx @@ -24,25 +24,23 @@ Write a message to your provider telling them about your intention to run a `nym #### Join Operators Legal Forum -This [Matrix channel]((https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu)) is the best place to ask questions and share your experience with others. You can share screen prints of abuse reports and ask for support. +This [Matrix channel]((https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu)) is the best place to ask questions and share your experience with others. You can share screen prints of abuse reports and ask for support. #### Join Operators Legal Clinic Do you have any questions directed for lawyers? Come and chat with Nym COO Alexis Roussel, every Wednesday 14:30 UTC for 60min in our [Operator Legal Forum channel on Matrix](https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu). #### Use a friendly provider Nym operators community shares their experience with different ISPs on [this page](isp-list). At the same time, consider to move away from these provides: - + - Servinga / VPS2day (AS39378) - Frantech / Ponynet / BuyVM (AS53667) -- OVH SAS / OVHcloud (AS16276) - Online S.A.S. / Scaleway (AS12876) - Hetzner Online GmbH (AS24940) -- IONOS SE (AS8560) -- Psychz Networks (AS40676) - 1337 Services GmbH / RDP.sh (AS210558) +- Stark Industries Solutions Ltd. / PQ.Hosting / The.Hosting / UFO-AS (AS44477 / ASN 33993) #### Backup your nodes -Your only way to restore your node is when you have an access to `.nym` directory locally. Follow [node](../nodes/maintenance#backup-a-node) and [proxy configuration](../nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](../nodes/maintenance#restoring-a-node) on another machine later on, without losing your delegation. +Your only way to restore your node is when you have an access to `.nym` directory locally. Follow [node](../nodes/maintenance#backup-a-node) and [proxy configuration](../nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](../nodes/maintenance#restoring-a-node) on another machine later on, without losing your delegation. #### Use `nym-exit` prefix on your landing page URL We would like to ask operators to use [reverse proxy](../nodes/nym-node/configuration/proxy-configuration) with a [landing page](landing-pages). When assigning a domain please use a common convention with `nym-exit` in the beginning of the the page URL as this will create a reputation and reference. The entire address should have this new format: @@ -69,10 +67,10 @@ nym-exit.mysquad.org **The `NYM-EXIT` part in the beginning is what's important.** #### Chose the right TLD -When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report. +When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report. #### Respond to abuse reports -Make sure to read notifications from your account provider and if you receive an abuse report, respond to it in time. Here is a template explaining the essential legal background of Nym Node Exit Gateway. Don't forget to adjust the variables. +Make sure to read notifications from your account provider and if you receive an abuse report, respond to it in time. Here is a template explaining the essential legal background of Nym Node Exit Gateway. Don't forget to adjust the variables.
@@ -81,4 +79,3 @@ Make sure to read notifications from your account provider and if you receive an #### Help us to improve these pages Add your findings by opening a [Pull Request](add-content). - diff --git a/documentation/docs/pages/operators/nodes.mdx b/documentation/docs/pages/operators/nodes.mdx index 3868d6cd22..9de49f6b00 100644 --- a/documentation/docs/pages/operators/nodes.mdx +++ b/documentation/docs/pages/operators/nodes.mdx @@ -2,7 +2,7 @@ import { Callout } from 'nextra/components'; import { Steps } from 'nextra/components'; import { Tabs } from 'nextra/components'; import NymNodeSpecs from 'components/operators/snippets/nym-node-specs.mdx'; -import TermsConditions from 'components/operators/snippets/tc-info.mdx'; +import TermsConditions from 'components/operators/snippets/tc-info.mdx' # Nym Nodes Operator Guides diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index ab425f8e4e..81044db705 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -454,7 +454,7 @@ The exit policy is same for all NRs, the content is shaped by an offchain govern There is a caveat though. NR is only routing TCP streams and therefore any other type of routing is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through wireguard and don't act as open proxies, the operators have to set up these rules via IP tables rules. -**Follow these steps, using a [setup script]i(https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:** +**Follow these steps, using a [setup script](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:** diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index a282d9a8ac..451fcac578 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -20,12 +20,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-04-01T09:55:02.982234741Z -Build Version: 1.8.0 -Commit SHA: a429d6528e99b878a310b71bdbe6d31923c52d84 -Commit Date: 2025-04-01T11:41:15.000000000+02:00 +Build Timestamp: 2025-08-20T10:37:05.965300480Z +Build Version: 1.17.0 +Commit SHA: 40e1cbc7a9f518eafbb5649c383626b096dd167d +Commit Date: 2025-08-20T12:34:04.000000000+02:00 Commit Branch: HEAD -rustc Version: 1.85.1 +rustc Version: 1.86.0 rustc Channel: stable cargo Profile: release ``` diff --git a/documentation/docs/pages/operators/nodes/performance-and-testing/gateway-probe.mdx b/documentation/docs/pages/operators/nodes/performance-and-testing/gateway-probe.mdx index d5a3c6fe53..8b1b8f38f2 100644 --- a/documentation/docs/pages/operators/nodes/performance-and-testing/gateway-probe.mdx +++ b/documentation/docs/pages/operators/nodes/performance-and-testing/gateway-probe.mdx @@ -9,7 +9,7 @@ Nym Node operators running Gateway functionality are already familiar with the m ## Preparation -We recommend to have install all [the prerequisites](../binaries/building-nym.md#prerequisites) needed to build `nym-node` from source including latest [Rust Toolchain](https://www.rust-lang.org/tools/install). +We recommend to have installed all [the prerequisites](../binaries/building-nym.md#prerequisites) needed to build `nym-node` from source including latest [Rust Toolchain](https://www.rust-lang.org/tools/install), **and** make sure to have [Go](https://go.dev/doc/install) installed. Go is necessary as the probe uses the `rust2go` FFI library to use `netstack` when making requests. ## Installation @@ -34,27 +34,65 @@ cargo build --release -p nym-gateway-probe To list all commands and options run the binary with `--help` command: ```sh -./target/release/nym-gateway-probe --help +./target/release/nym-gateway-probe -h ``` - Output: ```sh -Usage: nym-gateway-probe [OPTIONS] +Usage: nym-gateway-probe [OPTIONS] --mnemonic Options: - -c, --config-env-file Path pointing to an env file describing the network - -g, --gateway - -n, --no-log - -h, --help Print help - -V, --version Print version - + -c, --config-env-file + Path pointing to an env file describing the network + -g, --entry-gateway + The specific gateway specified by ID + -n, --node + Identity of the node to test + --min-gateway-mixnet-performance + + --min-gateway-vpn-performance + + --only-wireguard + + -i, --ignore-egress-epoch-role + Disable logging during probe + --no-log + + -a, --amnezia-args + Arguments to be appended to the wireguard config enabling amnezia-wg configuration + --netstack-download-timeout-sec + [default: 180] + --netstack-v4-dns + [default: 1.1.1.1] + --netstack-v6-dns + [default: 2606:4700:4700::1111] + --netstack-num-ping + [default: 5] + --netstack-send-timeout-sec + [default: 3] + --netstack-recv-timeout-sec + [default: 3] + --netstack-ping-hosts-v4 + [default: nymtech.net] + --netstack-ping-ips-v4 + [default: 1.1.1.1] + --netstack-ping-hosts-v6 + [default: ipv6.google.com] + --netstack-ping-ips-v6 + [default: 2001:4860:4860::8888 2606:4700:4700::1111 2620:fe::fe] + --mnemonic + + -h, --help + Print help + -V, --version + Print version ``` -To run the client, simply add a flag `--gateway` with a targeted gateway identity key. +To run the client, simply add `-n` flag followed by the ID key of the node you wish to test, as well as the mnemonic of a funded Nyx account; this is required to test the ticketbook generation. ```sh -./target/release/nym-gateway-probe --gateway +./target/release/nym-gateway-probe -n --mnemonic ``` For any `nym-node --mode exit-gateway` the aim is to have this outcome: @@ -87,4 +125,4 @@ For any `nym-node --mode exit-gateway` the aim is to have this outcome: **If your Gateway is blacklisted, the probe will not work.** -If you don't provide a `--gateway` flag it will pick a random one to test. +If you don't provide a `-n` flag it will pick a random node to test. diff --git a/documentation/docs/pages/operators/nodes/validator-setup.mdx b/documentation/docs/pages/operators/nodes/validator-setup.mdx index 21612c377d..8b7c3a8ac3 100644 --- a/documentation/docs/pages/operators/nodes/validator-setup.mdx +++ b/documentation/docs/pages/operators/nodes/validator-setup.mdx @@ -9,12 +9,12 @@ import { AccordionTemplate } from 'components/accordion-template.tsx'; > Nym has two main codebases: > - the [Nym platform](https://github.com/nymtech/nym), written in Rust. This contains all of our code except for the validators. -> - the [Nym validators](https://github.com/nymtech/nyxd), written in Go & maintained as a no-modification fork of [wasmd](https://github.com/CosmWasm/wasmd) +> - the [Nym validators](https://github.com/nymtech/nyxd), written in Go & maintained as fork of [wasmd](https://github.com/CosmWasm/wasmd) The validator is a Go application which implements it's functionalities using [Cosmos SDK](https://v1.cosmos.network/sdk). The underlying state-replication engine is powered by [CometBFT](https://cometbft.com/), where the consensus mechanism is based on the [Tendermint Consensus Algorithm](https://arxiv.org/abs/1807.04938). Finally, a [CosmWasm](https://cosmwasm.com) smart contract module controls crucial mixnet functionalities like decentralised directory service, node bonding, and delegated mixnet staking. -We are currently running mainnet with a closed set of reputable validators. To ensure decentralisation of Nyx chain, we are working on a mechanism to onboard new validators to the network. To join the waitlist, please drop an email to `validators [at] nymtech.net` with details of your setup, experience and any other relevent information +At present, our mainnet operates with a select group of reputed validators. We are not accepting new validators at this time. Any updates or changes to this policy will be promptly announced. ## Building your validator @@ -52,7 +52,7 @@ pacman -S git gcc jq - First remove any existing old Go installation and extract the archive you just downloaded into /usr/local: ```sh -rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.10.linux-amd64.tar.gz +rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.11.linux-amd64.tar.gz ``` - Then add /usr/local/go/bin to the PATH environment variable @@ -69,14 +69,14 @@ go version - Should return something like: ```sh -go version go1.20.10 linux/amd64 +go version go1.23.11 linux/amd64 ``` ### Download a precompiled validator binary -You can find pre-compiled binaries for Ubuntu `22.04` and `20.04` [here](https://github.com/nymtech/nyxd/releases). +You can find pre-compiled binaries for Ubuntu `22.04` and `24.04` [here](https://github.com/nymtech/nyxd/releases). ### Manually compiling your validator binary @@ -109,8 +109,9 @@ Usage: nyxd [command] Available Commands: + comet CometBFT subcommands completion Generate the autocompletion script for the specified shell - config Create or query an application CLI configuration file + config Utilities for managing application configuration debug Tool for helping with debugging your application export Export state to JSON genesis Application's genesis-related subcommands @@ -119,20 +120,20 @@ Available Commands: keys Manage your application's keys prune Prune app history states by keeping the recent heights and deleting old heights query Querying subcommands - rollback rollback cosmos-sdk and tendermint state by one height - rosetta spin up a rosetta server + rollback rollback Cosmos SDK and CometBFT state by one height + snapshots Manage local snapshots start Run the full node status Query remote node for status - tendermint Tendermint subcommands testnet subcommands for starting or configuring local testnets tx Transactions subcommands version Print the application binary version information Flags: -h, --help help for nyxd - --home string directory for config and data + --home string directory for config and data (default "/Users/neo/.nyxd") --log_format string The logging format (json|plain) (default "plain") - --log_level string The logging level (trace|debug|info|warn|error|fatal|panic) (default "info") + --log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:,:') (default "info") + --log_no_color Disable colored logs --trace print out full stack trace on errors Use "nyxd [command] --help" for more information about a command. @@ -180,8 +181,9 @@ Usage: nyxd [command] Available Commands: + comet CometBFT subcommands completion Generate the autocompletion script for the specified shell - config Create or query an application CLI configuration file + config Utilities for managing application configuration debug Tool for helping with debugging your application export Export state to JSON genesis Application's genesis-related subcommands @@ -190,20 +192,20 @@ Available Commands: keys Manage your application's keys prune Prune app history states by keeping the recent heights and deleting old heights query Querying subcommands - rollback rollback cosmos-sdk and tendermint state by one height - rosetta spin up a rosetta server + rollback rollback Cosmos SDK and CometBFT state by one height + snapshots Manage local snapshots start Run the full node status Query remote node for status - tendermint Tendermint subcommands testnet subcommands for starting or configuring local testnets tx Transactions subcommands version Print the application binary version information Flags: -h, --help help for nyxd - --home string directory for config and data + --home string directory for config and data (default "/Users/neo/.nyxd") --log_format string The logging format (json|plain) (default "plain") - --log_level string The logging level (trace|debug|info|warn|error|fatal|panic) (default "info") + --log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:,:') (default "info") + --log_no_color Disable colored logs --trace print out full stack trace on errors Use "nyxd [command] --help" for more information about a command. @@ -246,7 +248,7 @@ You can use the following command to download them for the correct network: wget -O $HOME/.nyxd/config/genesis.json https://nymtech.net/genesis/genesis.json # Sandbox testnet -curl https://rpc.sandbox.nymtech.net/snapshots/genesis.json | jq '.result.genesis' > $HOME/.nyxd/config/genesis.json +curl https://rpc.sandbox.nymtech.net/genesis | jq '.result.genesis' > $HOME/.nyxd/config/genesis.json ``` ### `config.toml` configuration @@ -512,7 +514,7 @@ nyxd tx slashing unjail --from="KEYRING_NAME" --chain-id=nyx --gas=auto - --gas-adjustment=1.4 + --gas-adjustment=1.5 --gas-prices=0.025unyx ``` @@ -523,7 +525,7 @@ nyxd tx slashing unjail --from="KEYRING_NAME" --chain-id=sandbox --gas=auto - --gas-adjustment=1.4 + --gas-adjustment=1.5 --gas-prices=0.025unyx ``` diff --git a/documentation/docs/pages/operators/tokenomics.mdx b/documentation/docs/pages/operators/tokenomics.mdx index 64d9fe1e2a..8247730461 100644 --- a/documentation/docs/pages/operators/tokenomics.mdx +++ b/documentation/docs/pages/operators/tokenomics.mdx @@ -11,6 +11,9 @@ import TokenTable from 'components/outputs/api-scraping-outputs/nyx-outputs/toke import StakingTarget from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md'; import StakingScaleFactor from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md'; import StakeSaturation from 'components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md'; +import StakeSaturationSnippet from 'components/operators/snippets/stake-saturation.mdx'; +import StakingSupply from 'components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md'; +import EpochRewardBudget from 'components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md'; import { TimeNow } from 'components/time-now.tsx'; import { AccordionTemplate } from 'components/accordion-template.tsx'; @@ -51,9 +54,9 @@ Besides the Mixnet itself, Nym Network is secured by its own blockchain Nyx (IBC * **Incentives:** Distribute rewards to decentralised nodes based on mixing and routing (work). This dynamic ensures that the network is as robust as possible - the nodes are chosen every hour according to their performance. -* **Network take over defense:** Another decisive factor for a node to be chosen to the network active set is reputation. Reputation is a size of stake (delegation) where delegators earn proportional percentage of nodes rewards. Nodes without reputation are not chosen to take part in the network active set. +* **Network take over defense:** Another decisive factor for a node to be chosen to the network active set is reputation. Reputation is a size of stake (self bond + delegation stake) where delegators earn proportional percentage of nodes rewards. Nodes without reputation are not chosen to take part in the network active set. -* **Centralisation defense:** Any node can only have a certain stake (called stake saturation) to earn maximum rewards, increasing stake level per node leads to decreasing rewards for the operator and all delegators. This feature makes it more difficult for whales to over-stake their nodes or to attract more delegators (stakers) as they would become dis-advantaged. +* **Centralisation defense:** Any node can only have a certain stake called [stake saturation](#stake-saturation) (self bond + delegation stake) to earn maximum rewards, increasing node stake level beyond this point leads to decreasing rewards for the operator and all delegators. This feature makes it more difficult for whales to over-stake their nodes or to attract more delegators (stakers) as they would become dis-advantaged. To learn more about rewards calculation and distribution, read the next page [*Nym Mixnet Rewards*](tokenomics/mixnet-rewards.mdx). @@ -83,7 +86,7 @@ Below is a table with token supply distribution. -To get live data, visit [Nym token page](https://nym.com.net/about/token) or see how to [query API endpoints](#query-tokenomics-api). +To get live data, visit [SpectreDAO token dashboard](https://explorer.nym.spectredao.net/token) or see how to [query API endpoints](#query-tokenomics-api). ### Calculation & Explanation @@ -95,12 +98,13 @@ To get a full comprehension of [node operators rewards](tokenomics/mixnet-reward │ │ supply │ │ nym nodes │ │ │circulating│ scale │ staking │ in rewarded │ stake │ │ supply │ factor │ target │ set │saturation │ - │ ├────────────►│ ├──────────────►│ │ + │ ├────────────►│ ├──────────────►│ level │ └───────────┘ └───────────┘ └───────────┘ ``` #### Supply +
Circulating supply is NYM. @@ -115,26 +119,18 @@ A number of aimed NYM tokens to be staked in the network. The staking target a i > **staking_target = staking_supply_scale_factor \* circulating_supply**
-Staking supply scale factor is currently it's set to be . +Staking supply scale factor is currently at . + +The value of this variable can is changed from time to time to optimize the metrics of the network. With a current circulating supply of NYM and staking supply scale factor , the staking target is NYM. -The value of this variable can be changed to optimize the metrics of the network. With a current circulating supply of NYM and staking supply scale factor , the staking target is NYM. #### Stake saturation -Node reputation in a form of self bond or stakers delegation. Stake saturation is calculated as: + - -> **stake_saturation = staking_target / rewarded_set_size** -> -> **rewarded_set_size = active_set_size + standby_set_size** - +#### Rewarded Set - -With current circulating supply of NYM, staking target of NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), the stake saturation level is NYM per node. - - - -#### Active set +> To read more about rewards calculation, please see next page [*Nym Operators Rewards*](tokenomics/mixnet-rewards.mdx) or you can go directly into details about [Rewarded set selection logic](tokenomics/mixnet-rewards#rewarded-set-selection). Nym Network needs an optimised number of nodes to route and mix the packets. This healthy balance lies in between being too congested - which would detriment speed and user experience - on one side, and having too little traffic per node - which would could weaken anonymity - on the other. @@ -146,22 +142,19 @@ The way how we approach this challenge is different for Mixnet (5-hop) and dVPN dVPN mode, ]} defaultIndex="0"> -Nym Mixnet is using an active set of chosen nodes. Currently the [active set size](https://validator.nymtech.net/api/v1/epoch/reward_params) is 240 nodes, 120 with Gateway functionality: 50 entry (1st layer) and 70 exit (5th layer) and 120 as Mixnode (2nd, 3rd and 4th mixing layer). The active set is chosen in the beginning of each epoch (60min). The best performing and reputated (optimal stake saturation) nodes are chosen. Performance is much more ample as you can see in the formula below: +Nym Mixnet is using an active set of chosen nodes. Currently the [active set size](https://validator.nymtech.net/api/v1/epoch/reward_params) is 240 nodes, 120 with Gateway functionality: 50 entry (1st layer) and 70 exit (5th layer) and 120 as Mixnode (2nd, 3rd and 4th mixing layer). The active set is chosen in the beginning of each epoch (60min). - -> **active_set_selection_probability = ( node_performance ^ 20 ) * stake_saturation** - +The alorithm for selecting the nodes into the Rewarded set is in detail explained in the [Rewarded set selection logic part](tokenomics/mixnet-rewards#rewarded-set-selection). -In dVPN (2-hop) mode every node which meets the performance criteria, including wireguard and IPv6 routing tests, becomes eligible to take part in the network. Whether the node is working on not then depends on the end users choise of the location or exact nodes selection. +In dVPN (2-hop) mode every node which meets the [performance criteria](tokenomics/mixnet-rewards#node-performance-calculation), including wireguard and IPv6 routing tests, becomes eligible to take part in the network. Whether the node is working or not then depends on the NymVPN end users choise of the location or exact nodes selection. -In both cases, the selection algorithm also looks whether the node runs with [Terms & Conditions](nodes/nym-node/setup.mdx#terms--conditions) accepted **AND** if it's not a legacy binary version. In case either of these criterias are not met, the node will have be excluded from the rewarded set selection. +In both cases, the selection algorithm also looks whether the node runs with [Terms & Conditions](nodes/nym-node/setup.mdx#terms--conditions) accepted **AND** if it's not a legacy binary version. In case either of these criterias are not met, the node will have be excluded from the [rewarded set selection](tokenomics/mixnet-rewards#rewarded-set-selection). -To read more about rewards calculation, please see next page [*Nym Operators Rewards*](tokenomics/mixnet-rewards.md). ## Query Validator API @@ -170,20 +163,20 @@ We have available API endpoints which can be accessed via [Swagger UI page](http ```sh curl -X 'GET' \ 'https://validator.nymtech.net/api/v1/circulating-supply' \ - -H 'accept: application/json'sh + -H 'accept: application/json' curl -X 'GET' \ 'https://validator.nymtech.net/api/v1/circulating-supply/total-supply-value' \ --H 'accept: application/json'sh +-H 'accept: application/json' curl -X 'GET' \ 'https://validator.nymtech.net/api/v1/circulating-supply-value' \ --H 'accept: application/json'sh +-H 'accept: application/json' curl -X 'GET' \ 'https://validator.nymtech.net/api/v1/epoch/reward_params' \ --H 'accept: application/json'sh +-H 'accept: application/json' ``` > The unit of value is measured in `uNYM`. diff --git a/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx b/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx index fc33513f9c..c7d1007bd7 100644 --- a/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx +++ b/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx @@ -6,9 +6,16 @@ import { VarInfo } from 'components/variable-info.tsx'; import { MigrateTabs } from 'components/operators/nodes/node-migrate-command-tabs'; import NyxPercentStake from 'components/outputs/nyx-outputs/nyx-percent-stake.md'; import NyxTotalStake from 'components/outputs/nyx-outputs/nyx-total-stake.md'; +import EpochRewardBudget from 'components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md'; +import StakeSaturation from 'components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md'; +import StakeSaturationSnippet from 'components/operators/snippets/stake-saturation.mdx'; +import CirculatingSupply from 'components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md'; +import StakingTarget from 'components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md'; import { TimeNow } from 'components/time-now.tsx'; import { AccordionTemplate } from 'components/accordion-template.tsx'; import { Clt } from 'components/callout-custom/CalloutCustom.jsx'; +import React, { useState, useEffect } from 'react'; +import RewardsCalculator from 'components/operators/interactive/calculators/reward-calculator.jsx'; # Nym Operators Rewards @@ -19,9 +26,9 @@ import { Clt } from 'components/callout-custom/CalloutCustom.jsx'; * Nym tokenomics are based on the research paper [*Reward Sharing for Mixnets*](https://nymtech.net/nym-cryptoecon-paper.pdf) -* For a more comprehensive overview, live data and supply graphs, visit [*nymtech.net/about/token*](https://nymtech.net/about/token) +* For a more comprehensive overview, live data and supply graphs, visit [*explorer.nym.spectredao.net/token*](https://explorer.nym.spectredao.net/token) -We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation and its detailed documentation. **The current design is called [*Naive rewarding*](#naive-rewarding).** It is an intermediate step, allowing operators to migrate to `nym-node` in Mixnet smart contract and for the first time recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards. +We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation and its detailed documentation. **The current design is called [*Naive rewarding*](#naive-rewarding).** It is an intermediate step, allowing operators to migrate to `nym-node` in Mixnet smart contract and for the first time receive delegations and earn rewards for any `nym-node` [functionality](../nodes/nym-node/setup#functionality-mode), in opposite to the past system, where only Mixnodes were able to receive delegations and rewards. **Please read the [roadmap section below](#roadmap) to see the planned development.** @@ -42,33 +49,46 @@ To make it easier for the reader, we use a highlighting line on the left side, w
*/} + Nodes bonded with vesting tokens are [not allowed to join rewarded set](https://github.com/nymtech/nym/pull/5129) - read more on [Nym operators forum](https://forum.nymtech.net/t/vesting-accounts-are-no-longer-supported/827). -## Overview +## Rewards Logic & Overview -This is a quick summary, to understand the full picture, please see detailed [*Rewards Logic & Calculation*](#rewards-logic--calculation) chapter below. +This is a quick summary, to understand the logic behind fundamentals like [rewarded set selection](#rewarded-set-selection), [node performance](#node-performance-calculation), [stake saturation](#stake-saturation), or [rewards calculation](#rewards-calculation), please read the chapters below. -* The operators of `nym-node` get rewarded from Mixmining pool, which emits around 6000 NYM per hour. -* A [rewarded set](../tokenomics.mdx#active-set) of `nym-nodes` selected for Nym network routing and mixing can be is currently 240 nodes in total and it's selected for each new epoch (60 min). The number can be adjusted - look here for the current value: [validator.nymtech.net/api/v1/epoch/reward_params](https://validator.nymtech.net/api/v1/epoch/reward_params) -* `nym-nodes` can run in mode `entry-gateway`, `exit-gateway` and `mixnode`, which are positioned into layers -* NymVPN users can chose to route through Nym Network in two ways: - - Mixnet: 5 layers routing and mixing - full privacy - - Wireguard: 2 layers routing, skipping 3 mixing layers - fast mode -* **The current reward system is [*Naive rewarding*](#naive-rewarding) - an intermediate step - where each layer get's rewarded the same** -* In the final model, nodes will get rewarded based on their layer position and the work they do (collected user tickets), where and the reward distribution per layer will be according to a [decision made by the operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407) as follows: - - 5-hop: 16%-16%-16%-16%-36% - - 2-hop: 33%-67% -* Currently Gateways earn rewards only from taking a part in the rewarded set. The operators can sign up to a grant program as a substitution for 2-hop routing. -* To read more about the final design and future implementation, see [*Roadmap*](#roadmap) chapter for more details. +* **The current reward system is called [*Naive rewarding*](#naive-rewarding) - an intermediate step - where the operators of `nym-node` get rewarded from [Mixmining pool](https://validator.nymtech.net/api/v1/epoch/reward_params), which emits NYM per hour** +* **Only nodes selected to [rewarded set](../tokenomics.mdx#active-set) of Mixnet receive rewards** +* The [rewarded set](../tokenomics.mdx#active-set) of the Mixnet is currently **240 nodes in total and it's selected for each new epoch (60 min)**, from all the nodes registered (bonded) in the network +* Each node gets the same proportion of work factor because of the *naive* distribution of work +* In the [final model](#roadmap), nodes will get rewarded based on their layer position and the work they do (collected user tickets), where the work factor distribution per layer will be according to a [decision made by the operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407) as [listed below](#nym-network-rewarded-set-distribution) +* If a node is selected to the rewarded set, it will be rewarded in the end of the epoch, based on this reward calculation formula: -## Rewards Logic & Calculation + +> **node_epoch_rewards = [total_epoch_reward_budget](https://validator.nymtech.net/api/v1/epoch/reward_params) \* node_work_fraction \* [node_stake_saturation](#stake-saturation) \* [node_performance](#node-performance-calculation)** +> +> We know that:
+> **[total_epoch_reward_budget](https://validator.nymtech.net/api/v1/epoch/reward_params) = **
+> **node_work_fraction = 1 / active_set_size**
+> **[active_set_size](https://validator.nymtech.net/api/v1/epoch/reward_params) = 240** +> +> Therefore:
+> **node_epoch_rewards = \* 1 / 240 \* [node_stake_saturation](#stake-saturation) \* [node_performance](#node-performance-calculation)** +
-**Note that in the current intermediate model we use one active set to reward all nodes and they are asign same (naive) work factor of 1 / 240, whether they work as Mixnode or Gateway of any kind, in both 2-hop and 5-hop mode. In reality it means that all nodes are rewarded within 5-hop reward scheme only.** +In reality there is a an additional value called **α**, giving a premium to nodes with a higher self bond. And additionally an operator gets more rewards based on [*Operators cost*](#rewards-distribution) and [*Profit margin*](#rewards-distribution) size. **Read chapter [Rewards calculation](#rewards-calculation) to be able to navigate in all the details relevant for operators and delegators.** -**However NymVPN client can chose any `nym-node --mode entry-gateway` and `--mode exit-gateway` in the network to route through the mixnet and as well as any of those which passed [wireguard probing test](https://harbourmaster.nymtech.net) to route as dVPN nodes.** + +**In the current intermediate model we use one active set to reward all nodes and they are assigned same work factor of 1 / 240**, whether they work as Mixnode or Gateway of any kind, in both 2-hop and 5-hop mode (hence *naive rewarding*). + +**In reality it means that all nodes are rewarded within the [Mixnet (5-hop) reward set](#rewarded-set-selection) only.** + +**However NymVPN client can choose any `nym-node` with `--wireguard-enabled true` flag (which passed [wireguard probing test](https://harbourmaster.nymtech.net)) to route as dVPN Gateway, both entry and exit.** + + +{/* ### Nym Network rewarded set distribution @@ -95,13 +115,17 @@ This is a quick summary, to understand the full picture, please see detailed [*R ``` + + | **Network layer** | **1** | **2** | **3** | **4** | **5** | | :-- | :---: | :---: | :---: | :---: | :---: | | Node functionality in layer | Entry Gateway | Mixnode | Mixnode | Mixnode | Exit Gateway | -| Nodes in [active set](tokenomics.mdx#active-set) | 50 | 40 | 40 | 40 | 70 | -| Naive rewarding: Rewards distribution per node | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | -| Final model: Rewards distribution per node | 0.16 / 240 | 0.16 / 240 | 0.16 / 240 | 0.16 / 240 | 0.36 / 240 | +| Nodes in [active set](#rewarded-set-selection) | 50 | 40 | 40 | 40 | 70 | +| Naive rewarding \*: Maximum work fraction per node | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | +| Final model \*\*: Layer multiplier | 0.16 | 0.16 | 0.16 | 0.16 | 0.36 | +> \* Only nodes chosen to the [rewarded set](#rewarded-set-selection) will be rewarded in Mixnet mode (5-hop).
+> \*\* In the final model the nodes routing as Exit Gateway get premium rewards due to the complexity and legal challenges coming along operating this type of node. Read [roadmap section](#roadmap) in the bottom of the page to get a detailed breakdown of the final implementation. @@ -121,66 +145,187 @@ This is a quick summary, to understand the full picture, please see detailed [*R | **Network layer** | **1** | **2** | | :-- | :---: | :---: | | Node functionality in layer | Entry Gateway | Exit Gateway | -| Naive rewarding: Nodes in [active set](tokenomics.mdx#active-set) | 50 | 70 | -| Naive rewarding: Rewards distribution per node | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | 1 / 240 | -| Final model: Active nodes | All following criteria for eligibility | All following criteria for eligibility | -| Final model: Rewards distribution per node | 0.33 \* collected_user_tickets | 0.67 \* collected_user_tickets | +| Naive rewarding: Nodes in [active set](tokenomics.mdx#active-set) | only Mixnet mode | only Mixnet mode | +| Naive rewarding\*: Maximum work fraction per node | only Mixnet mode | only Mixnet mode | +| Final model\*\*: Layer multiplier | 0.33 | 0.67 | +> \* In the current state called [*Naive rewarding*](#naive-rewarding) only nodes in the Mixnet [rewarded set](#rewarded-set-selection) get rewarded.
+> \*\* In the final model the nodes routing as Exit Gateway get premium rewards due to the complexity and legal challenges coming along operating this type of node. Read [roadmap section](#roadmap) in the bottom of the page to get a detailed breakdown of the final implementation.
+*/} -### Rewarded Set Selection +## Rewarded Set Selection -For a node to be rewarded, the node must be part of a [Rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params) (which currently = active set) in the first place. The Rewarded set is freshly selected at the start of each epoch (every 60 min), and it consists of 240 Nym nodes that are probabilistically chosen from all the available nodes. These 240 nodes include 120 gateways and 120 mixnodes (40 for each of 3 mixnet layers). +For a node to be rewarded, the node must be part of a [Rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params) (which currently = active set) in the first place. The Rewarded set is freshly selected at the start of each epoch (every 60 min), and it consists of 240 Nym nodes that are probabilistically chosen from all the available nodes. These 240 nodes are composed of 120 gateways (50 entry and 70 exit) and 120 mixnodes (40 for each of 3 mixnet layers). -Rewarded set nodes are randomly selected, and their selection chances increase with a node score that includes three parameters: +Nodes selected into the rewarded set are chosen probabilisticaly, and their selection chances increase the larger nodes weight is. Weight value is always between `0` and `1` and it's calculated by multiplying these parameters, each of them also having a value between `0` and `1` (some are floats, some are binary): -1. [Config score](#config-score-calculation): highest (`1`) when the node is running the latest version of the software with [T&C's accepted](../nodes/nym-node/setup.mdx#terms--conditions) -2. [Performance](#performance-calculation): highest (`1`) when the node is consistently online and correctly processes all the received traffic -3. [Stake saturation](../tokenomics.mdx#stake-saturation): including bond and delegated stake +**1. [Performance](#node-performance-calculation):** This value consists of: +- [Config score](#config-score-calculation): highest (`1`) when the node is running the latest version of the software, has [T&C's accepted](../nodes/nym-node/setup.mdx#terms--conditions) and self described API endpoint available +- [Routing ](#routing-score-calculation): highest (`1`) when the node is consistently online and correctly processes all the received traffic (100% of time) -Besides these values, the API is also looking whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!** +**2. [Stake saturation](#stake-saturation):** combining bond and delegated stake (a float number between `0` and `1` representing percentage) -**The node score is calculated with this formula:** +**Node weight is calculated with this formula:** -> **active_set_selection_probability = total_stake \* (( config_score \* node_performance ) ^ 20 )** +> **active_set_selection_weight = stake_saturation \* ( node_performance ^ 20 )** -Note that the score helps prioritize some nodes over others. If all available nodes have the same score, then the selection is done uniformly at random. By raising the config and performance components to 20, values of these parameters that are below one incur a heavy penalization for the node’s selection chances. +For the rewarded set selection weight, good [performance](#node-performance-calculation) is much more essential than [stake saturation](#stake-saturation), because it's lifted to 20th power in the selection algorhitm. -Besides these values, the API is also checks whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set. Thus, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!** +For a comparison we made an example with 5 nodes, where first number is node performance and second stake saturation (assuming all of them [`config_score`](#config-score-calculation) = `1` for simplification): -#### Config Score Calculation +
+ +> node_1 = 1.00 ^ 20 \* 1.0 = **1**
+> node_2 = 1.00 ^ 20 \* 0.5 = **0.5**
+> node_3 = 0.99 ^ 20 \* 1.0 = **0.818**
+> node_4 = 0.95 ^ 20 \* 1.0 = **0.358**
+> node_5 = 0.90 ^ 20 \* 1.0 = **0.122**
+
-The nodes selection to the active set has a new parameter - `config_score`. Config score currently looks into three paramteres: +As you can see the performance is much more important during the Rewarded set selection. A node with 100% performance but only 50% stake saturation has much bigger chance to be chosen than a node with 95% performance and 100% stake saturation and incomparably bigger chance than 90% performing node with 100% stake saturation. + +The nodes are chosen probababilistically in each epoch (60 min), so even nodes with lower performance will eventually be chosen, just much less often, as their chances decrease. +Note that the score helps prioritize some nodes over others. If all available nodes have the same score, then the selection is done uniformly at random. By raising the node performance to 20, values of these parameters that are below one incur a heavy penalization for the node’s selection chances. + +
+ +**Explanation** + +The nodes are selected probabilistically, that means that even nodes with lower weight have a small chace to get slected. The probabilistic alorithm follows this logic: + +1. Summarize all nodes weight together +2. Make a random selection roll for the first slot in the active set +3. If a node is selected, take all its weight away from the draft queue +4. Repeat points 1. - 3. for each slot in the active set + +**Example** + +We know that nodes weight is a float between 0 and 1. For simplification we will use integers in this example and much smaller set. + +- Total nodes: 8 +- Rewarded set: 4 +- Nodes weight: + - node1 = 5 + - node2 = 5 + - node3 = 10 + - node4 = 10 + - node5 = 20 + - node6 = 40 + - node7 = 50 + - node8 = 60 + +1. Summarize all nodes weight together: +``` +weight_total = 5 + 5 + 10 + 10 + 20 + 40 + 50 + 60 +weight_total = 200 +``` +2. Roll a dice from 1 to 200: +- Imagine the nodes are in line each representing the weight like index: + - node1 = 1-5 + - node2 = 6-10 + - node3 = 11-20 + - node4 = 21-30 + - node5 = 31-50 + - node6 = 51-90 + - node7 = 91-150 + - node8 = 151-200 +- Say the function resulted in number 170 +3. Add node8 to the rewarded set and take it out of the lottery, summarize all the weights again: +``` +weight_total = 5 + 5 + 10 + 10 + 20 + 40 + 50 +weight_total = 140 +``` +4. Roll a dice from 1 to 140: +- Say the function resulted in number 4 +5. Add node1 to the rewarded set and take it out of the lottery, summarize all the weights again: +``` +weight_total = 5 + 10 + 10 + 20 + 40 + 50 +weight_total = 135 +``` +6. Roll a dice from 1 to 135: +- Say the function resulted in number 72 +7. Add node6 to the rewarded set and take it out of the lottery, summarize all the weights again: +``` +weight_total = 5 + 10 + 10 + 20 + 50 +weight_total = 95 +``` +8. Roll a dice from 1 to 95: +- Say the function resulted in number 21 +9. Add node4 to the rewarded set +10. Rewarded set of 4 nodes is selected with these nodes to be chosen: + 1. node8 + 2. node1 + 3. node6 + 4. node4 +11. After an epoch - 60 minutes - pull all bonded nodes and repeat the exact same process with their current weights + +In reality we have mixing nodes selected into 3 layers. To increase security, there is an additional function in place where a node cannot be assigned to the same layer in two following epochs. + + +Below we break down [performance calculation](#node-performance-calculation) and show examples. + + +## Node Performance Calculation + +Performance is a value between `0` and `1`. The final performance number is a result of multiplying [config score](#config-score-calculation) and [routing score](#routing-score-calculation). + + +> **node_performance = config_score \* routing_score** + + +Performance value is an average of last 24h. + + +All parameters regarding performance score can be browsed or pull live from: + +`https://validator.nymtech.net/api/v1/nym-nodes/annotation/` + +In case you don't know your nodes `NODE_ID`, it's easy to find as long as your node is bonded. Visit [validator.nymtech.net/api/v1/nym-nodes/bonded](https://validator.nymtech.net/api/v1/nym-nodes/bonded) and search your node using `identity_key` or bonding Nyx account address (denoted as `owner`). + + +### Config Score Calculation + +Config score is in place to ensure that the node configuration is done properly so the node is eligible for taking part in Nym network. The API looks into these paramteres: 1. If the node binary is `nym-node` (not legacy `nym-mixnode` or `nym-gateway`): `1` if `True`, `0` if `False` 2. If [Terms & Conditions](../nodes/nym-node/setup.mdx#terms--conditions) are accepted: `1` if `True`, `0` if `False` -3. Version of `nym-node` binary: decreasing weight for outdated versions, as explained below +3. If the nodes self described endpoint is available: `1` if `True`, `0` if `False` +4. Version of `nym-node` binary: decreasing weight for outdated versions, as [explained below](#versions-behind-calculation) -**The `config_score` parameter calculation formula:** +**The `config_score` calculation formula:** -> **config_score = is_tc_accepted \* is_nym-node_binary \* ( 0.995 ^ ( ( X * versions_behind) ^ 1.65 ) )** +> **config_score = is_tc_accepted \* is_nym-node_binary \* self_described_api_available \* ( 0.995 ^ ( ( X * versions_behind) ^ 1.65 ) )** -First two points have binary values of either 0 or 1, with a following logic: +First three points have binary values of either `0` or `1`, with a following logic: -| **Run `nym-node` binary** | **T&C's accepted** | **Value** | -| :-- | :-- | ---: | -| True | True | 1 | -| True | False | 0 | -| False | True | 0 | -| False | False | 0 | +| **Run `nym-node` binary** | **T&C's accepted** | **Self described available** | **Value** | +| :-- | :-- | :-- | ---: | +| **True** | **True** | **True** | **1** | +| True | False | False | 0 | +| True | True | False | 0 | +| False | True | True | 0 | +| False | False | True | 0 | +| True | False | True | 0 | +| False | False | False | 0 | +| False | True | False | 0 | -Only if both conditions above are `True` the node can have any chance to be selected, as otherwise the probability will always be 0. +**Only if ALL conditions above are `True` the node can have any chance to be selected, as otherwise the probability will always be 0.** -**The `versions_behind` parameter in `config_score` calculation** + +Besides these values, the API also checks whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set. Thus, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!** + -From release `2024.14-crunch` (`nym-node v1.2.0`), the `config_score` parameter takes into account also nodes version. The "current version" is the one marked as `Latest` in our repository. The parameter `versions_behind` indicates the number of versions between the `Latest` version and the version run by the node, and it is factored into the config score with the formula: +#### Versions Behind Calculation + +From release `2024.14-crunch` (`nym-node v1.2.0`), the `config_score` parameter takes into account also nodes version (denoted as `versions_behind`). The "current version" is the one marked as `Latest` in the [repository](https://github.com/nymtech/nym/releases/). The parameter `versions_behind` indicates the number of versions between the `Latest` version and the version run by the node, and it is factored into the config score with this formula: > **0.995 ^ ( ( X * versions_behind ) ^ 1.65 )** @@ -213,30 +358,135 @@ Note that the `X` multiplier heavily lowers the `config_score` when nodes are ou As you can see above, the algorithm is designed to give maximum selection score (`1`) to the latest version, while non-upgraded nodes receive a lower score. The score decreases faster when the node has failed to make a major version upgrade, and slower when the node is behind only with minor updates. This scoring de-prioritizes the selection of outdated nodes, even if their saturation and performance are high. Nodes are selected probabilistically in each epoch (60 min), according to their scores, to be part of the Rewarded set. This scoring mechanism gives priority to the operators running up-to-date nodes, ensuring that the network is as updated as possible. -#### Performance Calculation +### Routing Score Calculation -Performance is measured by Nym Network Monitor which sends thousands of packages through different routes every 15 minutes and measures how many were dropped on the way. Test result represents percentage of packets succesfully returned (can be anything between 0 and 1). Performance value is nodes average of these tests in last 24h. +Routing score is measured by Nym Network Monitor which sends thousands of packages through different routes every 15 minutes and measures how many were dropped on the way. Test result represents percentage of packets succesfully returned which are then converted into floats bettween `0` and `1`. -Good performance is much more essential than [total stake](../tokenomics.mdx#stake-saturation), because it's lifted to 20th power in the selection formula. +## Stake Saturation -For a comparison we made an example with 5 nodes, where first number is node performance and second stake saturation (assuming all of them `config_score` = 1 for simplification): +> If you want to understand more about NYM supply, read [tokenomics page](../tokenomics#tokenomics) first. + + + + +## Rewards Calculation + +Once the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params) (currently 120 Mixnodes and 120 Gateways) is selected, the nodes can start to route and mix packets in the Nym Network. Each hour a total of NYM is distributed between the layers from Mixmining pool. Currently in our *Naive rewarding* intermediate design, all layers get a same portion, therefore each node is *naively* assigned same working factor and therefore earns 1/240 of the rewards per epoch. + +If a node is active in the rewarded set, it will receive rewards in the end of the epoch, the size is dependant on [stake saturation](../tokenomics.mdx#stake-saturation) and [performance](#performance-calculation). This is how rewards get distributed between nodes in the rewarded set. + +**Node rewards calculation formula:** + + +> **node_epoch_rewards = [total_epoch_reward_budget](https://validator.nymtech.net/api/v1/epoch/reward_params) \* [node_performance](#node-performance-calculation) \* [node_stake_saturation](#stake-saturation) \* ( node_work_fraction + α \* ( ( node_bond_size / [stake_saturation_level](https://validator.nymtech.net/api/v1/epoch/reward_params) ) / rewarded_set_size ) ) \* 1 / ( 1 + α )** +> +> Where:
+> **[total_epoch_reward_budget](https://validator.nymtech.net/api/v1/epoch/reward_params) = **
+> **node_work_fraction = 1 / rewarded_set_size**
+> **[rewarded_set_size](https://validator.nymtech.net/api/v1/epoch/reward_params) = 240**
+> **[stake_saturation_level](https://validator.nymtech.net/api/v1/epoch/reward_params) = **
+> **α = 0.3** +> +> Therefore:
+> **node_epoch_rewards = \* [node_performance](#node-performance-calculation) \* [node_stake_saturation](#stake-saturation) \* ( ( 1 / 240 ) + 0.3 \* ( ( node_bond_size / ) / 240 ) ) \* 1 / ( 1 + 0.3 )** +
+ +Performance and stake saturation (both are a float between `0` and `1` representing percentage) play an equally decisive role in the size of rewards earned after the epoch. The closer a node is to maximum value (`1`) of each of these parameters, the more rewards it will get.
- -> node_1 = 1.00 ^ 20 \* 1.0 = 1
-> node_2 = 1.00 ^ 20 \* 0.5 = 0.5
-> node_3 = 0.99 ^ 20 \* 1.0 = 0.818
-> node_4 = 0.95 ^ 20 \* 1.0 = 0.358
-> node_5 = 0.90 ^ 20 \* 1.0 = 0.122
-
+ +Operators aim to get [stake saturation](#stake-saturation) on their nodes as close to maximum value `1` as possible. The value of node stake is composed of **delegations** (stake from others or self) and **bond** (tokens locked by the operator on top of the node when registering it to the network). -As you can see the performance (also known as *Routing score*) is much more important during the Rewarded set selection. A node with 100% performance but only 50% stake saturation has much bigger chance to be chosen than a node with 95% performance but full stake saturation and incomparably bigger chance than 90% performing node with 100% stake saturation. +Constant **α** is a in place to increase rewards for nodes with higher bond. Minimum value of bond is 100 NYM. The higher bond an operator locks, the more skin in the game they have and therefore they receive a small premium. -The nodes are chosen probababilistically in each epoch (60 min), so even nodes with lower performance will eventually be chosen, just much less often, as their chances decrease. +Let's compare 3 nodes to see their rewards, for simplicity all of them having maximum performance and stake saturation, being active in the same epoch with reward budget of 5_000 NYM, stake saturation level is 1_000_000_000 NYM and node work fraction 1 / 240. The only variable is then the size of their bond. The formula will look like this: -### Layer Distribution +``` +node_epoch_rewards = 5_000 * 1 * 1 * ( ( 1 / 240 ) + 0.3 * ( ( node_bond_size / 1_000_000 ) / 240 ) ) * 1 / ( 1 + 0.3 ) +``` -Once the rewarded set (currently 120 Mixnodes and 120 Gateways) is selected, the nodes can start to route and mix packets in the Nym Network. Each hour a total of 6000 NYM is distributed between the layers from Mixmining pool. Currently in our *Naive rewarding* intermediate design, all layers get a same portion, therefore each node is *naively* assigned same working factor and therefore earns 1/240 of the rewards per epoch. +Bond size of our 3 example nodes: + +``` +node1_bond_size = 100 NYM +node2_bond_size = 250_000 NYM +node3_bond_size = 1_000_000 NYM +``` +Rewards calculation: + +``` +node1_epoch_rewards = 5_000 * 1 * 1 * ( ( 1 / 240 ) + 0.3 * ( ( 100 / 1_000_000 ) / 240 ) ) * 1 / ( 1 + 0.3 ) +node2_epoch_rewards = 5_000 * 1 * 1 * ( ( 1 / 240 ) + 0.3 * ( ( 250_000 / 1_000_000 ) / 240 ) ) * 1 / ( 1 + 0.3 ) +node3_epoch_rewards = 5_000 * 1 * 1 * ( ( 1 / 240 ) + 0.3 * ( ( 1_000_000 / 1_000_000 ) / 240 ) ) * 1 / ( 1 + 0.3 ) +``` + +The result: +``` +node1_epoch_rewards = 16.0261 NYM +node2_epoch_rewards = 17.2275 NYM +node3_epoch_rewards = 20.8333 NYM +``` + +Difference between the smallest possible bond 100 NYM and a maximum bond 1mm NYM (equal full stake saturation point) is about 23% of increase in epoch rewards. + + +**Try to calculate rewards yourself** + + + +> Nym documentation pages are rendered statically (like in Nextra or Next.js static export mode), that's why we don't fetch from APIs at runtime, therefore on chain data must be filled by the user at the moment to ensure they are up to date. + +**Rewards are sent to the Nyx account used for bonding the node and each delegator automatically by the end of an epoch in which the node was part of the rewarded set,** following the logic [described below](#rewards-distribution). + +Given that there is a highly unlikely chance of all nodes having maximum stake saturation and performance, in majority of cases there will be some part of the reward budget left undistributed. This "change" is then kept in the [Mixmining reserve](../tokenomics#tokenomics). + + +All parameters regarding node performance score can be browsed or pull live from: + +`https://validator.nymtech.net/api/v1/nym-nodes/annotation/` + +In case you don't know your nodes `NODE_ID`, it's easy to find as long as your node is bonded. Visit [validator.nymtech.net/api/v1/nym-nodes/bonded](https://validator.nymtech.net/api/v1/nym-nodes/bonded) and search your node using `identity_key` or bonding Nyx account address (denoted as `owner`). + + + +### Rewards Distribution + +Once the [rewards are assigned per each node](#rewards-calculation) they need to be distributed between the operator of the node and delegators (people who staked their NYM on that node). The distribution is pretty straightforward and it happens in the following order: + +1. **Operators Cost (O.C.)**: How many NYM the operator requests to cover their costs per month, [divided by `720`](#nyx-epoch-vs-interval) (this value is set by the operator in the bonding wallet node settings) +2. **Profit Margin (P.M.)**: The extra % cut that the operator requests (this value is set by the operator in the bonding wallet node settings, smallest value is 20% to prevent race to bottom) +3. **Bond & Stake proportionally**: The remaining rewards are distributed proportionally to the weight of every stake (including self bond, self delegation and each delegation). + +#### Nyx Epoch vs Interval + + +> **1 epoch = 60 min**
+> **1 interval = 720 epochs** +> +> The logic is that interval is 30 days:
+> 24 epochs * 30 days = 720 + + +The Operators Cost (O.C). is a value denominated in NYM, that a node operator requires to get paid before the rewards get distributed. The cost is estimated per one month. However, it's paid only in epochs when the node is active. To calculate how O.C. works, we use a value called `interval` which represents 30 days (approximate month), or more precisely 720 epochs. To get covered a full O.C, the node would have to be active for the entire month. + +**O.C. real revenue formula** + +Therefore every epoch a node is active, the operator gets: + +> **epoch_operator_cost_revenue = operators_cost / epochs_per_interval** +> +> that is: +> +> **epoch_operator_cost_revenue = operators_cost / 720** + + +To calculate O.C. per month, multiply it by number of active epochs: + +> **monthly_operator_cost_revenue = ( operator_cost / 720 ) * active_epochs** + + +{/* +#### Final Layer Distribution (under development) We are working on the final design with the ratio implementing a [decision made by the operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407) as follows: @@ -248,7 +498,6 @@ We are working on the final design with the ratio implementing a [decision made > 33%; 67% -{/* In real numbers: If hourly revenue to all 240 nodes is 6000 NYM, the layer compartmentalisation is 960 NYM for Entry Gateway layer and each Mixnode layer and 2160 NYM for Exit Gateway layer. The calculation is in the example below: @@ -271,11 +520,9 @@ $33\% - 67\%$ ## Roadmap -We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation. The current design is called [*Naive rewarding*](#naive-rewarding). This is an intermediate step, expecting operators to migrate to `nym-node` in Mixnet smart contract and be able to recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards. +We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation, following the [decision made by the node operators](https://forum.nymtech.net/t/poll-what-should-be-the-split-of-mixmining-rewards-among-the-layers-of-the-nym-mixnet/407). The current design is called [*Naive rewarding*](#naive-rewarding). This is an intermediate step, expecting operators to migrate to `nym-node` in Mixnet smart contract and be able to recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards. -On November 5th, we presented a release roadmap in live [Operators Townhall](https://www.youtube.com/watch?v=3G1pJqvO2VM) where we explained in detail the steps of Nym node and tokenomics development and the effect it will have on node operators and put it into a rough timeline. - -![](/images/operators/tokenomics/roadmap_24-q4.png) +On November 5th 2024, we presented a release roadmap in live [Operators Townhall](https://www.youtube.com/watch?v=3G1pJqvO2VM) where we explained in detail the steps of Nym node and tokenomics development and the effect it will have on node operators and put it into a rough timeline. ### Naive Rewarding diff --git a/documentation/docs/pnpm-lock.yaml b/documentation/docs/pnpm-lock.yaml index 7c1df27a6c..fbe8b0b3ab 100644 --- a/documentation/docs/pnpm-lock.yaml +++ b/documentation/docs/pnpm-lock.yaml @@ -99,14 +99,14 @@ importers: specifier: ^0.438.0 version: 0.438.0(react@18.3.1) next: - specifier: ^14.2.15 - version: 14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^15.2.4 + version: 15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra: specifier: '2' - version: 2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra-theme-docs: specifier: '2' - version: 2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -355,6 +355,9 @@ packages: '@cosmjs/amino': '>= ^0.32' '@cosmjs/proto-signing': '>= ^0.32' + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + '@emotion/babel-plugin@11.12.0': resolution: {integrity: sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==} @@ -549,6 +552,111 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@interchain-ui/react@1.25.6': resolution: {integrity: sha512-9n2zOUlkr1H8AOxnUuSpgIB5EgEaa7LQHJsTot4UW3pbTBWE98H/OlquvQbhE72DPApFLDHogK0t9m5xfIRxMA==} peerDependencies: @@ -575,6 +683,10 @@ packages: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -673,6 +785,7 @@ packages: '@mui/base@5.0.0-beta.40': resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} engines: {node: '>=12.0.0'} + deprecated: This package has been replaced by @base-ui-components/react peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 @@ -875,68 +988,63 @@ packages: resolution: {integrity: sha512-jMxvwzkKzd3cXo2EB9GM2ic0eYo2rP/BS6gJt6HnWbsDO1O8GSD4k7o2Cpr2YERtMpGF/MGcDfsfj2EbQPtrXw==} engines: {node: '>= 10'} - '@next/env@14.2.15': - resolution: {integrity: sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==} + '@next/env@15.2.4': + resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==} '@next/eslint-plugin-next@13.4.13': resolution: {integrity: sha512-RpZeXlPxQ9FLeYN84XHDqRN20XxmVNclYCraLYdifRsmibtcWUWdwE/ANp2C8kgesFRsvwfsw6eOkYNl9sLJ3A==} - '@next/swc-darwin-arm64@14.2.15': - resolution: {integrity: sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==} + '@next/swc-darwin-arm64@15.2.4': + resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.15': - resolution: {integrity: sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==} + '@next/swc-darwin-x64@15.2.4': + resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.15': - resolution: {integrity: sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==} + '@next/swc-linux-arm64-gnu@15.2.4': + resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.15': - resolution: {integrity: sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==} + '@next/swc-linux-arm64-musl@15.2.4': + resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.15': - resolution: {integrity: sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==} + '@next/swc-linux-x64-gnu@15.2.4': + resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.15': - resolution: {integrity: sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==} + '@next/swc-linux-x64-musl@15.2.4': + resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.15': - resolution: {integrity: sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==} + '@next/swc-win32-arm64-msvc@15.2.4': + resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.15': - resolution: {integrity: sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - - '@next/swc-win32-x64-msvc@14.2.15': - resolution: {integrity: sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==} + '@next/swc-win32-x64-msvc@15.2.4': + resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] '@nextui-org/accordion@2.0.40': resolution: {integrity: sha512-aJmhflLOXOFTjbBWlWto30hYzimw+sw1EZwSRG9CdxbjRact2dRfCLsZQmHkJW2ifVx51g/qLNE2NSFAi2L8dA==} + deprecated: This package has been deprecated. Please use @heroui/accordion instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -952,6 +1060,7 @@ packages: '@nextui-org/autocomplete@2.1.7': resolution: {integrity: sha512-T3dF5akCXvJ21OxwPxAQmTjHoiB/GMUa2ppcJ9PStfCCPiI2vjwb4CO4q/duj/nXJIpQf/UfPhpSonnJ444o6g==} + deprecated: This package has been deprecated. Please use @heroui/autocomplete instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -961,6 +1070,7 @@ packages: '@nextui-org/avatar@2.0.33': resolution: {integrity: sha512-SPnIKM+34T/a+KCRCBiG8VwMBzu2/bap7IPHhmICtQ6KmG8Dzmazj3tGZsVt7HjhMRVY7e1vzev4IMaHqkIdRg==} + deprecated: This package has been deprecated. Please use @heroui/avatar instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -969,6 +1079,7 @@ packages: '@nextui-org/badge@2.0.32': resolution: {integrity: sha512-vlV/SY0e7/AmpVP7hB57XoSOo95Fr3kRWcLfMx8yL8VDR2UWMFaMlrT7JTghdgTGFSO7L1Ov1BFwDRRKVe3eyg==} + deprecated: This package has been deprecated. Please use @heroui/badge instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -977,6 +1088,7 @@ packages: '@nextui-org/breadcrumbs@2.0.13': resolution: {integrity: sha512-tdet47IBOwUaJL0PmxTuGH+ZI2nucyNwG3mX1OokfIXmq5HuMCGKaVFXaNP8mWb4Pii2bvtRqaqTfxmUb3kjGw==} + deprecated: This package has been deprecated. Please use @heroui/breadcrumbs instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -985,6 +1097,7 @@ packages: '@nextui-org/button@2.0.38': resolution: {integrity: sha512-XbgyqBv+X7QirXeriGwkqkMOENpAxXRo+jzfMyBMvfsM3kwrFj92OSF1F7/dWDvcW7imVZB9o2Ci7LIppq9ZZQ==} + deprecated: This package has been deprecated. Please use @heroui/button instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -994,6 +1107,7 @@ packages: '@nextui-org/calendar@2.0.12': resolution: {integrity: sha512-FnEnOQnsuyN+F+hy4LEJBvZZcfXMpDGgLkTdnDdoZObXQWwd0PWPjU8GzY+ukhhR5eiU7QIj2AADVRCvuAkiLA==} + deprecated: This package has been deprecated. Please use @heroui/calendar instead. peerDependencies: '@nextui-org/system': '>=2.1.0' '@nextui-org/theme': '>=2.2.0' @@ -1002,6 +1116,7 @@ packages: '@nextui-org/card@2.0.34': resolution: {integrity: sha512-2RYNPsQkM0FOifGCKmRBR3AuYgYCNmPV7dyA5M3D9Lf0APsHHtsXRA/GeIJ/AuPnglZrYBX8wpM5kLt3dnlQjQ==} + deprecated: This package has been deprecated. Please use @heroui/card instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1011,6 +1126,7 @@ packages: '@nextui-org/checkbox@2.1.5': resolution: {integrity: sha512-PSCWmxEzFPfeIJfoGAtbQS5T7JvBRblUMz5NdCMArA8MLvWW8EKL41cMPsqWjaUanjD0fAI8Q9HuDfBZnkcPbw==} + deprecated: This package has been deprecated. Please use @heroui/checkbox instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1019,6 +1135,7 @@ packages: '@nextui-org/chip@2.0.33': resolution: {integrity: sha512-6cQkMTV/34iPprjnfK6xlwkv5lnZjMsbYBN3ZqHHrQfV2zQg19ewFcuIw9XlRYA3pGYPpoycdOmSdQ6qXc66lQ==} + deprecated: This package has been deprecated. Please use @heroui/chip instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1027,6 +1144,7 @@ packages: '@nextui-org/code@2.0.33': resolution: {integrity: sha512-G2254ov2rsPxFEoJ0UHVHe+rSmNYwoHZc7STAtiTsJ2HfebZPQbNnfuCifMIpa+kgvHrMBGb85eGk0gy1R+ArA==} + deprecated: This package has been deprecated. Please use @heroui/code instead. peerDependencies: '@nextui-org/theme': '>=2.1.0' react: '>=18' @@ -1034,6 +1152,7 @@ packages: '@nextui-org/date-input@2.1.4': resolution: {integrity: sha512-U8Pbe7EhMp9VTfFxB/32+A9N9cJJWswebIz1qpaPy0Hmr92AHS3c1qVTcspkop6wbIM8AnHWEST0QkR95IXPDA==} + deprecated: This package has been deprecated. Please use @heroui/date-input instead. peerDependencies: '@nextui-org/system': '>=2.1.0' '@nextui-org/theme': '>=2.2.0' @@ -1042,6 +1161,7 @@ packages: '@nextui-org/date-picker@2.1.8': resolution: {integrity: sha512-pokAFcrf6AdM53QHf1EzvqVhj8imQRZHWitK9eZPtIdGzJzx28dW0ir7ID0lQFMiNNIQTesSpBLzedTawbcJrg==} + deprecated: This package has been deprecated. Please use @heroui/date-picker instead. peerDependencies: '@nextui-org/system': '>=2.1.0' '@nextui-org/theme': '>=2.2.0' @@ -1050,6 +1170,7 @@ packages: '@nextui-org/divider@2.0.32': resolution: {integrity: sha512-2B2j3VmvVDFnMc9Uw7UWMkByA+osgnRmVwMZNZjl9g3oCycz3UDXotNJXjgsLocT8tGO8UwMcrdgo7QBZl52uw==} + deprecated: This package has been deprecated. Please use @heroui/divider instead. peerDependencies: '@nextui-org/theme': '>=2.1.0' react: '>=18' @@ -1057,6 +1178,7 @@ packages: '@nextui-org/dropdown@2.1.31': resolution: {integrity: sha512-tP6c5MAhWK4hJ6U02oX6APUpjjrn97Zn7t+56Xx4YyQOSj0CJx18VF0JsU+MrjFZxPX3UBKU3B2zGBHOEGE4Kw==} + deprecated: This package has been deprecated. Please use @heroui/dropdown instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1073,6 +1195,7 @@ packages: '@nextui-org/image@2.0.32': resolution: {integrity: sha512-JpE0O8qAeJpQA61ZnXNLH76to+dbx93PR5tTOxSvmTxtnuqVg4wl5ar/SBY3czibJPr0sj33k8Mv2EfULjoH7Q==} + deprecated: This package has been deprecated. Please use @heroui/image instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1081,6 +1204,7 @@ packages: '@nextui-org/input@2.2.5': resolution: {integrity: sha512-xLgyKcnb+RatRZ62AbCFfTpS3exd2bPSSR75UFKylHHhgX+nvVOkX0dQgmr9e0V8IEECeRvbltw2s/laNFPTtg==} + deprecated: This package has been deprecated. Please use @heroui/input instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1089,6 +1213,7 @@ packages: '@nextui-org/kbd@2.0.34': resolution: {integrity: sha512-sO6RJPgEFccFV8gmfYMTVeQ4f9PBYh09OieRpsZhN4HqdfWwEaeT6LrmdBko3XnJ0T6Me3tBrYULgKWcDcNogw==} + deprecated: This package has been deprecated. Please use @heroui/kbd instead. peerDependencies: '@nextui-org/theme': '>=2.1.0' react: '>=18' @@ -1096,6 +1221,7 @@ packages: '@nextui-org/link@2.0.35': resolution: {integrity: sha512-0XVUsSsysu+WMssokTlLHiMnjr1N6D2Uh3bIBcdFwSqmTLyq+Llgexlm6Fuv1wADRwsR8/DGFp3Pr826cv2Svg==} + deprecated: This package has been deprecated. Please use @heroui/link instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1104,6 +1230,7 @@ packages: '@nextui-org/listbox@2.1.27': resolution: {integrity: sha512-B9HW/k0awfXsYaNyjaqv/GvEioVzrsCsOdSxVQZgQ3wQ6jNXmGRe1/X6IKg0fIa+P0v379kSgAqrZcwfRpKnWw==} + deprecated: This package has been deprecated. Please use @heroui/listbox instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1112,6 +1239,7 @@ packages: '@nextui-org/menu@2.0.30': resolution: {integrity: sha512-hZRr/EQ5JxB6yQFmUhDSv9pyLTJmaB4SFC/t5A17UljRhMexlvTU6QpalYIkbY0R/bUXvOkTJNzsRgI5OOQ/aA==} + deprecated: This package has been deprecated. Please use @heroui/menu instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1120,6 +1248,7 @@ packages: '@nextui-org/modal@2.0.41': resolution: {integrity: sha512-Sirn319xAf7E4cZqvQ0o0Vd3Xqy0FRSuhOTwp8dALMGTMY61c2nIyurgVCNP6hh8dMvMT7zQEPP9/LE0boFCEQ==} + deprecated: This package has been deprecated. Please use @heroui/modal instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1129,6 +1258,7 @@ packages: '@nextui-org/navbar@2.0.37': resolution: {integrity: sha512-HuHXMU+V367LlvSGjqRPBNKmOERLvc4XWceva+KmiT99BLqHmMECkQVTR6ogO36eJUU96aR8JSfAiHLjvw5msw==} + deprecated: This package has been deprecated. Please use @heroui/navbar instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1138,6 +1268,7 @@ packages: '@nextui-org/pagination@2.0.36': resolution: {integrity: sha512-VKs2vMj8dybNzb/WkAMmvFBsxdgBvpVihIA4eXSo2ve7fpcLjIF1iPLHuDgpSyv3h3dy009sQTVo3lVTVT1a6w==} + deprecated: This package has been deprecated. Please use @heroui/pagination instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1146,6 +1277,7 @@ packages: '@nextui-org/popover@2.1.29': resolution: {integrity: sha512-qGjMnAQVHQNfG571h9Tah2MXPs5mhxcTIj4TuBgwPzQTWXjjeffaHV3FlHdg5PxjTpNZOdDfrg0eRhDqIjKocQ==} + deprecated: This package has been deprecated. Please use @heroui/popover instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1155,6 +1287,7 @@ packages: '@nextui-org/progress@2.0.34': resolution: {integrity: sha512-rJmZCrLdufJKLsonJ37oPOEHEpZykD4c+0G749zcKOkRXHOD9DiQian2YoZEE/Yyf3pLdFQG3W9vSLbsgED3PQ==} + deprecated: This package has been deprecated. Please use @heroui/progress instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1163,6 +1296,7 @@ packages: '@nextui-org/radio@2.1.5': resolution: {integrity: sha512-0tF/VkMQv+KeYmFQpkrpz9S7j7U8gqCet+F97Cz7fFjdb+Q3w9waBzg84QayD7EZdjsYW4FNSkjPeiBhLdVUsw==} + deprecated: This package has been deprecated. Please use @heroui/radio instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1181,6 +1315,7 @@ packages: '@nextui-org/react@2.4.8': resolution: {integrity: sha512-ZwXg6As3A+Gs+Jyc42t4MHNupHEsh9YmEaypE20ikqIPTCLQnrGQ/RWOGwzZ2a9kZWbZ89a/3rJwZMRKdcemxg==} + deprecated: This package has been deprecated. Please use @heroui/react instead. peerDependencies: framer-motion: '>=10.17.0' react: '>=18' @@ -1188,6 +1323,7 @@ packages: '@nextui-org/ripple@2.0.33': resolution: {integrity: sha512-Zsa60CXtGCF7weTCFbSfT0OlxlGHdd5b/sSJTYrmMZRHOIUpHW8kT0bxVYF/6X8nCCJYxzBKXUqdE3Y31fhNeQ==} + deprecated: This package has been deprecated. Please use @heroui/ripple instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1197,6 +1333,7 @@ packages: '@nextui-org/scroll-shadow@2.1.20': resolution: {integrity: sha512-8ULiUmbZ/Jzr1okI8Yzjzl5M4Ow3pJEm34hT5id0EaMIgklNa3Nnp/Dyp54JwwUbI8Kt3jOAMqkPitGIZyo5Ag==} + deprecated: This package has been deprecated. Please use @heroui/scroll-shadow instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1205,6 +1342,7 @@ packages: '@nextui-org/select@2.2.7': resolution: {integrity: sha512-lA2EOjquhiHmLSInHFEarq64ZOQV37+ry1d8kvsqJ7R9dsqw1QEuMzH2Kk8/NqwrYMccHh5iAZ7PaLp90NSSxg==} + deprecated: This package has been deprecated. Please use @heroui/select instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1222,6 +1360,7 @@ packages: '@nextui-org/skeleton@2.0.32': resolution: {integrity: sha512-dS0vuRrc4oWktW3wa/KFhcBNnV0oiDqKXP4BqRj7wgS01fOAqj3cJiqwUDLKO8GbEnxLkbqLBFcUoLgktpRszQ==} + deprecated: This package has been deprecated. Please use @heroui/skeleton instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1230,6 +1369,7 @@ packages: '@nextui-org/slider@2.2.17': resolution: {integrity: sha512-MgeJv3X+bT7Bw+LK1zba4vToOUzv8lCvDuGe0U5suJy1AKGN6uGDgSAxpIZhCYNWsuNRsopwdvsGtyeIjOEStA==} + deprecated: This package has been deprecated. Please use @heroui/slider instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1238,6 +1378,7 @@ packages: '@nextui-org/snippet@2.0.43': resolution: {integrity: sha512-PLxc9ph9CLj52L26XSv4vBmQcSytCNc3ZBxkOTBEqmLSHCWwGQExrqKPnVZTE1etr6dcULiy5vNIpD8R7taO8A==} + deprecated: This package has been deprecated. Please use @heroui/snippet instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1247,6 +1388,7 @@ packages: '@nextui-org/spacer@2.0.33': resolution: {integrity: sha512-0YDtovMWuAVgBvVXUmplzohObGxMPFhisHXn6v+0nflAE9LiVeiXf121WVOEMrd08S7xvmrAANcMwo4TsYi49g==} + deprecated: This package has been deprecated. Please use @heroui/spacer instead. peerDependencies: '@nextui-org/theme': '>=2.1.0' react: '>=18' @@ -1254,6 +1396,7 @@ packages: '@nextui-org/spinner@2.0.34': resolution: {integrity: sha512-YKw/6xSLhsXU1k22OvYKyWhtJCHzW2bRAiieVSVG5xak3gYwknTds5H9s5uur+oAZVK9AkyAObD19QuZND32Jg==} + deprecated: This package has been deprecated. Please use @heroui/spinner instead. peerDependencies: '@nextui-org/theme': '>=2.1.0' react: '>=18' @@ -1261,6 +1404,7 @@ packages: '@nextui-org/switch@2.0.34': resolution: {integrity: sha512-SczQiHswo8eR94ecDgcULIsSIPfYVncqfKllcHEGqAs9BDpZun44KK0/R0xhWuPpx5oqB60VeSABN7JtEAxF+Q==} + deprecated: This package has been deprecated. Please use @heroui/switch instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1282,6 +1426,7 @@ packages: '@nextui-org/table@2.0.40': resolution: {integrity: sha512-qDbSsu6mpWnr1Mt3DYTBzTFtN8Z5Gv7GDqECGcDVradkDVuJFZvkB9Ke392LcVZoXSk99Rpamq4WSWkEewBhWg==} + deprecated: This package has been deprecated. Please use @heroui/table instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1290,6 +1435,7 @@ packages: '@nextui-org/tabs@2.0.37': resolution: {integrity: sha512-IQicuDggxTL+JeW3fRoZR4Rr24EwinxAdfU1jqcvT6gZywumndV27+I00kARz8P03kobYoY9t73NY92qo8T5gg==} + deprecated: This package has been deprecated. Please use @heroui/tabs instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1304,6 +1450,7 @@ packages: '@nextui-org/tooltip@2.0.41': resolution: {integrity: sha512-1c+vkCCszKcKl15HywlZ7UOL7c1UFgLudqBB/dEdWZiclT01BRiracMbcQ7McKHQCRl77Aa7LFv5x4wHOicWHQ==} + deprecated: This package has been deprecated. Please use @heroui/tooltip instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1411,6 +1558,7 @@ packages: '@nextui-org/user@2.0.34': resolution: {integrity: sha512-7MN/xBaMhDJ0b+hB2YpGIm2DsC9CTpN1ab+EKwhUuWn26SgXw2FNu8CSHViyDEkvOP7sYKdHLp9UtSo/f3JnsQ==} + deprecated: This package has been deprecated. Please use @heroui/user instead. peerDependencies: '@nextui-org/system': '>=2.0.0' '@nextui-org/theme': '>=2.1.0' @@ -1435,6 +1583,10 @@ packages: resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2481,17 +2633,17 @@ packages: '@stablelib/x25519@1.0.3': resolution: {integrity: sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==} - '@starknet-io/types-js@0.7.7': - resolution: {integrity: sha512-WLrpK7LIaIb8Ymxu6KF/6JkGW1sso988DweWu7p5QY/3y7waBIiPvzh27D9bX5KIJNRDyOoOVoHVEKYUYWZ/RQ==} + '@starknet-io/types-js@0.7.10': + resolution: {integrity: sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==} '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@swc/helpers@0.5.5': - resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} '@tanstack/react-virtual@3.10.8': resolution: {integrity: sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==} @@ -2531,6 +2683,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} @@ -2704,6 +2859,7 @@ packages: '@walletconnect/sign-client@2.17.1': resolution: {integrity: sha512-6rLw6YNy0smslH9wrFTbNiYrGsL3DrOsS5FcuU4gIN6oh8pGYOFZ5FiSyTTroc5tngOk3/Sd7dlGY9S7O4nveg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} @@ -2723,50 +2879,50 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} - '@webassemblyjs/ast@1.12.1': - resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - '@webassemblyjs/floating-point-hex-parser@1.11.6': - resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - '@webassemblyjs/helper-api-error@1.11.6': - resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - '@webassemblyjs/helper-buffer@1.12.1': - resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - '@webassemblyjs/helper-numbers@1.11.6': - resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - '@webassemblyjs/helper-wasm-bytecode@1.11.6': - resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - '@webassemblyjs/helper-wasm-section@1.12.1': - resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - '@webassemblyjs/ieee754@1.11.6': - resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - '@webassemblyjs/leb128@1.11.6': - resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - '@webassemblyjs/utf8@1.11.6': - resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - '@webassemblyjs/wasm-edit@1.12.1': - resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - '@webassemblyjs/wasm-gen@1.12.1': - resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - '@webassemblyjs/wasm-opt@1.12.1': - resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - '@webassemblyjs/wasm-parser@1.12.1': - resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - '@webassemblyjs/wast-printer@1.12.1': - resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -2774,8 +2930,8 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - abi-wan-kanabi@2.2.3: - resolution: {integrity: sha512-JlqiAl9CPvTm5kKG0QXmVCWNWoC/XyRMOeT77cQlbxXWllgjf6SqUmaNqFon72C2o5OSZids+5FvLdsw6dvWaw==} + abi-wan-kanabi@2.2.4: + resolution: {integrity: sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg==} hasBin: true abort-controller@3.0.0: @@ -2797,6 +2953,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.1: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} @@ -2892,6 +3053,10 @@ packages: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -2916,6 +3081,10 @@ packages: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + array.prototype.flatmap@1.3.2: resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} engines: {node: '>= 0.4'} @@ -2928,6 +3097,10 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -2935,6 +3108,10 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -3030,6 +3207,9 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -3037,8 +3217,8 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - browserslist@4.24.2: - resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + browserslist@4.25.0: + resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3058,10 +3238,22 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -3076,8 +3268,8 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001669: - resolution: {integrity: sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==} + caniuse-lite@1.0.30001722: + resolution: {integrity: sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==} cardinal@2.1.1: resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} @@ -3116,8 +3308,8 @@ packages: cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - cheerio@1.0.0: - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + cheerio@1.1.0: + resolution: {integrity: sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==} engines: {node: '>=18.17'} chokidar@3.6.0: @@ -3268,6 +3460,10 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crossws@0.3.1: resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==} @@ -3454,14 +3650,26 @@ packages: resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} engines: {node: '>= 0.4'} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + data-view-byte-length@1.0.1: resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} engines: {node: '>= 0.4'} + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + data-view-byte-offset@1.0.0: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -3547,6 +3755,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -3594,8 +3806,12 @@ packages: dompurify@3.1.6: resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} - domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -3606,8 +3822,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.43: - resolution: {integrity: sha512-NxnmFBHDl5Sachd2P46O7UJiMaMHMLSofoIWVJq3mj8NJgG0umiSeljAVP9lGzjI0UDLJJ5jjoGjcrB8RSbjLQ==} + electron-to-chromium@1.5.166: + resolution: {integrity: sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -3628,8 +3844,8 @@ packages: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} - encoding-sniffer@0.2.0: - resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -3638,10 +3854,18 @@ packages: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + enzyme-shallow-equal@1.0.7: resolution: {integrity: sha512-/um0GFqUXnpM9SvKtje+9Tjoz3f1fpBC3eXRFrNs8kpYn69JljciYP7KZTqM/YQbUY9KUjvKB4jo/q+L6WGGvg==} @@ -3655,6 +3879,10 @@ packages: resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} engines: {node: '>= 0.4'} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + es-array-method-boxes-properly@1.0.0: resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} @@ -3662,6 +3890,10 @@ packages: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -3670,24 +3902,40 @@ packages: resolution: {integrity: sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==} engines: {node: '>= 0.4'} - es-module-lexer@1.5.4: - resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + es-to-primitive@1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} @@ -3886,6 +4134,10 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3909,8 +4161,8 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} - fetch-cookie@3.0.1: - resolution: {integrity: sha512-ZGXe8Y5Z/1FWqQ9q/CrJhkUD73DyBU9VF0hBQmEO/wPHe4A9PKTjplFDLeFX8aOsYypZUcX5Ji/eByn3VCVO3Q==} + fetch-cookie@3.1.0: + resolution: {integrity: sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==} file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} @@ -3963,11 +4215,15 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreach@2.0.6: resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} form-data@4.0.1: @@ -4010,6 +4266,10 @@ packages: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} @@ -4021,6 +4281,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -4028,8 +4292,13 @@ packages: get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-starknet-core@4.0.0: resolution: {integrity: sha512-6pLmidQZkC3wZsrHY99grQHoGpuuXqkbSP65F8ov1/JsEI8DDLkhsAuLCKFzNOK56cJp+f1bWWfTJ57e9r5eqQ==} + deprecated: Package no longer supported. Please use @starknet-io/get-starknet-core get-stream@3.0.0: resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} @@ -4043,6 +4312,10 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -4101,6 +4374,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4122,6 +4399,10 @@ packages: has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@2.0.0: resolution: {integrity: sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==} engines: {node: '>=0.10.0'} @@ -4141,10 +4422,18 @@ packages: resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} engines: {node: '>= 0.4'} + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} @@ -4216,8 +4505,8 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + htmlparser2@10.0.0: + resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} @@ -4273,6 +4562,10 @@ packages: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -4302,6 +4595,10 @@ packages: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -4312,9 +4609,17 @@ packages: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -4323,6 +4628,10 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + is-buffer@2.0.5: resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} engines: {node: '>=4'} @@ -4338,14 +4647,26 @@ packages: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-data-view@1.0.1: resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -4365,6 +4686,10 @@ packages: is-finalizationregistry@1.0.2: resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -4373,6 +4698,10 @@ packages: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} engines: {node: '>= 0.4'} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -4397,6 +4726,10 @@ packages: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4424,6 +4757,10 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -4432,6 +4769,10 @@ packages: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} engines: {node: '>= 0.4'} + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-ssh@1.4.0: resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} @@ -4447,6 +4788,10 @@ packages: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + is-subset@0.1.1: resolution: {integrity: sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==} @@ -4454,10 +4799,18 @@ packages: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + is-typed-array@1.1.13: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -4465,6 +4818,10 @@ packages: is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + is-weakset@2.0.3: resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} engines: {node: '>= 0.4'} @@ -4502,8 +4859,8 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jiti@1.21.6: - resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true jiti@2.3.3: @@ -4624,8 +4981,8 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - lilconfig@3.1.2: - resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} lines-and-columns@1.2.4: @@ -4667,9 +5024,11 @@ packages: lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -4682,6 +5041,7 @@ packages: lodash.omit@4.5.0: resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4699,8 +5059,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lossless-json@4.0.2: - resolution: {integrity: sha512-+z0EaLi2UcWi8MZRxA5iTb6m4Ys4E80uftGY+yG5KNFJb5EceQXOhdW/pWJZ8m97s26u7yZZAYMcKWNztSZssA==} + lossless-json@4.1.0: + resolution: {integrity: sha512-DgoRs42jH/yNubp8iinRqvG0xn5awHKXVY+7lGYjBaByoHGZt/Dz5Jkaf5znP2XHbTnAA+bbkhK3lMIaf3+92A==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4737,6 +5097,10 @@ packages: match-sorter@6.3.4: resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} @@ -5039,8 +5403,8 @@ packages: nan@2.22.0: resolution: {integrity: sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -5075,21 +5439,24 @@ packages: react: '*' react-dom: '*' - next@14.2.15: - resolution: {integrity: sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==} - engines: {node: '>=18.17.0'} + next@15.2.4: + resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 - react: ^18.2.0 - react-dom: ^18.2.0 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': optional: true '@playwright/test': optional: true + babel-plugin-react-compiler: + optional: true sass: optional: true @@ -5139,8 +5506,8 @@ packages: node-readfiles@0.2.0: resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==} - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} non-layered-tidy-tree-layout@2.0.2: resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} @@ -5192,6 +5559,10 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-is@1.1.6: resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} engines: {node: '>= 0.4'} @@ -5204,10 +5575,18 @@ packages: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + object.entries@1.1.8: resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} engines: {node: '>= 0.4'} + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + object.fromentries@2.0.8: resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} @@ -5220,6 +5599,10 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + ofetch@1.4.1: resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} @@ -5246,6 +5629,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -5293,6 +5680,9 @@ packages: parse5@7.2.0: resolution: {integrity: sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -5363,8 +5753,8 @@ packages: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} pkg-types@1.2.1: @@ -5382,6 +5772,10 @@ packages: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -5427,8 +5821,8 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + postcss@8.5.4: + resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -5465,9 +5859,6 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -5476,9 +5867,6 @@ packages: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5625,6 +6013,10 @@ packages: react-dom: ^16.8.4 || ^17.0.0 || ^18.0.0 styled-components: ^4.1.1 || ^5.1.1 || ^6.0.5 + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -5639,6 +6031,10 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + rehype-katex@7.0.1: resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} @@ -5680,9 +6076,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5690,6 +6083,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -5737,13 +6135,25 @@ packages: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -5765,6 +6175,10 @@ packages: resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} engines: {node: '>= 12.13.0'} + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + scroll-into-view-if-needed@3.0.10: resolution: {integrity: sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==} @@ -5784,6 +6198,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -5798,6 +6217,10 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + sha.js@2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true @@ -5805,6 +6228,10 @@ packages: shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -5842,10 +6269,26 @@ packages: should@13.2.3: resolution: {integrity: sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==} + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5923,6 +6366,10 @@ packages: stickyfill@1.1.1: resolution: {integrity: sha512-GCp7vHAfpao+Qh/3Flh9DXEJ/qSi0KJwJw6zYlZOtRYXWUIpMM6mC2rIep/dK8RQqwW0KxGJIllmjPIBOGN8AA==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + stream-combiner@0.2.2: resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==} @@ -5956,6 +6403,10 @@ packages: string.prototype.repeat@1.0.0: resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + string.prototype.trim@1.2.9: resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} engines: {node: '>= 0.4'} @@ -5963,6 +6414,10 @@ packages: string.prototype.trimend@1.0.8: resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + string.prototype.trimstart@1.0.8: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} @@ -6014,13 +6469,13 @@ packages: react: '>= 16.8.0' react-dom: '>= 16.8.0' - styled-jsx@5.1.1: - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' peerDependenciesMeta: '@babel/core': optional: true @@ -6094,8 +6549,12 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} - terser-webpack-plugin@5.3.10: - resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} engines: {node: '>= 10.13.0'} peerDependencies: '@swc/core': '*' @@ -6110,8 +6569,8 @@ packages: uglify-js: optional: true - terser@5.36.0: - resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} + terser@5.42.0: + resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} engines: {node: '>=10'} hasBin: true @@ -6143,6 +6602,13 @@ packages: resolution: {integrity: sha512-TARUb7z1pGvlLxgPk++7wJ6aycXF3GJ0sNSBTAsTuJrQG5QuZlkUQP+zl+nbjAh4gMX9yDw9ZYklMd7vAfJKEw==} engines: {node: '>=0.10.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -6150,9 +6616,9 @@ packages: toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} - tough-cookie@4.1.4: - resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} - engines: {node: '>=6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -6188,8 +6654,8 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tslib@2.8.0: - resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -6207,18 +6673,34 @@ packages: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typed-array-byte-length@1.0.1: resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} engines: {node: '>= 0.4'} + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + typed-array-byte-offset@1.0.2: resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} engines: {node: '>= 0.4'} + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + typed-array-length@1.0.6: resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} engines: {node: '>= 0.4'} + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + typeforce@1.18.0: resolution: {integrity: sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==} @@ -6241,12 +6723,16 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici@6.21.0: - resolution: {integrity: sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==} - engines: {node: '>=18.17'} + undici@7.10.0: + resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==} + engines: {node: '>=20.18.1'} unenv@1.10.0: resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} @@ -6308,10 +6794,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -6364,8 +6846,8 @@ packages: resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} hasBin: true - update-browserslist-db@1.1.1: - resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6382,9 +6864,6 @@ packages: url-join@4.0.1: resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - url-template@2.0.8: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} @@ -6476,8 +6955,8 @@ packages: vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} - watchpack@2.4.2: - resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} engines: {node: '>=10.13.0'} web-namespaces@2.0.1: @@ -6489,8 +6968,8 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + webpack-sources@3.3.2: + resolution: {integrity: sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==} engines: {node: '>=10.13.0'} webpack@5.95.0: @@ -6520,10 +6999,18 @@ packages: which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + which-builtin-type@1.1.4: resolution: {integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==} engines: {node: '>= 0.4'} + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + which-collection@1.0.2: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} @@ -6532,6 +7019,10 @@ packages: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -6591,9 +7082,9 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.6.0: - resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} - engines: {node: '>= 14'} + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} hasBin: true yargs-parser@20.2.9: @@ -7196,6 +7687,11 @@ snapshots: '@cosmjs/proto-signing': 0.32.4 uuid: 9.0.1 + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.12.0': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -7452,26 +7948,26 @@ snapshots: dependencies: '@formatjs/fast-memoize': 2.2.1 '@formatjs/intl-localematcher': 0.5.5 - tslib: 2.8.0 + tslib: 2.8.1 '@formatjs/fast-memoize@2.2.1': dependencies: - tslib: 2.8.0 + tslib: 2.8.1 '@formatjs/icu-messageformat-parser@2.8.0': dependencies: '@formatjs/ecma402-abstract': 2.2.0 '@formatjs/icu-skeleton-parser': 1.8.4 - tslib: 2.8.0 + tslib: 2.8.1 '@formatjs/icu-skeleton-parser@1.8.4': dependencies: '@formatjs/ecma402-abstract': 2.2.0 - tslib: 2.8.0 + tslib: 2.8.1 '@formatjs/intl-localematcher@0.5.5': dependencies: - tslib: 2.8.0 + tslib: 2.8.1 '@formkit/auto-animate@0.8.2': {} @@ -7494,6 +7990,81 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@interchain-ui/react@1.25.6(@types/react@18.3.12)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/core': 1.6.8 @@ -7530,20 +8101,20 @@ snapshots: '@internationalized/date@3.5.6': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 '@internationalized/message@3.1.5': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 intl-messageformat: 10.7.1 '@internationalized/number@3.5.4': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 '@internationalized/string@3.2.4': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 '@isaacs/cliui@8.0.2': dependencies: @@ -7560,13 +8131,19 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/sourcemap-codec@1.5.0': {} @@ -7667,7 +8244,7 @@ snapshots: '@ledgerhq/errors': 6.19.1 '@ledgerhq/logs': 6.12.0 rxjs: 7.8.1 - semver: 7.6.3 + semver: 7.7.2 '@ledgerhq/errors@6.19.1': {} @@ -7901,37 +8478,34 @@ snapshots: '@napi-rs/simple-git-win32-arm64-msvc': 0.1.19 '@napi-rs/simple-git-win32-x64-msvc': 0.1.19 - '@next/env@14.2.15': {} + '@next/env@15.2.4': {} '@next/eslint-plugin-next@13.4.13': dependencies: glob: 7.1.7 - '@next/swc-darwin-arm64@14.2.15': + '@next/swc-darwin-arm64@15.2.4': optional: true - '@next/swc-darwin-x64@14.2.15': + '@next/swc-darwin-x64@15.2.4': optional: true - '@next/swc-linux-arm64-gnu@14.2.15': + '@next/swc-linux-arm64-gnu@15.2.4': optional: true - '@next/swc-linux-arm64-musl@14.2.15': + '@next/swc-linux-arm64-musl@15.2.4': optional: true - '@next/swc-linux-x64-gnu@14.2.15': + '@next/swc-linux-x64-gnu@15.2.4': optional: true - '@next/swc-linux-x64-musl@14.2.15': + '@next/swc-linux-x64-musl@15.2.4': optional: true - '@next/swc-win32-arm64-msvc@14.2.15': + '@next/swc-win32-arm64-msvc@15.2.4': optional: true - '@next/swc-win32-ia32-msvc@14.2.15': - optional: true - - '@next/swc-win32-x64-msvc@14.2.15': + '@next/swc-win32-x64-msvc@15.2.4': optional: true '@nextui-org/accordion@2.0.40(@nextui-org/system@2.2.6(@nextui-org/theme@2.2.11(tailwindcss@3.4.14))(framer-motion@11.11.9(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@nextui-org/theme@2.2.11(tailwindcss@3.4.14))(framer-motion@11.11.9(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -8911,6 +9485,8 @@ snapshots: '@noble/hashes@1.5.0': {} + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9027,7 +9603,7 @@ snapshots: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/breadcrumbs': 3.7.5(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/breadcrumbs@3.5.18(react@18.3.1)': @@ -9037,7 +9613,7 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/breadcrumbs': 3.7.8(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/button@3.10.1(react@18.3.1)': @@ -9048,7 +9624,7 @@ snapshots: '@react-stately/toggle': 3.7.8(react@18.3.1) '@react-types/button': 3.10.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/button@3.9.5(react@18.3.1)': @@ -9059,7 +9635,7 @@ snapshots: '@react-stately/toggle': 3.7.8(react@18.3.1) '@react-types/button': 3.10.0(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/calendar@3.5.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9073,7 +9649,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/calendar': 3.4.10(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9088,7 +9664,7 @@ snapshots: '@react-types/button': 3.9.4(react@18.3.1) '@react-types/calendar': 3.4.6(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9104,7 +9680,7 @@ snapshots: '@react-stately/toggle': 3.7.4(react@18.3.1) '@react-types/checkbox': 3.8.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/checkbox@3.14.8(react@18.3.1)': @@ -9119,7 +9695,7 @@ snapshots: '@react-stately/toggle': 3.7.8(react@18.3.1) '@react-types/checkbox': 3.8.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/color@3.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9136,7 +9712,7 @@ snapshots: '@react-stately/form': 3.0.6(react@18.3.1) '@react-types/color': 3.0.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9156,7 +9732,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/combobox': 3.13.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9176,7 +9752,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/combobox': 3.11.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9199,7 +9775,7 @@ snapshots: '@react-types/datepicker': 3.7.4(react@18.3.1) '@react-types/dialog': 3.5.13(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9222,7 +9798,7 @@ snapshots: '@react-types/datepicker': 3.8.3(react@18.3.1) '@react-types/dialog': 3.5.13(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9233,7 +9809,7 @@ snapshots: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/dialog': 3.5.13(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9244,7 +9820,7 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/dialog': 3.5.13(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9259,7 +9835,7 @@ snapshots: '@react-stately/dnd': 3.4.3(react@18.3.1) '@react-types/button': 3.10.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9268,7 +9844,7 @@ snapshots: '@react-aria/interactions': 3.21.3(react@18.3.1) '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -9277,7 +9853,7 @@ snapshots: '@react-aria/interactions': 3.22.4(react@18.3.1) '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -9287,7 +9863,7 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-stately/form': 3.0.6(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/form@3.0.5(react@18.3.1)': @@ -9296,7 +9872,7 @@ snapshots: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-stately/form': 3.0.6(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/grid@3.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9313,7 +9889,7 @@ snapshots: '@react-types/checkbox': 3.8.4(react@18.3.1) '@react-types/grid': 3.2.9(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9329,7 +9905,7 @@ snapshots: '@react-stately/list': 3.11.0(react@18.3.1) '@react-stately/tree': 3.8.5(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9342,7 +9918,7 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/i18n@3.12.3(react@18.3.1)': @@ -9354,7 +9930,7 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/interactions@3.21.3(react@18.3.1)': @@ -9362,7 +9938,7 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/interactions@3.22.4(react@18.3.1)': @@ -9370,21 +9946,21 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/label@3.7.12(react@18.3.1)': dependencies: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/label@3.7.8(react@18.3.1)': dependencies: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/link@3.7.1(react@18.3.1)': @@ -9394,7 +9970,7 @@ snapshots: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/link': 3.5.5(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/link@3.7.6(react@18.3.1)': @@ -9404,7 +9980,7 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/link': 3.5.8(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/listbox@3.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9417,7 +9993,7 @@ snapshots: '@react-stately/list': 3.10.5(react@18.3.1) '@react-types/listbox': 3.5.2(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9431,13 +10007,13 @@ snapshots: '@react-stately/list': 3.11.0(react@18.3.1) '@react-types/listbox': 3.5.2(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@react-aria/live-announcer@3.4.0': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 '@react-aria/menu@3.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -9453,7 +10029,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/menu': 3.9.9(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9471,7 +10047,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/menu': 3.9.12(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9480,7 +10056,7 @@ snapshots: '@react-aria/progress': 3.4.17(react@18.3.1) '@react-types/meter': 3.4.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/numberfield@3.11.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9495,7 +10071,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/numberfield': 3.8.6(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9511,7 +10087,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/overlays': 3.8.7(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9527,7 +10103,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/overlays': 3.8.10(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9538,7 +10114,7 @@ snapshots: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/progress': 3.5.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/progress@3.4.17(react@18.3.1)': @@ -9548,7 +10124,7 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/progress': 3.5.7(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/radio@3.10.4(react@18.3.1)': @@ -9562,7 +10138,7 @@ snapshots: '@react-stately/radio': 3.10.4(react@18.3.1) '@react-types/radio': 3.8.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/radio@3.10.9(react@18.3.1)': @@ -9576,7 +10152,7 @@ snapshots: '@react-stately/radio': 3.10.8(react@18.3.1) '@react-types/radio': 3.8.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/searchfield@3.7.10(react@18.3.1)': @@ -9588,7 +10164,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/searchfield': 3.5.9(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/select@3.14.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9606,7 +10182,7 @@ snapshots: '@react-types/button': 3.10.0(react@18.3.1) '@react-types/select': 3.9.7(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9618,7 +10194,7 @@ snapshots: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-stately/selection': 3.17.0(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9630,7 +10206,7 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-stately/selection': 3.17.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9638,7 +10214,7 @@ snapshots: dependencies: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/slider@3.7.13(react@18.3.1)': @@ -9651,7 +10227,7 @@ snapshots: '@react-stately/slider': 3.5.8(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/slider': 3.7.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/slider@3.7.8(react@18.3.1)': @@ -9664,7 +10240,7 @@ snapshots: '@react-stately/slider': 3.5.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/slider': 3.7.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/spinbutton@3.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9674,18 +10250,18 @@ snapshots: '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/button': 3.10.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@react-aria/ssr@3.9.4(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/ssr@3.9.6(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/switch@3.6.4(react@18.3.1)': @@ -9693,7 +10269,7 @@ snapshots: '@react-aria/toggle': 3.10.9(react@18.3.1) '@react-stately/toggle': 3.7.4(react@18.3.1) '@react-types/switch': 3.5.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/switch@3.6.9(react@18.3.1)': @@ -9702,7 +10278,7 @@ snapshots: '@react-stately/toggle': 3.7.8(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/switch': 3.5.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/table@3.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -9722,7 +10298,7 @@ snapshots: '@react-types/grid': 3.2.6(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/table': 3.9.5(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9742,7 +10318,7 @@ snapshots: '@react-types/grid': 3.2.9(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/table': 3.10.2(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9755,7 +10331,7 @@ snapshots: '@react-stately/tabs': 3.6.6(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) '@react-types/tabs': 3.3.7(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9768,7 +10344,7 @@ snapshots: '@react-stately/tabs': 3.6.10(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/tabs': 3.3.10(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9783,7 +10359,7 @@ snapshots: '@react-stately/list': 3.11.0(react@18.3.1) '@react-types/button': 3.10.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9797,7 +10373,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/textfield': 3.9.7(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/textfield@3.14.5(react@18.3.1)': @@ -9810,7 +10386,7 @@ snapshots: '@react-stately/utils': 3.10.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) '@react-types/textfield': 3.9.3(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/toggle@3.10.9(react@18.3.1)': @@ -9821,7 +10397,7 @@ snapshots: '@react-stately/toggle': 3.7.8(react@18.3.1) '@react-types/checkbox': 3.8.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/tooltip@3.7.4(react@18.3.1)': @@ -9832,7 +10408,7 @@ snapshots: '@react-stately/tooltip': 3.4.9(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/tooltip': 3.4.9(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/tooltip@3.7.9(react@18.3.1)': @@ -9843,7 +10419,7 @@ snapshots: '@react-stately/tooltip': 3.4.13(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/tooltip': 3.4.12(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/utils@3.24.1(react@18.3.1)': @@ -9851,7 +10427,7 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -9860,7 +10436,7 @@ snapshots: '@react-aria/ssr': 3.9.6(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 clsx: 2.1.1 react: 18.3.1 @@ -9869,7 +10445,7 @@ snapshots: '@react-aria/interactions': 3.22.4(react@18.3.1) '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-aria/visually-hidden@3.8.17(react@18.3.1)': @@ -9877,7 +10453,7 @@ snapshots: '@react-aria/interactions': 3.22.4(react@18.3.1) '@react-aria/utils': 3.25.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-icons/all-files@4.1.0(react@18.3.1)': @@ -9890,7 +10466,7 @@ snapshots: '@react-stately/utils': 3.10.1(react@18.3.1) '@react-types/calendar': 3.4.6(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/calendar@3.5.5(react@18.3.1)': @@ -9899,7 +10475,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/calendar': 3.4.10(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/checkbox@3.6.5(react@18.3.1)': @@ -9908,7 +10484,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/checkbox': 3.8.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/checkbox@3.6.9(react@18.3.1)': @@ -9917,19 +10493,19 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/checkbox': 3.8.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/collections@3.10.7(react@18.3.1)': dependencies: '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/collections@3.11.0(react@18.3.1)': dependencies: '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/color@3.8.0(react@18.3.1)': @@ -9943,7 +10519,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/color': 3.0.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/combobox@3.10.0(react@18.3.1)': @@ -9956,7 +10532,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/combobox': 3.13.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/combobox@3.8.4(react@18.3.1)': @@ -9969,13 +10545,13 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/combobox': 3.11.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/data@3.11.7(react@18.3.1)': dependencies: '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/datepicker@3.10.3(react@18.3.1)': @@ -9987,7 +10563,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/datepicker': 3.8.3(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/datepicker@3.9.4(react@18.3.1)': @@ -9999,30 +10575,30 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/datepicker': 3.7.4(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/dnd@3.4.3(react@18.3.1)': dependencies: '@react-stately/selection': 3.17.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/flags@3.0.4': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 '@react-stately/form@3.0.3(react@18.3.1)': dependencies: '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/form@3.0.6(react@18.3.1)': dependencies: '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/grid@3.9.3(react@18.3.1)': @@ -10031,7 +10607,7 @@ snapshots: '@react-stately/selection': 3.17.0(react@18.3.1) '@react-types/grid': 3.2.9(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/list@3.10.5(react@18.3.1)': @@ -10040,7 +10616,7 @@ snapshots: '@react-stately/selection': 3.17.0(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/list@3.11.0(react@18.3.1)': @@ -10049,7 +10625,7 @@ snapshots: '@react-stately/selection': 3.17.0(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/menu@3.7.1(react@18.3.1)': @@ -10057,7 +10633,7 @@ snapshots: '@react-stately/overlays': 3.6.11(react@18.3.1) '@react-types/menu': 3.9.9(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/menu@3.8.3(react@18.3.1)': @@ -10065,7 +10641,7 @@ snapshots: '@react-stately/overlays': 3.6.11(react@18.3.1) '@react-types/menu': 3.9.12(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/numberfield@3.9.7(react@18.3.1)': @@ -10074,21 +10650,21 @@ snapshots: '@react-stately/form': 3.0.6(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/numberfield': 3.8.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/overlays@3.6.11(react@18.3.1)': dependencies: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/overlays': 3.8.10(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/overlays@3.6.7(react@18.3.1)': dependencies: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/overlays': 3.8.7(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/radio@3.10.4(react@18.3.1)': @@ -10097,7 +10673,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/radio': 3.8.1(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/radio@3.10.8(react@18.3.1)': @@ -10106,14 +10682,14 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/radio': 3.8.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/searchfield@3.5.7(react@18.3.1)': dependencies: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/searchfield': 3.5.9(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/select@3.6.8(react@18.3.1)': @@ -10123,7 +10699,7 @@ snapshots: '@react-stately/overlays': 3.6.11(react@18.3.1) '@react-types/select': 3.9.7(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/selection@3.17.0(react@18.3.1)': @@ -10131,7 +10707,7 @@ snapshots: '@react-stately/collections': 3.11.0(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/slider@3.5.4(react@18.3.1)': @@ -10139,7 +10715,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/slider': 3.7.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/slider@3.5.8(react@18.3.1)': @@ -10147,7 +10723,7 @@ snapshots: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/slider': 3.7.6(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/table@3.11.8(react@18.3.1)': @@ -10160,7 +10736,7 @@ snapshots: '@react-types/grid': 3.2.6(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/table': 3.9.5(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/table@3.12.3(react@18.3.1)': @@ -10173,7 +10749,7 @@ snapshots: '@react-types/grid': 3.2.9(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/table': 3.10.2(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/tabs@3.6.10(react@18.3.1)': @@ -10181,7 +10757,7 @@ snapshots: '@react-stately/list': 3.11.0(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) '@react-types/tabs': 3.3.10(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/tabs@3.6.6(react@18.3.1)': @@ -10189,35 +10765,35 @@ snapshots: '@react-stately/list': 3.11.0(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) '@react-types/tabs': 3.3.7(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/toggle@3.7.4(react@18.3.1)': dependencies: '@react-stately/utils': 3.10.1(react@18.3.1) '@react-types/checkbox': 3.8.4(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/toggle@3.7.8(react@18.3.1)': dependencies: '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/checkbox': 3.8.4(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/tooltip@3.4.13(react@18.3.1)': dependencies: '@react-stately/overlays': 3.6.11(react@18.3.1) '@react-types/tooltip': 3.4.12(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/tooltip@3.4.9(react@18.3.1)': dependencies: '@react-stately/overlays': 3.6.11(react@18.3.1) '@react-types/tooltip': 3.4.9(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/tree@3.8.1(react@18.3.1)': @@ -10226,7 +10802,7 @@ snapshots: '@react-stately/selection': 3.17.0(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.23.1(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/tree@3.8.5(react@18.3.1)': @@ -10235,24 +10811,24 @@ snapshots: '@react-stately/selection': 3.17.0(react@18.3.1) '@react-stately/utils': 3.10.4(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/utils@3.10.1(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/utils@3.10.4(react@18.3.1)': dependencies: - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-stately/virtualizer@3.7.1(react@18.3.1)': dependencies: '@react-aria/utils': 3.24.1(react@18.3.1) '@react-types/shared': 3.25.0(react@18.3.1) - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.17 react: 18.3.1 '@react-types/accordion@3.0.0-alpha.21(react@18.3.1)': @@ -10643,18 +11219,17 @@ snapshots: '@stablelib/random': 1.0.2 '@stablelib/wipe': 1.0.1 - '@starknet-io/types-js@0.7.7': {} + '@starknet-io/types-js@0.7.10': {} '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.13': + '@swc/helpers@0.5.15': dependencies: - tslib: 2.8.0 + tslib: 2.8.1 - '@swc/helpers@0.5.5': + '@swc/helpers@0.5.17': dependencies: - '@swc/counter': 0.1.3 - tslib: 2.8.0 + tslib: 2.8.1 '@tanstack/react-virtual@3.10.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -10699,6 +11274,8 @@ snapshots: '@types/estree@1.0.6': {} + '@types/estree@1.0.8': {} + '@types/hast@2.3.10': dependencies: '@types/unist': 2.0.11 @@ -10790,7 +11367,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.6.3 + semver: 7.7.2 ts-api-utils: 1.3.0(typescript@4.9.5) optionalDependencies: typescript: 4.9.5 @@ -11087,87 +11664,87 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 - '@webassemblyjs/ast@1.12.1': + '@webassemblyjs/ast@1.14.1': dependencies: - '@webassemblyjs/helper-numbers': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/floating-point-hex-parser@1.11.6': {} + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - '@webassemblyjs/helper-api-error@1.11.6': {} + '@webassemblyjs/helper-api-error@1.13.2': {} - '@webassemblyjs/helper-buffer@1.12.1': {} + '@webassemblyjs/helper-buffer@1.14.1': {} - '@webassemblyjs/helper-numbers@1.11.6': + '@webassemblyjs/helper-numbers@1.13.2': dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.11.6 - '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 '@xtuc/long': 4.2.2 - '@webassemblyjs/helper-wasm-bytecode@1.11.6': {} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - '@webassemblyjs/helper-wasm-section@1.12.1': + '@webassemblyjs/helper-wasm-section@1.14.1': dependencies: - '@webassemblyjs/ast': 1.12.1 - '@webassemblyjs/helper-buffer': 1.12.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/ieee754@1.11.6': + '@webassemblyjs/ieee754@1.13.2': dependencies: '@xtuc/ieee754': 1.2.0 - '@webassemblyjs/leb128@1.11.6': + '@webassemblyjs/leb128@1.13.2': dependencies: '@xtuc/long': 4.2.2 - '@webassemblyjs/utf8@1.11.6': {} + '@webassemblyjs/utf8@1.13.2': {} - '@webassemblyjs/wasm-edit@1.12.1': + '@webassemblyjs/wasm-edit@1.14.1': dependencies: - '@webassemblyjs/ast': 1.12.1 - '@webassemblyjs/helper-buffer': 1.12.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/helper-wasm-section': 1.12.1 - '@webassemblyjs/wasm-gen': 1.12.1 - '@webassemblyjs/wasm-opt': 1.12.1 - '@webassemblyjs/wasm-parser': 1.12.1 - '@webassemblyjs/wast-printer': 1.12.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 - '@webassemblyjs/wasm-gen@1.12.1': + '@webassemblyjs/wasm-gen@1.14.1': dependencies: - '@webassemblyjs/ast': 1.12.1 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/ieee754': 1.11.6 - '@webassemblyjs/leb128': 1.11.6 - '@webassemblyjs/utf8': 1.11.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 - '@webassemblyjs/wasm-opt@1.12.1': + '@webassemblyjs/wasm-opt@1.14.1': dependencies: - '@webassemblyjs/ast': 1.12.1 - '@webassemblyjs/helper-buffer': 1.12.1 - '@webassemblyjs/wasm-gen': 1.12.1 - '@webassemblyjs/wasm-parser': 1.12.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wasm-parser@1.12.1': + '@webassemblyjs/wasm-parser@1.14.1': dependencies: - '@webassemblyjs/ast': 1.12.1 - '@webassemblyjs/helper-api-error': 1.11.6 - '@webassemblyjs/helper-wasm-bytecode': 1.11.6 - '@webassemblyjs/ieee754': 1.11.6 - '@webassemblyjs/leb128': 1.11.6 - '@webassemblyjs/utf8': 1.11.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 - '@webassemblyjs/wast-printer@1.12.1': + '@webassemblyjs/wast-printer@1.14.1': dependencies: - '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} - abi-wan-kanabi@2.2.3: + abi-wan-kanabi@2.2.4: dependencies: ansicolors: 0.3.2 cardinal: 2.1.1 @@ -11178,9 +11755,9 @@ snapshots: dependencies: event-target-shim: 5.0.1 - acorn-import-attributes@1.9.5(acorn@8.13.0): + acorn-import-attributes@1.9.5(acorn@8.15.0): dependencies: - acorn: 8.13.0 + acorn: 8.15.0 acorn-jsx@5.3.2(acorn@8.13.0): dependencies: @@ -11188,6 +11765,8 @@ snapshots: acorn@8.13.0: {} + acorn@8.15.0: {} + agent-base@7.1.1: dependencies: debug: 4.3.7 @@ -11271,6 +11850,11 @@ snapshots: call-bind: 1.0.7 is-array-buffer: 3.0.4 + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-includes@3.1.8: dependencies: call-bind: 1.0.7 @@ -11284,12 +11868,12 @@ snapshots: array.prototype.filter@1.0.4: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.3 + es-abstract: 1.24.0 es-array-method-boxes-properly: 1.0.0 - es-object-atoms: 1.0.0 - is-string: 1.0.7 + es-object-atoms: 1.1.1 + is-string: 1.1.1 array.prototype.findlast@1.2.5: dependencies: @@ -11316,6 +11900,13 @@ snapshots: es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + array.prototype.flatmap@1.3.2: dependencies: call-bind: 1.0.7 @@ -11342,10 +11933,22 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + ast-types-flow@0.0.8: {} astring@1.9.0: {} + async-function@1.0.0: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -11439,18 +12042,22 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 brorand@1.1.0: {} - browserslist@4.24.2: + browserslist@4.25.0: dependencies: - caniuse-lite: 1.0.30001669 - electron-to-chromium: 1.5.43 - node-releases: 2.0.18 - update-browserslist-db: 1.1.1(browserslist@4.24.2) + caniuse-lite: 1.0.30001722 + electron-to-chromium: 1.5.166 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.0) bs58@4.0.1: dependencies: @@ -11473,6 +12080,11 @@ snapshots: dependencies: streamsearch: 1.1.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -11481,6 +12093,18 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + call-me-maybe@1.0.2: {} callsites@3.1.0: {} @@ -11489,7 +12113,7 @@ snapshots: camelize@1.0.1: {} - caniuse-lite@1.0.30001669: {} + caniuse-lite@1.0.30001722: {} cardinal@2.1.1: dependencies: @@ -11534,20 +12158,20 @@ snapshots: css-what: 6.1.0 domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 - cheerio@1.0.0: + cheerio@1.1.0: dependencies: cheerio-select: 2.1.0 dom-serializer: 2.0.0 domhandler: 5.0.3 - domutils: 3.1.0 - encoding-sniffer: 0.2.0 - htmlparser2: 9.1.0 - parse5: 7.2.0 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.0.0 + parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 6.21.0 + undici: 7.10.0 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -11721,6 +12345,12 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crossws@0.3.1: dependencies: uncrypto: 0.1.3 @@ -11734,7 +12364,7 @@ snapshots: boolbase: 1.0.0 css-what: 6.1.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 nth-check: 2.1.1 css-to-react-native@3.2.0: @@ -11936,18 +12566,36 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + data-view-byte-length@1.0.1: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 is-data-view: 1.0.1 + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + data-view-byte-offset@1.0.0: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 is-data-view: 1.0.1 + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dayjs@1.11.13: {} debug@3.2.7: @@ -12006,6 +12654,9 @@ snapshots: detect-libc@1.0.3: {} + detect-libc@2.0.4: + optional: true + detect-node-es@1.1.0: {} devlop@1.1.0: @@ -12051,12 +12702,18 @@ snapshots: dompurify@3.1.6: {} - domutils@3.1.0: + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} duplexify@4.1.3: @@ -12068,7 +12725,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.43: {} + electron-to-chromium@1.5.166: {} elkjs@0.9.3: {} @@ -12098,7 +12755,7 @@ snapshots: emojis-list@3.0.0: {} - encoding-sniffer@0.2.0: + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 whatwg-encoding: 3.1.1 @@ -12112,8 +12769,15 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + entities@4.5.0: {} + entities@6.0.1: {} + enzyme-shallow-equal@1.0.7: dependencies: hasown: 2.0.2 @@ -12121,28 +12785,28 @@ snapshots: enzyme@3.11.0: dependencies: - array.prototype.flat: 1.3.2 - cheerio: 1.0.0 + array.prototype.flat: 1.3.3 + cheerio: 1.1.0 enzyme-shallow-equal: 1.0.7 - function.prototype.name: 1.1.6 + function.prototype.name: 1.1.8 has: 1.0.4 html-element-map: 1.3.1 - is-boolean-object: 1.1.2 + is-boolean-object: 1.2.2 is-callable: 1.2.7 - is-number-object: 1.0.7 - is-regex: 1.1.4 - is-string: 1.0.7 + is-number-object: 1.1.1 + is-regex: 1.2.1 + is-string: 1.1.1 is-subset: 0.1.1 lodash.escape: 4.0.1 lodash.isequal: 4.5.0 - object-inspect: 1.13.2 + object-inspect: 1.13.4 object-is: 1.1.6 - object.assign: 4.1.5 - object.entries: 1.1.8 - object.values: 1.2.0 + object.assign: 4.1.7 + object.entries: 1.1.9 + object.values: 1.2.1 raf: 3.4.1 rst-selector-parser: 2.2.3 - string.prototype.trim: 1.2.9 + string.prototype.trim: 1.2.10 error-ex@1.3.2: dependencies: @@ -12197,12 +12861,71 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.15 + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + es-array-method-boxes-properly@1.0.0: {} es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-iterator-helpers@1.1.0: @@ -12222,28 +12945,49 @@ snapshots: iterator.prototype: 1.1.3 safe-array-concat: 1.1.2 - es-module-lexer@1.5.4: {} + es-module-lexer@1.7.0: {} es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.3: dependencies: get-intrinsic: 1.2.4 has-tostringtag: 1.0.2 hasown: 2.0.2 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-shim-unscopables@1.0.2: dependencies: hasown: 2.0.2 + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + es-to-primitive@1.2.1: dependencies: is-callable: 1.2.7 is-date-object: 1.0.5 is-symbol: 1.0.4 + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + es6-promise@3.3.1: {} escalade@3.2.0: {} @@ -12547,6 +13291,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -12565,10 +13317,10 @@ snapshots: dependencies: reusify: 1.0.4 - fetch-cookie@3.0.1: + fetch-cookie@3.1.0: dependencies: set-cookie-parser: 2.7.1 - tough-cookie: 4.1.4 + tough-cookie: 5.1.2 file-entry-cache@6.0.1: dependencies: @@ -12609,11 +13361,15 @@ snapshots: dependencies: is-callable: 1.2.7 + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreach@2.0.6: {} - foreground-child@3.3.0: + foreground-child@3.3.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 form-data@4.0.1: @@ -12624,7 +13380,7 @@ snapshots: framer-motion@11.11.9(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - tslib: 2.8.0 + tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.3.1 react: 18.3.1 @@ -12652,6 +13408,15 @@ snapshots: es-abstract: 1.23.3 functions-have-names: 1.2.3 + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + functions-have-names@1.2.3: {} get-caller-file@2.0.5: {} @@ -12664,13 +13429,31 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port-please@3.1.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-starknet-core@4.0.0: dependencies: - '@starknet-io/types-js': 0.7.7 + '@starknet-io/types-js': 0.7.10 get-stream@3.0.0: {} @@ -12682,6 +13465,12 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -12709,7 +13498,7 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.3.0 + foreground-child: 3.3.1 jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 @@ -12766,6 +13555,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -12801,6 +13592,8 @@ snapshots: has-bigints@1.0.2: {} + has-bigints@1.1.0: {} + has-flag@2.0.0: {} has-flag@3.0.0: {} @@ -12813,8 +13606,14 @@ snapshots: has-proto@1.0.3: {} + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: dependencies: has-symbols: 1.0.3 @@ -12959,16 +13758,16 @@ snapshots: html-element-map@1.3.1: dependencies: array.prototype.filter: 1.0.4 - call-bind: 1.0.7 + call-bind: 1.0.8 html-void-elements@3.0.0: {} - htmlparser2@9.1.0: + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 + domutils: 3.2.2 + entities: 6.0.1 http-shutdown@1.2.2: {} @@ -13017,6 +13816,12 @@ snapshots: hasown: 2.0.2 side-channel: 1.0.6 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + internmap@1.0.1: {} internmap@2.0.3: {} @@ -13028,7 +13833,7 @@ snapshots: '@formatjs/ecma402-abstract': 2.2.0 '@formatjs/fast-memoize': 2.2.1 '@formatjs/icu-messageformat-parser': 2.8.0 - tslib: 2.8.0 + tslib: 2.8.1 invariant@2.2.4: dependencies: @@ -13048,6 +13853,12 @@ snapshots: call-bind: 1.0.7 get-intrinsic: 1.2.4 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} is-arrayish@0.3.2: {} @@ -13056,10 +13867,22 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -13069,11 +13892,16 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-buffer@2.0.5: {} is-bun-module@1.2.1: dependencies: - semver: 7.6.3 + semver: 7.7.2 is-callable@1.2.7: {} @@ -13081,14 +13909,29 @@ snapshots: dependencies: hasown: 2.0.2 + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-data-view@1.0.1: dependencies: is-typed-array: 1.1.13 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + is-date-object@1.0.5: dependencies: has-tostringtag: 1.0.2 + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -13101,12 +13944,23 @@ snapshots: dependencies: call-bind: 1.0.7 + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} is-generator-function@1.0.10: dependencies: has-tostringtag: 1.0.2 + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -13125,6 +13979,11 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-obj@3.0.0: {} @@ -13144,12 +14003,23 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-set@2.0.3: {} is-shared-array-buffer@1.0.3: dependencies: call-bind: 1.0.7 + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-ssh@1.4.0: dependencies: protocols: 2.0.1 @@ -13162,22 +14032,41 @@ snapshots: dependencies: has-tostringtag: 1.0.2 + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-subset@0.1.1: {} is-symbol@1.0.4: dependencies: has-symbols: 1.0.3 + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-typed-array@1.1.13: dependencies: which-typed-array: 1.1.15 + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + is-weakmap@2.0.2: {} is-weakref@1.0.2: dependencies: call-bind: 1.0.7 + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + is-weakset@2.0.3: dependencies: call-bind: 1.0.7 @@ -13226,7 +14115,7 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jiti@1.21.6: {} + jiti@1.21.7: {} jiti@2.3.3: {} @@ -13327,7 +14216,7 @@ snapshots: lilconfig@2.1.0: {} - lilconfig@3.1.2: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -13400,7 +14289,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - lossless-json@4.0.2: {} + lossless-json@4.1.0: {} lru-cache@10.4.3: {} @@ -13430,6 +14319,8 @@ snapshots: '@babel/runtime': 7.25.9 remove-accents: 0.5.0 + math-intrinsics@1.1.0: {} + md5.js@1.3.5: dependencies: hash-base: 3.1.0 @@ -13965,7 +14856,7 @@ snapshots: minimatch@9.0.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimist@1.2.8: {} @@ -14014,7 +14905,7 @@ snapshots: nan@2.22.0: {} - nanoid@3.3.8: {} + nanoid@3.3.11: {} natural-compare@1.4.0: {} @@ -14038,44 +14929,44 @@ snapshots: transitivePeerDependencies: - supports-color - next-seo@6.6.0(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-seo@6.6.0(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - next: 14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next-themes@0.2.1(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-themes@0.2.1(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - next: 14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.15 - '@swc/helpers': 0.5.5 + '@next/env': 15.2.4 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001669 - graceful-fs: 4.2.11 + caniuse-lite: 1.0.30001722 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1) + styled-jsx: 5.1.6(babel-plugin-macros@3.1.0)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.15 - '@next/swc-darwin-x64': 14.2.15 - '@next/swc-linux-arm64-gnu': 14.2.15 - '@next/swc-linux-arm64-musl': 14.2.15 - '@next/swc-linux-x64-gnu': 14.2.15 - '@next/swc-linux-x64-musl': 14.2.15 - '@next/swc-win32-arm64-msvc': 14.2.15 - '@next/swc-win32-ia32-msvc': 14.2.15 - '@next/swc-win32-x64-msvc': 14.2.15 + '@next/swc-darwin-arm64': 15.2.4 + '@next/swc-darwin-x64': 15.2.4 + '@next/swc-linux-arm64-gnu': 15.2.4 + '@next/swc-linux-arm64-musl': 15.2.4 + '@next/swc-linux-x64-gnu': 15.2.4 + '@next/swc-linux-x64-musl': 15.2.4 + '@next/swc-win32-arm64-msvc': 15.2.4 + '@next/swc-win32-x64-msvc': 15.2.4 + sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@popperjs/core': 2.11.8 @@ -14086,16 +14977,16 @@ snapshots: git-url-parse: 13.1.1 intersection-observer: 0.12.2 match-sorter: 6.3.4 - next: 14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-seo: 6.6.0(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-themes: 0.2.1(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-seo: 6.6.0(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: 0.2.1(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nextra: 2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.23.8 - nextra@2.13.4(next@14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra@2.13.4(next@15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/mdx': 2.3.0 @@ -14109,7 +15000,7 @@ snapshots: gray-matter: 4.0.3 katex: 0.16.11 lodash.get: 4.4.2 - next: 14.2.15(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 15.2.4(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-mdx-remote: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) p-limit: 3.1.0 react: 18.3.1 @@ -14155,7 +15046,7 @@ snapshots: dependencies: es6-promise: 3.3.1 - node-releases@2.0.18: {} + node-releases@2.0.19: {} non-layered-tidy-tree-layout@2.0.2: {} @@ -14212,6 +15103,8 @@ snapshots: object-inspect@1.13.2: {} + object-inspect@1.13.4: {} + object-is@1.1.6: dependencies: call-bind: 1.0.7 @@ -14226,12 +15119,28 @@ snapshots: has-symbols: 1.0.3 object-keys: 1.1.1 + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + object.entries@1.1.8: dependencies: call-bind: 1.0.7 define-properties: 1.2.1 es-object-atoms: 1.0.0 + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + object.fromentries@2.0.8: dependencies: call-bind: 1.0.7 @@ -14251,6 +15160,13 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + ofetch@1.4.1: dependencies: destr: 2.0.3 @@ -14286,6 +15202,12 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-finally@1.0.0: {} p-limit@3.1.0: @@ -14335,16 +15257,20 @@ snapshots: parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 - parse5: 7.2.0 + parse5: 7.3.0 parse5-parser-stream@7.1.2: dependencies: - parse5: 7.2.0 + parse5: 7.3.0 parse5@7.2.0: dependencies: entities: 4.5.0 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -14409,7 +15335,7 @@ snapshots: sonic-boom: 2.8.0 thread-stream: 0.15.2 - pirates@4.0.6: {} + pirates@4.0.7: {} pkg-types@1.2.1: dependencies: @@ -14425,28 +15351,30 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-import@15.1.0(postcss@8.4.47): + possible-typed-array-names@1.1.0: {} + + postcss-import@15.1.0(postcss@8.5.4): dependencies: - postcss: 8.4.47 + postcss: 8.5.4 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.4.47): + postcss-js@4.0.1(postcss@8.5.4): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.47 + postcss: 8.5.4 - postcss-load-config@4.0.2(postcss@8.4.47): + postcss-load-config@4.0.2(postcss@8.5.4): dependencies: - lilconfig: 3.1.2 - yaml: 2.6.0 + lilconfig: 3.1.3 + yaml: 2.8.0 optionalDependencies: - postcss: 8.4.47 + postcss: 8.5.4 - postcss-nested@6.2.0(postcss@8.4.47): + postcss-nested@6.2.0(postcss@8.5.4): dependencies: - postcss: 8.4.47 + postcss: 8.5.4 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -14458,19 +15386,19 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 postcss@8.4.38: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.47: + postcss@8.5.4: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -14512,8 +15440,6 @@ snapshots: pseudomap@1.0.2: {} - psl@1.9.0: {} - punycode@2.3.1: {} query-string@7.1.3: @@ -14523,8 +15449,6 @@ snapshots: split-on-first: 1.1.0 strict-uri-encode: 2.0.0 - querystringify@2.2.0: {} - queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -14614,7 +15538,7 @@ snapshots: dependencies: react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) - tslib: 2.8.0 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 @@ -14623,7 +15547,7 @@ snapshots: react: 18.3.1 react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) - tslib: 2.8.0 + tslib: 2.8.1 use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) optionalDependencies: @@ -14668,7 +15592,7 @@ snapshots: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 - tslib: 2.8.0 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 @@ -14759,6 +15683,17 @@ snapshots: - react-native - supports-color + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -14780,6 +15715,15 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + rehype-katex@7.0.1: dependencies: '@types/hast': 3.0.4 @@ -14854,12 +15798,16 @@ snapshots: require-from-string@2.0.2: {} - requires-port@1.0.0: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + resolve@1.22.8: dependencies: is-core-module: 2.15.1 @@ -14900,7 +15848,7 @@ snapshots: rxjs@7.8.1: dependencies: - tslib: 2.8.0 + tslib: 2.8.1 sade@1.8.1: dependencies: @@ -14913,14 +15861,33 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + safe-regex-test@1.0.3: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 is-regex: 1.1.4 + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -14949,6 +15916,13 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + scroll-into-view-if-needed@3.0.10: dependencies: compute-scroll-into-view: 3.1.0 @@ -14966,6 +15940,8 @@ snapshots: semver@7.6.3: {} + semver@7.7.2: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -14988,6 +15964,12 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + sha.js@2.4.11: dependencies: inherits: 2.0.4 @@ -14995,6 +15977,33 @@ snapshots: shallowequal@1.1.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -15040,6 +16049,26 @@ snapshots: should-type-adaptors: 1.1.0 should-util: 1.0.1 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -15047,6 +16076,14 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.2 + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -15109,16 +16146,16 @@ snapshots: starknet@6.11.0: dependencies: '@noble/curves': 1.4.2 - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.8.0 '@scure/base': 1.1.9 '@scure/starknet': 1.0.0 - abi-wan-kanabi: 2.2.3 - fetch-cookie: 3.0.1 + abi-wan-kanabi: 2.2.4 + fetch-cookie: 3.1.0 get-starknet-core: 4.0.0 isomorphic-fetch: 3.0.0 - lossless-json: 4.0.2 + lossless-json: 4.1.0 pako: 2.1.0 - starknet-types-07: '@starknet-io/types-js@0.7.7' + starknet-types-07: '@starknet-io/types-js@0.7.10' ts-mixer: 6.0.4 url-join: 4.0.1 transitivePeerDependencies: @@ -15128,6 +16165,11 @@ snapshots: stickyfill@1.1.1: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + stream-combiner@0.2.2: dependencies: duplexer: 0.1.2 @@ -15177,6 +16219,16 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.3 + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + string.prototype.trim@1.2.9: dependencies: call-bind: 1.0.7 @@ -15190,6 +16242,13 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + string.prototype.trimstart@1.0.8: dependencies: call-bind: 1.0.7 @@ -15243,7 +16302,7 @@ snapshots: stylis: 4.3.2 tslib: 2.6.2 - styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1): + styled-jsx@5.1.6(babel-plugin-macros@3.1.0)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 @@ -15258,12 +16317,12 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 - pirates: 4.0.6 + pirates: 4.0.7 ts-interface-checker: 0.1.13 supports-color@4.5.0: @@ -15320,41 +16379,43 @@ snapshots: chokidar: 3.6.0 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.3.2 + fast-glob: 3.3.3 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.6 + jiti: 1.21.7 lilconfig: 2.1.0 micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.4.47 - postcss-import: 15.1.0(postcss@8.4.47) - postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47) - postcss-nested: 6.2.0(postcss@8.4.47) + postcss: 8.5.4 + postcss-import: 15.1.0(postcss@8.5.4) + postcss-js: 4.0.1(postcss@8.5.4) + postcss-load-config: 4.0.2(postcss@8.5.4) + postcss-nested: 6.2.0(postcss@8.5.4) postcss-selector-parser: 6.1.2 - resolve: 1.22.8 + resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node tapable@2.2.1: {} - terser-webpack-plugin@5.3.10(webpack@5.95.0): + tapable@2.2.2: {} + + terser-webpack-plugin@5.3.14(webpack@5.95.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 - schema-utils: 3.3.0 + schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.36.0 + terser: 5.42.0 webpack: 5.95.0 - terser@5.36.0: + terser@5.42.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.13.0 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -15391,18 +16452,21 @@ snapshots: titleize@1.0.0: {} + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toggle-selection@1.0.6: {} - tough-cookie@4.1.4: + tough-cookie@5.1.2: dependencies: - psl: 1.9.0 - punycode: 2.3.1 - universalify: 0.2.0 - url-parse: 1.5.10 + tldts: 6.1.86 tr46@0.0.3: {} @@ -15431,7 +16495,7 @@ snapshots: tslib@2.6.2: {} - tslib@2.8.0: {} + tslib@2.8.1: {} type-check@0.4.0: dependencies: @@ -15447,6 +16511,12 @@ snapshots: es-errors: 1.3.0 is-typed-array: 1.1.13 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typed-array-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -15455,6 +16525,14 @@ snapshots: has-proto: 1.0.3 is-typed-array: 1.1.13 + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + typed-array-byte-offset@1.0.2: dependencies: available-typed-arrays: 1.0.7 @@ -15464,6 +16542,16 @@ snapshots: has-proto: 1.0.3 is-typed-array: 1.1.13 + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + typed-array-length@1.0.6: dependencies: call-bind: 1.0.7 @@ -15473,6 +16561,15 @@ snapshots: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typeforce@1.18.0: {} typescript@4.9.5: {} @@ -15493,9 +16590,16 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + uncrypto@0.1.3: {} - undici@6.21.0: {} + undici@7.10.0: {} unenv@1.10.0: dependencies: @@ -15599,8 +16703,6 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - universalify@0.2.0: {} - universalify@2.0.1: {} unstorage@1.12.0(idb-keyval@6.2.1): @@ -15624,9 +16726,9 @@ snapshots: consola: 3.2.3 pathe: 1.1.2 - update-browserslist-db@1.1.1(browserslist@4.24.2): + update-browserslist-db@1.1.3(browserslist@4.25.0): dependencies: - browserslist: 4.24.2 + browserslist: 4.25.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -15640,17 +16742,12 @@ snapshots: url-join@4.0.1: {} - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - url-template@2.0.8: {} use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 - tslib: 2.8.0 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 @@ -15675,7 +16772,7 @@ snapshots: dependencies: detect-node-es: 1.1.0 react: 18.3.1 - tslib: 2.8.0 + tslib: 2.8.1 optionalDependencies: '@types/react': 18.3.12 @@ -15733,7 +16830,7 @@ snapshots: vscode-textmate@8.0.0: {} - watchpack@2.4.2: + watchpack@2.4.4: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -15744,20 +16841,20 @@ snapshots: webidl-conversions@3.0.1: {} - webpack-sources@3.2.3: {} + webpack-sources@3.3.2: {} webpack@5.95.0: dependencies: - '@types/estree': 1.0.6 - '@webassemblyjs/ast': 1.12.1 - '@webassemblyjs/wasm-edit': 1.12.1 - '@webassemblyjs/wasm-parser': 1.12.1 - acorn: 8.13.0 - acorn-import-attributes: 1.9.5(acorn@8.13.0) - browserslist: 4.24.2 + '@types/estree': 1.0.8 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + browserslist: 4.25.0 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.17.1 - es-module-lexer: 1.5.4 + enhanced-resolve: 5.18.1 + es-module-lexer: 1.7.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -15767,10 +16864,10 @@ snapshots: mime-types: 2.1.35 neo-async: 2.6.2 schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.95.0) - watchpack: 2.4.2 - webpack-sources: 3.2.3 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(webpack@5.95.0) + watchpack: 2.4.4 + webpack-sources: 3.3.2 transitivePeerDependencies: - '@swc/core' - esbuild @@ -15797,6 +16894,14 @@ snapshots: is-string: 1.0.7 is-symbol: 1.0.4 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + which-builtin-type@1.1.4: dependencies: function.prototype.name: 1.1.6 @@ -15812,6 +16917,22 @@ snapshots: which-collection: 1.0.2 which-typed-array: 1.1.15 + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + which-collection@1.0.2: dependencies: is-map: 2.0.3 @@ -15827,6 +16948,16 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.2 + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -15872,7 +17003,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.6.0: {} + yaml@2.8.0: {} yargs-parser@20.2.9: {} diff --git a/documentation/docs/public/favicon.svg b/documentation/docs/public/favicon.svg new file mode 100644 index 0000000000..73752de315 --- /dev/null +++ b/documentation/docs/public/favicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/docs/public/images/Nym_meta_Image.png b/documentation/docs/public/images/Nym_meta_Image.png new file mode 100644 index 0000000000..b94571f147 Binary files /dev/null and b/documentation/docs/public/images/Nym_meta_Image.png differ diff --git a/documentation/docs/theme.config.tsx b/documentation/docs/theme.config.tsx index 19bc1049ab..9d07c2ba75 100644 --- a/documentation/docs/theme.config.tsx +++ b/documentation/docs/theme.config.tsx @@ -10,7 +10,8 @@ const config: DocsThemeConfig = { const config = useConfig(); const { route } = useRouter(); const url = process.env.NEXT_PUBLIC_SITE_URL; - const image = url + "/nym_logo.jpg"; + const image = url + "/images/Nym_meta_Image.png"; + const favicon = url + "/favicon.svg"; // Define descriptions for different "books" const bookDescriptions: Record = { @@ -33,14 +34,14 @@ const config: DocsThemeConfig = { bookDescriptions[topLevel] || defaultDescription; - const title = config.title + (route === "/" ? "" : " - Nym docs"); + const title = (route === "/" ? "Nym docs" : config.title + " - Nym docs"); return ( <> {title} - + diff --git a/documentation/scripts/api-scraping/api_targets.py b/documentation/scripts/api-scraping/api_targets.py index d7491dec27..6bf8015047 100644 --- a/documentation/scripts/api-scraping/api_targets.py +++ b/documentation/scripts/api-scraping/api_targets.py @@ -140,6 +140,7 @@ def get_nested_value(response, args): def _return_percent_annotation(value): value = float(value) * 100 + value = round(value, 2) value = f"{value}%" return value diff --git a/documentation/scripts/next-scripts/python-prebuild.sh b/documentation/scripts/next-scripts/python-prebuild.sh index 34c23964b0..a78bfbf11e 100755 --- a/documentation/scripts/next-scripts/python-prebuild.sh +++ b/documentation/scripts/next-scripts/python-prebuild.sh @@ -13,17 +13,25 @@ cd cmdrun && ./nyx-total-stake.sh > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md && cd ../api-scraping && -python api_targets.py v --api mainnet --endpoint circulating-supply --value circulating_supply amount --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md && +python api_targets.py validator --api mainnet --endpoint circulating-supply --value circulating_supply amount --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md && -python api_targets.py v --api mainnet --endpoint circulating-supply --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md && +python api_targets.py validator --api mainnet --endpoint circulating-supply --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md && -python api_targets.py v --api mainnet --endpoint epoch/reward_params --value interval staking_supply_scale_factor --format percent > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md && +python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply_scale_factor --format percent > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md && -python api_targets.py v --api mainnet --endpoint epoch/reward_params --value interval stake_saturation_point --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md && +python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval stake_saturation_point --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md && + +python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval staking_supply --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md && + +python api_targets.py validator --api mainnet --endpoint epoch/reward_params --value interval epoch_reward_budget --format markdown --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md && python api_targets.py time_now > ../../docs/components/outputs/api-scraping-outputs/time-now.md && -python api_targets.py c --staking_target --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md && +python api_targets.py calculate --staking_target --separator _ > ../../docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md && + +curl -L https://validator.nymtech.net/api/v1/circulating-supply | jq > ../../docs/components/outputs/api-scraping-outputs/circulating-supply.json && + +curl -L https://validator.nymtech.net/api/v1/epoch/reward_params | jq > ../../docs/components/outputs/api-scraping-outputs/reward-params.json && cd ../../../scripts && echo '```python' > ../documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md && diff --git a/envs/canary.env b/envs/canary.env index 6bdd85494e..f494ad3270 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -18,7 +18,6 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 -EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/ NYXD=https://rpc.canary-validator.performance.nymte.ch NYM_API=https://canary-api.performance.nymte.ch/api/ NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket diff --git a/envs/mainnet-local-api.env b/envs/mainnet-local-api.env index d6683c937d..f7d1fec6d5 100644 --- a/envs/mainnet-local-api.env +++ b/envs/mainnet-local-api.env @@ -23,5 +23,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://rpc.nymtech.net" NYM_API="http://127.0.0.1:8000" NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" NYM_VPN_API="https://nymvpn.com/api" diff --git a/envs/mainnet.env b/envs/mainnet.env index ad1e2cd88e..444307f836 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -24,5 +24,4 @@ STATISTICS_SERVICE_DOMAIN_ADDRESS=https://mainnet-stats.nymte.ch:8090 NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS=wss://rpc.nymtech.net/websocket -EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API=https://nymvpn.com/api/ diff --git a/envs/qa.env b/envs/qa.env deleted file mode 100644 index 2a4fddb0a7..0000000000 --- a/envs/qa.env +++ /dev/null @@ -1,25 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 -NETWORK_NAME=qa -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 - -MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296 -ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7 -GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t -MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0 -COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr -VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj -REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 - -EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/ -NYXD=https://rpc.qa-validator.qa.nymte.ch -NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket -NYM_API=https://qa-nym-api.qa.nymte.ch/api/ -NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/ diff --git a/envs/sandbox.env b/envs/sandbox.env index 68e51fec59..e90257f015 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,7 +19,6 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865 ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -EXPLORER_API=https://sandbox-explorer.nymtech.net/api/ NYXD=https://rpc.sandbox.nymtech.net NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket NYM_API=https://sandbox-nym-api1.nymtech.net/api/ diff --git a/explorer-nextjs/.eslintrc.json b/explorer-nextjs/.eslintrc.json deleted file mode 100644 index 957cd1545e..0000000000 --- a/explorer-nextjs/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals"] -} diff --git a/explorer-nextjs/README.md b/explorer-nextjs/README.md deleted file mode 100644 index c4033664f8..0000000000 --- a/explorer-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/explorer-nextjs/app/App.tsx b/explorer-nextjs/app/App.tsx deleted file mode 100644 index 6763797c84..0000000000 --- a/explorer-nextjs/app/App.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import { Navbar } from './components/Nav/Navbar' -import { Providers } from './providers' - -const App = ({ children }: { children: React.ReactNode }) => ( - - {children} - -) - -export { App } diff --git a/explorer-nextjs/app/account/[id]/page.tsx b/explorer-nextjs/app/account/[id]/page.tsx deleted file mode 100644 index c0c482476c..0000000000 --- a/explorer-nextjs/app/account/[id]/page.tsx +++ /dev/null @@ -1,407 +0,0 @@ -'use client' - -import * as React from 'react' -import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material' -import { useParams } from 'next/navigation' -import { useMainContext } from '@/app/context/main' -import { Title } from '@/app/components/Title' -import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table"; -import { useMemo } from "react"; -import { humanReadableCurrencyToString } from "@/app/utils/currency"; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import { PieChart } from '@mui/x-charts/PieChart'; -import { useTheme } from "@mui/material/styles"; -import { useIsMobile } from "@/app/hooks"; -import { StyledLink } from "@/app/components"; - -const AccumulatedRewards = ({account}: { account?: any}) => { - const columns = useMemo< - MRT_ColumnDef[] - >(() => { - return [ - { - id: 'accumulated-rewards-data', - header: 'Accumulated Rewards Data', - columns: [ - { - id: 'node_id', - accessorKey: 'node_id', - header: 'Node ID', - size: 150, - Cell: ({ row }) => ( - {row.original.node_id} - ), - }, - { - id: 'node_still_fully_bonded', - accessorKey: 'node_still_fully_bonded', - header: 'Node still bonded?', - width: 150, - Cell: ({ row }) => ( - <>{row.original.node_still_fully_bonded ? : - theme.palette.warning.main }}> - - Unbonded - } - ) - }, - { - id: 'amount_staked', - accessorKey: 'amount_staked', - header: 'Amount', - width: 150, - Cell: ({ row }) => ( - <>{humanReadableCurrencyToString(row.original.amount_staked)} - ) - }, - { - id: 'rewards', - accessorKey: 'rewards', - header: 'Rewards', - width: 150, - Cell: ({ row }) => ( - {humanReadableCurrencyToString(row.original.rewards)} - ) - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: account?.accumulated_rewards || [], - enableFullScreenToggle: false, - }) - - return (); -} - -const DelegationHistory = ({account}: { account?: any}) => { - const columns = useMemo< - MRT_ColumnDef[] - >(() => { - return [ - { - id: 'delegation-history-data', - header: 'Delegation History', - columns: [ - { - id: 'node_id', - accessorKey: 'node_id', - header: 'Node ID', - size: 150, - }, - { - id: 'delegated', - accessorKey: 'delegated', - header: 'Amount', - width: 150, - Cell: ({ row }) => ( - <>{humanReadableCurrencyToString(row.original.delegated)} - ) - }, - { - id: 'height', - accessorKey: 'height', - header: 'Delegated at height', - width: 150, - Cell: ({ row }) => ( - <>{row.original.height} - ) - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: account?.delegations || [], - enableFullScreenToggle: false, - }) - - return (); -} - - -/** - * Shows account details - */ -const PageAccountWithState = ({ account }: { - account?: any; -}) => { - const theme = useTheme(); - const isMobile = useIsMobile(); - - const pieChartData = React.useMemo(() => { - if(!account) { - return []; - } - - const parts = []; - - const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6; - - if(nymBalance > 0) { - parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main}); - } - - if(account.vesting_account) { - if (`${account.vesting_account.locked?.amount}` !== "0") { - const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Vesting locked", - value, - color: 'red' - }); - } - } - if (`${account.vesting_account.spendable?.amount}` !== "0") { - const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Vesting spendable", - value, - color: theme.palette.primary.light - }); - } - } - } - - if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") { - const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Claimable delegation rewards", - value, - color: theme.palette.success.light - }); - } - } - if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") { - const value = Number.parseFloat(account.operator_rewards.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Claimable operator rewards", - value, - color: theme.palette.success.dark - }); - } - } - if (account.total_delegations && `${account.total_delegations.amount}` !== "0") { - const value = Number.parseFloat(account.total_delegations.amount) / 1e6; - if(value > 0) { - parts.push({ - label: "Total delegations", - value, - color: '#888' - }); - } - } - - return parts; - }, [account]); - - return ( - - - - </Box> - - <Box mt={4} sx={{ maxWidth: "600px" }}> - <PieChart - series={[ - { - data: pieChartData, - innerRadius: 40, - outerRadius: 80, - cy: isMobile ? 200 : undefined, - }, - ]} - height={300} - slotProps={isMobile ? { - legend: { position: { vertical: "top", horizontal: "right" } } - } : undefined} - /> - </Box> - - <Box mt={4}> - <TableContainer component={Paper} sx={{ maxWidth: "400px" }}> - <Table> - <TableBody> - <TableRow sx={{ color: theme => theme.palette.primary.main }}> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - <strong>Spendable Balance</strong> - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))} - </TableCell> - </TableRow> - <TableRow> - <TableCell component="th" scope="row"> - Total delegations - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString(account.total_delegations)} - </TableCell> - </TableRow> - {account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - Claimable delegation rewards - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.claimable_rewards)} - </TableCell> - </TableRow>} - {account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - Claimable operator rewards - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.operator_rewards)} - </TableCell> - </TableRow>} - {account.vesting_account && ( - <> - <TableRow> - <TableCell component="th" scope="row" colSpan={2}> - Vesting account - </TableCell> - </TableRow> - {`${account.vesting_account.locked.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Locked - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.locked)} - </TableCell> - </TableRow> - } - {`${account.vesting_account.vested.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Vested - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.vested)} - </TableCell> - </TableRow> - } - {`${account.vesting_account.vesting.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Vesting - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.vesting)} - </TableCell> - </TableRow> - } - {`${account.vesting_account.spendable.amount}` !== "0" && - <TableRow> - <TableCell component="th" scope="row" sx={{ pl: 4 }}> - Spendable - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - {humanReadableCurrencyToString(account.vesting_account.spendable)} - </TableCell> - </TableRow> - } - </> - )} - <TableRow> - <TableCell component="th" scope="row" sx={{ color: "inherit" }}> - <h3>Total value</h3> - </TableCell> - <TableCell align="right" sx={{ color: "inherit" }}> - <h3>{humanReadableCurrencyToString(account.total_value)}</h3> - </TableCell> - </TableRow> - </TableBody> - </Table> - </TableContainer> - </Box> - <Box mt={4}> - <AccumulatedRewards account={account}/> - </Box> - <Box mt={4}> - <DelegationHistory account={account}/> - </Box> - </Box> - ) -} - -/** - * Guard component to handle loading and not found states - */ -const PageAccountDetailGuard = ({ account } : { account: string }) => { - const [accountDetails, setAccountDetails] = React.useState<any>(); - const [isLoading, setLoading] = React.useState<boolean>(true); - const [error, setError] = React.useState<string>(); - const { fetchAccountById } = useMainContext() - const { id } = useParams() - - React.useEffect(() => { - setLoading(true); - (async () => { - if(typeof(id) === "string") { - try { - const res = await fetchAccountById(account); - setAccountDetails(res); - } catch(e: any) { - setError(e.message); - } - finally { - setLoading(false); - } - } - })(); - }, [id]) - - if (isLoading) { - return <CircularProgress /> - } - - // loaded, but not found - if (error) { - return ( - <Alert severity="warning"> - <AlertTitle>Account not found</AlertTitle> - Sorry, we could not find the account <code>{id || ''}</code> - </Alert> - ) - } - - return <PageAccountWithState account={accountDetails} /> -} - -/** - * Wrapper component that adds the account details based on the `id` in the address URL - */ -const PageAccountDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! Could not find that account</Alert> - ) - } - - return ( - <PageAccountDetailGuard account={id} /> - ) -} - -export default PageAccountDetail diff --git a/explorer-nextjs/app/api/constants.ts b/explorer-nextjs/app/api/constants.ts deleted file mode 100644 index 515655c184..0000000000 --- a/explorer-nextjs/app/api/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -// master APIs -export const API_BASE_URL = process.env.NEXT_PUBLIC_EXPLORER_API_URL || 'https://explorer.nymtech.net/api/v1'; -export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https://validator.nymtech.net'; - -export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net'; - -export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru'; - -// specific API routes -export const OVERVIEW_API = `${API_BASE_URL}/overview`; -export const MIXNODE_PING = `${API_BASE_URL}/ping`; -export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; -export const MIXNODE_API = `${API_BASE_URL}/mix-node`; -export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`; -export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`; -export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; -export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. -export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this -export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`; -export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`; -export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`; -export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`; -export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`; - -export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`; -export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`; - -// errors -export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; - -export const NYM_WEBSITE = 'https://nymtech.net'; - -export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one - -export const NYM_MIXNET_CONTRACT = - process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; -export const COSMOS_KIT_USE_CHAIN = process.env.NEXT_PUBLIC_COSMOS_KIT_USE_CHAIN || 'sandbox'; -export const WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID || ''; diff --git a/explorer-nextjs/app/api/index.ts b/explorer-nextjs/app/api/index.ts deleted file mode 100644 index ae190de9b9..0000000000 --- a/explorer-nextjs/app/api/index.ts +++ /dev/null @@ -1,217 +0,0 @@ -import keyBy from 'lodash/keyBy'; -import { - API_BASE_URL, - BLOCK_API, - COUNTRY_DATA_API, - UPTIME_STORY_API_GATEWAY, - MIXNODE_API, - MIXNODE_PING, - MIXNODES_API, - OVERVIEW_API, - UPTIME_STORY_API, - VALIDATORS_API, - SERVICE_PROVIDERS, - TEMP_UNSTABLE_NYM_NODES, - NYM_API_NODE_UPTIME, - NYM_API_NODE_PERFORMANCE, - TEMP_UNSTABLE_ACCOUNT, - LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API, -} from './constants'; - -import { - CountryDataResponse, - DelegationsResponse, - UniqDelegationsResponse, - GatewayReportResponse, - UptimeStoryResponse, - MixNodeDescriptionResponse, - MixNodeResponse, - MixNodeResponseItem, - MixnodeStatus, - MixNodeEconomicDynamicsStatsResponse, - StatsResponse, - StatusResponse, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - GatewayBondAnnotated, - GatewayBond, - DirectoryServiceProvider, - LocatedGateway, -} from '../typeDefs/explorer-api'; - -function getFromCache(key: string) { - const ts = Number(localStorage.getItem('ts')); - const hasExpired = Date.now() - ts > 5000; - const curr = localStorage.getItem(key); - if (curr && !hasExpired) { - return JSON.parse(curr); - } - return undefined; -} - -function storeInCache(key: string, data: any) { - localStorage.setItem(key, data); - localStorage.setItem('ts', Date.now().toString()); -} - -export class Api { - static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => { - const cache = getFromCache('overview-summary'); - if (cache) { - return cache; - } - const res = await fetch(`${OVERVIEW_API}/summary`); - const json: SummaryOverviewResponse = await res.json(); - - if (json.nymnodes?.roles) { - json.mixnodes.count += json.nymnodes.roles.mixnode; - json.gateways.count += json.nymnodes.roles.entry; - json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr); - } - - storeInCache('overview-summary', JSON.stringify(json)); - return json; - }; - - static fetchMixnodes = async (): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache('mixnodes'); - if (cachedMixnodes) { - return cachedMixnodes; - } - - const res = await fetch(LEGACY_MIXNODES_API); - const json = await res.json(); - storeInCache('mixnodes', JSON.stringify(json)); - return json; - }; - - static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache(`mixnodes-${status}`); - if (cachedMixnodes) { - return cachedMixnodes; - } - const res = await fetch(`${MIXNODES_API}/active-set/${status}`); - const json = await res.json(); - storeInCache(`mixnodes-${status}`, JSON.stringify(json)); - return json; - }; - - static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => { - const response = await fetch(`${MIXNODE_API}/${id}`); - - // when the mixnode is not found, returned undefined - if (response.status === 404) { - return undefined; - } - - return response.json(); - }; - - static fetchGateways = async (): Promise<LocatedGateway[]> => { - // const res = await fetch(GATEWAYS_API); - // const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); - // const res2 = await fetch(GATEWAYS_EXPLORER_API); - // const locatedGateways: LocatedGateway[] = await res2.json(); - // const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); - // return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ - // ...gateway_bond, - // node_performance, - // location: locatedGatewaysByOwner[gateway_bond.owner]?.location, - // })); - - const res = await fetch(LEGACY_GATEWAYS_API); - const locatedGateways: LocatedGateway[] = await res.json(); - return locatedGateways; - }; - - static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); - - static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); - - static fetchValidators = async (): Promise<ValidatorsResponse> => { - const res = await fetch(VALIDATORS_API); - const json = await res.json(); - return json.result; - }; - - static fetchBlock = async (): Promise<number> => { - const res = await fetch(BLOCK_API); - const json = await res.json(); - const { height } = json.result.block.header; - return height; - }; - - static fetchCountryData = async (): Promise<CountryDataResponse> => { - const result: CountryDataResponse = {}; - const res = await fetch(COUNTRY_DATA_API); - const json = await res.json(); - Object.keys(json).forEach((ISO3) => { - result[ISO3] = { ISO3, nodes: json[ISO3] }; - }); - return result; - }; - - static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations`)).json(); - - static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json(); - - static fetchStatsById = async (id: string): Promise<StatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/stats`)).json(); - - static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> => - (await fetch(`${MIXNODE_API}/${id}/description`)).json(); - - static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json(); - - static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json(); - - static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API}/${id}/history`)).json(); - - static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => { - const res = await fetch(SERVICE_PROVIDERS); - const json = await res.json(); - return json; - }; - - static fetchNodes = async () => { - const res = await fetch(TEMP_UNSTABLE_NYM_NODES); - const json = await res.json(); - return json; - } - - static fetchNodeById = async (id: number) => { - const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`); - const json = await res.json(); - return json; - } - - static fetchNymNodeUptimeHistoryById = async (id: number | string) => { - const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`) - const json = await res.json(); - return json; - } - - static fetchNymNodePerformanceById = async (id: number | string) => { - const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`) - const json = await res.json(); - return json; - } - - static fetchAccountById = async (id: string) => { - const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`); - const json = await res.json(); - return json; - } -} - -export const getEnvironment = (): Environment => { - const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env; - return matchEnv('sandbox') || matchEnv('qa') || 'mainnet'; -}; diff --git a/explorer-nextjs/app/assets/world-110m.json b/explorer-nextjs/app/assets/world-110m.json deleted file mode 100644 index 5c999c82ca..0000000000 --- a/explorer-nextjs/app/assets/world-110m.json +++ /dev/null @@ -1,39718 +0,0 @@ -{ - "type": "Topology", - "arcs": [ - [ - [ - 16814, - 15074 - ], - [ - 71, - -45 - ], - [ - 53, - 16 - ], - [ - 15, - 54 - ], - [ - 55, - 18 - ], - [ - 39, - 37 - ], - [ - 14, - 95 - ], - [ - 59, - 24 - ], - [ - 11, - 42 - ], - [ - 32, - -32 - ], - [ - 21, - -3 - ] - ], - [ - [ - 17184, - 15280 - ], - [ - 39, - -1 - ], - [ - 53, - -26 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 21, - -14 - ], - [ - 51, - 38 - ], - [ - 23, - -23 - ], - [ - 23, - 55 - ], - [ - 41, - -2 - ], - [ - 11, - 17 - ], - [ - 7, - 49 - ], - [ - 30, - 41 - ], - [ - 38, - -27 - ], - [ - -8, - -37 - ], - [ - 22, - -5 - ], - [ - -7, - -101 - ], - [ - 28, - -39 - ], - [ - 24, - 25 - ], - [ - 31, - 12 - ], - [ - 43, - 53 - ], - [ - 48, - -8 - ], - [ - 72, - -1 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - 13, - -34 - ] - ], - [ - [ - 17787, - 15252 - ], - [ - -41, - -13 - ], - [ - -35, - -23 - ], - [ - -80, - -14 - ], - [ - -75, - -25 - ], - [ - -41, - -52 - ], - [ - 17, - -51 - ], - [ - 8, - -60 - ], - [ - -35, - -50 - ], - [ - 3, - -46 - ], - [ - -19, - -43 - ], - [ - -67, - 4 - ], - [ - 28, - -80 - ], - [ - -45, - -30 - ], - [ - -29, - -73 - ], - [ - 4, - -72 - ], - [ - -28, - -33 - ], - [ - -26, - 11 - ], - [ - -53, - -16 - ], - [ - -7, - -33 - ], - [ - -52, - 0 - ], - [ - -39, - -68 - ], - [ - -3, - -102 - ], - [ - -90, - -50 - ], - [ - -49, - 10 - ], - [ - -14, - -26 - ], - [ - -42, - 15 - ], - [ - -69, - -17 - ], - [ - -117, - 61 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 63, - 109 - ], - [ - -6, - 77 - ], - [ - -52, - 20 - ], - [ - -6, - 76 - ], - [ - -23, - 96 - ], - [ - 30, - 66 - ], - [ - -30, - 17 - ], - [ - 19, - 88 - ], - [ - 28, - 149 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - -128, - -49 - ], - [ - -169, - 17 - ], - [ - -48, - 58 - ], - [ - -283, - -6 - ], - [ - -11, - -8 - ], - [ - -41, - 54 - ], - [ - -45, - 4 - ], - [ - -42, - -21 - ], - [ - -34, - -22 - ] - ], - [ - [ - 13365, - 8722 - ], - [ - -6, - 75 - ], - [ - 10, - 105 - ], - [ - 24, - 110 - ], - [ - 3, - 52 - ], - [ - 23, - 108 - ], - [ - 16, - 49 - ], - [ - 41, - 79 - ], - [ - 22, - 53 - ], - [ - 7, - 89 - ], - [ - -3, - 68 - ], - [ - -21, - 43 - ], - [ - -19, - 72 - ], - [ - -17, - 72 - ], - [ - 4, - 25 - ], - [ - 21, - 48 - ], - [ - -21, - 116 - ], - [ - -14, - 80 - ], - [ - -35, - 76 - ], - [ - 6, - 23 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - 29, - 16 - ], - [ - 20, - -2 - ], - [ - 25, - 15 - ], - [ - 206, - -2 - ], - [ - 17, - -89 - ], - [ - 20, - -72 - ], - [ - 16, - -39 - ], - [ - 27, - -63 - ], - [ - 46, - 10 - ], - [ - 23, - 17 - ], - [ - 38, - -17 - ], - [ - 11, - 30 - ], - [ - 17, - 70 - ], - [ - 43, - 4 - ], - [ - 4, - 21 - ], - [ - 36, - 1 - ], - [ - -6, - -44 - ], - [ - 84, - 2 - ], - [ - 1, - -76 - ], - [ - 15, - -46 - ], - [ - -11, - -73 - ], - [ - 5, - -73 - ], - [ - 24, - -45 - ], - [ - -4, - -143 - ], - [ - 17, - 11 - ], - [ - 30, - -3 - ], - [ - 44, - 18 - ], - [ - 31, - -7 - ] - ], - [ - [ - 14214, - 9486 - ], - [ - 8, - -37 - ], - [ - -8, - -58 - ], - [ - 12, - -56 - ], - [ - -10, - -45 - ], - [ - 6, - -42 - ], - [ - -146, - 2 - ], - [ - -3, - -382 - ], - [ - 47, - -98 - ], - [ - 46, - -75 - ] - ], - [ - [ - 13397, - 10103 - ], - [ - -19, - 90 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - 28, - 52 - ], - [ - 21, - 20 - ], - [ - 26, - -41 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - -25, - -26 - ], - [ - -11, - -30 - ], - [ - -3, - -53 - ], - [ - -17, - -12 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - -2, - -31 - ], - [ - -22, - -18 - ], - [ - -4, - -39 - ], - [ - -33, - -58 - ] - ], - [ - [ - 13952, - 15551 - ], - [ - -12, - 8 - ], - [ - -1, - 27 - ], - [ - -39, - 40 - ], - [ - -6, - 57 - ], - [ - 6, - 82 - ], - [ - 10, - 37 - ], - [ - -12, - 19 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -5, - 38 - ], - [ - 30, - 59 - ], - [ - 5, - -22 - ], - [ - 19, - 11 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 14, - -33 - ], - [ - 17, - -12 - ], - [ - 5, - -43 - ] - ], - [ - [ - 13983, - 15819 - ], - [ - -9, - -41 - ], - [ - 10, - -52 - ], - [ - 29, - -29 - ] - ], - [ - [ - 16143, - 13706 - ], - [ - 12, - 6 - ], - [ - 3, - -33 - ], - [ - 55, - 19 - ], - [ - 57, - -3 - ], - [ - 42, - -4 - ], - [ - 48, - 81 - ], - [ - 52, - 77 - ], - [ - 44, - 74 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 13, - -41 - ] - ], - [ - [ - 16469, - 13882 - ], - [ - 10, - -95 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - -36, - 0 - ], - [ - -5, - -78 - ], - [ - 12, - -17 - ], - [ - -32, - -24 - ], - [ - 0, - -49 - ], - [ - -20, - -49 - ], - [ - -2, - -49 - ] - ], - [ - [ - 16396, - 13521 - ], - [ - -14, - -25 - ], - [ - -210, - 61 - ], - [ - -26, - 121 - ], - [ - -3, - 28 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -42, - 4 - ], - [ - -75, - 0 - ], - [ - 0, - 267 - ] - ], - [ - [ - 7763, - 4482 - ], - [ - 27, - -55 - ], - [ - 35, - -90 - ], - [ - 90, - -72 - ], - [ - 98, - -30 - ], - [ - -31, - -60 - ], - [ - -67, - -6 - ], - [ - -35, - 42 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -64, - 19 - ], - [ - -169, - 16 - ], - [ - -28, - 70 - ], - [ - 1, - 90 - ], - [ - -47, - -8 - ], - [ - -24, - 43 - ], - [ - -6, - 128 - ], - [ - 53, - 52 - ], - [ - 22, - 76 - ], - [ - -8, - 61 - ], - [ - 37, - 102 - ], - [ - 26, - 159 - ], - [ - -8, - 71 - ], - [ - 31, - 22 - ], - [ - -8, - 46 - ], - [ - -32, - 24 - ], - [ - 23, - 50 - ], - [ - -32, - 46 - ], - [ - -16, - 138 - ], - [ - 28, - 24 - ], - [ - -12, - 147 - ], - [ - 17, - 122 - ], - [ - 18, - 107 - ], - [ - 42, - 44 - ], - [ - -21, - 117 - ], - [ - 0, - 110 - ], - [ - 52, - 79 - ], - [ - -1, - 100 - ], - [ - 40, - 117 - ], - [ - 0, - 110 - ], - [ - -18, - 22 - ], - [ - -32, - 207 - ], - [ - 43, - 124 - ], - [ - -7, - 116 - ], - [ - 25, - 109 - ], - [ - 46, - 113 - ], - [ - 49, - 74 - ], - [ - -21, - 47 - ], - [ - 14, - 39 - ], - [ - -2, - 200 - ], - [ - 76, - 59 - ], - [ - 24, - 125 - ], - [ - -8, - 30 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - 58, - 108 - ], - [ - 91, - -29 - ], - [ - 41, - -87 - ], - [ - 27, - 97 - ], - [ - 80, - -5 - ], - [ - 11, - -26 - ] - ], - [ - [ - 8178, - 8128 - ], - [ - 128, - -196 - ], - [ - 57, - -18 - ], - [ - 85, - -89 - ], - [ - 72, - -47 - ], - [ - 10, - -52 - ], - [ - -69, - -183 - ], - [ - 71, - -32 - ], - [ - 78, - -19 - ], - [ - 55, - 20 - ], - [ - 63, - 91 - ], - [ - 12, - 106 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 34, - 23 - ], - [ - 35, - -69 - ], - [ - -1, - -96 - ], - [ - -59, - -66 - ], - [ - -47, - -49 - ], - [ - -78, - -116 - ], - [ - -93, - -164 - ] - ], - [ - [ - 8531, - 7172 - ], - [ - -18, - -96 - ], - [ - -19, - -123 - ], - [ - 1, - -120 - ], - [ - -15, - -26 - ], - [ - -5, - -78 - ] - ], - [ - [ - 8475, - 6729 - ], - [ - -5, - -63 - ], - [ - 88, - -102 - ], - [ - -9, - -83 - ], - [ - 43, - -52 - ], - [ - -3, - -59 - ], - [ - -67, - -154 - ], - [ - -103, - -64 - ], - [ - -140, - -25 - ], - [ - -77, - 12 - ], - [ - 15, - -71 - ], - [ - -14, - -90 - ], - [ - 12, - -61 - ], - [ - -41, - -42 - ], - [ - -72, - -17 - ], - [ - -67, - 44 - ], - [ - -27, - -31 - ], - [ - 10, - -119 - ], - [ - 47, - -37 - ], - [ - 38, - 38 - ], - [ - 21, - -62 - ], - [ - -64, - -37 - ], - [ - -56, - -75 - ], - [ - -10, - -121 - ], - [ - -17, - -64 - ], - [ - -66, - 0 - ], - [ - -54, - -62 - ], - [ - -20, - -90 - ], - [ - 68, - -87 - ], - [ - 67, - -25 - ], - [ - -24, - -107 - ], - [ - -83, - -68 - ], - [ - -45, - -141 - ], - [ - -63, - -47 - ], - [ - -29, - -56 - ], - [ - 22, - -125 - ], - [ - 47, - -69 - ], - [ - -30, - 6 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - 96, - 19 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 15, - -32 - ], - [ - 26, - -21 - ], - [ - -14, - -30 - ], - [ - 38, - -41 - ], - [ - -20, - -38 - ], - [ - 29, - -33 - ], - [ - 32, - -19 - ], - [ - 1, - -84 - ] - ], - [ - [ - 15789, - 15448 - ], - [ - -25, - -3 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -28, - 69 - ], - [ - 0, - 19 - ], - [ - -31, - 0 - ], - [ - -20, - 32 - ], - [ - -15, - -3 - ] - ], - [ - [ - 15670, - 15562 - ], - [ - -27, - 35 - ], - [ - -52, - 29 - ], - [ - 6, - 59 - ], - [ - -11, - 42 - ] - ], - [ - [ - 8395, - 1195 - ], - [ - -21, - -61 - ], - [ - -20, - -54 - ], - [ - -146, - 16 - ], - [ - -156, - -7 - ], - [ - -87, - 40 - ], - [ - 0, - 5 - ], - [ - -38, - 35 - ], - [ - 157, - -5 - ], - [ - 150, - -11 - ], - [ - 52, - 49 - ], - [ - 36, - 42 - ], - [ - 73, - -49 - ] - ], - [ - [ - 1449, - 1260 - ], - [ - -133, - -16 - ], - [ - -92, - 42 - ], - [ - -41, - 42 - ], - [ - -3, - 7 - ], - [ - -45, - 33 - ], - [ - 43, - 45 - ], - [ - 129, - -19 - ], - [ - 70, - -38 - ], - [ - 53, - -42 - ], - [ - 19, - -54 - ] - ], - [ - [ - 9400, - 1434 - ], - [ - 86, - -52 - ], - [ - 30, - -73 - ], - [ - 8, - -51 - ], - [ - 3, - -61 - ], - [ - -108, - -38 - ], - [ - -113, - -31 - ], - [ - -131, - -28 - ], - [ - -147, - -23 - ], - [ - -165, - 7 - ], - [ - -91, - 40 - ], - [ - 12, - 49 - ], - [ - 149, - 33 - ], - [ - 60, - 40 - ], - [ - 44, - 52 - ], - [ - 31, - 44 - ], - [ - 42, - 43 - ], - [ - 45, - 49 - ], - [ - 36, - 0 - ], - [ - 104, - 26 - ], - [ - 105, - -26 - ] - ], - [ - [ - 4098, - 1979 - ], - [ - 90, - -18 - ], - [ - 83, - 21 - ], - [ - -39, - -43 - ], - [ - -66, - -30 - ], - [ - -97, - 9 - ], - [ - -69, - 43 - ], - [ - 15, - 40 - ], - [ - 83, - -22 - ] - ], - [ - [ - 3795, - 1982 - ], - [ - 106, - -47 - ], - [ - -41, - 4 - ], - [ - -90, - 12 - ], - [ - -95, - 33 - ], - [ - 50, - 26 - ], - [ - 70, - -28 - ] - ], - [ - [ - 5648, - 2167 - ], - [ - 76, - -16 - ], - [ - 77, - 14 - ], - [ - 41, - -68 - ], - [ - -55, - 9 - ], - [ - -85, - -4 - ], - [ - -86, - 4 - ], - [ - -94, - -7 - ], - [ - -71, - 24 - ], - [ - -37, - 49 - ], - [ - 44, - 21 - ], - [ - 89, - -16 - ], - [ - 101, - -10 - ] - ], - [ - [ - 7776, - 2285 - ], - [ - 8, - -54 - ], - [ - -12, - -47 - ], - [ - -19, - -45 - ], - [ - -82, - -16 - ], - [ - -78, - -24 - ], - [ - -92, - 2 - ], - [ - 35, - 47 - ], - [ - -82, - -16 - ], - [ - -78, - -17 - ], - [ - -53, - 36 - ], - [ - -5, - 49 - ], - [ - 77, - 47 - ], - [ - 48, - 14 - ], - [ - 80, - -5 - ], - [ - 21, - 62 - ], - [ - 4, - 44 - ], - [ - -2, - 97 - ], - [ - 40, - 56 - ], - [ - 64, - 19 - ], - [ - 37, - -45 - ], - [ - 17, - -44 - ], - [ - 30, - -55 - ], - [ - 23, - -51 - ], - [ - 19, - -54 - ] - ], - [ - [ - 8462, - 3101 - ], - [ - -30, - -26 - ], - [ - -52, - 19 - ], - [ - -58, - -12 - ], - [ - -47, - -28 - ], - [ - -51, - -31 - ], - [ - -34, - -35 - ], - [ - -10, - -47 - ], - [ - 4, - -45 - ], - [ - 33, - -40 - ], - [ - -48, - -28 - ], - [ - -65, - -9 - ], - [ - -38, - -40 - ], - [ - -41, - -38 - ], - [ - -44, - -51 - ], - [ - -11, - -45 - ], - [ - 25, - -50 - ], - [ - 37, - -37 - ], - [ - 57, - -28 - ], - [ - 53, - -38 - ], - [ - 29, - -47 - ], - [ - 15, - -45 - ], - [ - 20, - -47 - ], - [ - 33, - -40 - ], - [ - 21, - -44 - ], - [ - 9, - -111 - ], - [ - 21, - -44 - ], - [ - 5, - -47 - ], - [ - 22, - -47 - ], - [ - -10, - -64 - ], - [ - -38, - -49 - ], - [ - -41, - -40 - ], - [ - -93, - -17 - ], - [ - -31, - -42 - ], - [ - -42, - -40 - ], - [ - -106, - -45 - ], - [ - -92, - -18 - ], - [ - -88, - -26 - ], - [ - -94, - -26 - ], - [ - -56, - -50 - ], - [ - -112, - -4 - ], - [ - -123, - 4 - ], - [ - -110, - -9 - ], - [ - -118, - 0 - ], - [ - 22, - -47 - ], - [ - 107, - -21 - ], - [ - 77, - -33 - ], - [ - 44, - -42 - ], - [ - -78, - -38 - ], - [ - -120, - 12 - ], - [ - -100, - -31 - ], - [ - -4, - -49 - ], - [ - -2, - -47 - ], - [ - 82, - -40 - ], - [ - 15, - -45 - ], - [ - 88, - -44 - ], - [ - 148, - -19 - ], - [ - 125, - -33 - ], - [ - 100, - -38 - ], - [ - 127, - -37 - ], - [ - 173, - -19 - ], - [ - 171, - -33 - ], - [ - 119, - -35 - ], - [ - 130, - -40 - ], - [ - 68, - -57 - ], - [ - 34, - -44 - ], - [ - 85, - 42 - ], - [ - 114, - 35 - ], - [ - 122, - 38 - ], - [ - 144, - 30 - ], - [ - 125, - 33 - ], - [ - 173, - 3 - ], - [ - 171, - -17 - ], - [ - 140, - -28 - ], - [ - 45, - 52 - ], - [ - 97, - 35 - ], - [ - 177, - 2 - ], - [ - 137, - 26 - ], - [ - 131, - 26 - ], - [ - 145, - 16 - ], - [ - 154, - 22 - ], - [ - 108, - 30 - ], - [ - -49, - 42 - ], - [ - -30, - 43 - ], - [ - 0, - 44 - ], - [ - -135, - -4 - ], - [ - -143, - -19 - ], - [ - -137, - 0 - ], - [ - -19, - 45 - ], - [ - 10, - 89 - ], - [ - 31, - 26 - ], - [ - 100, - 28 - ], - [ - 117, - 28 - ], - [ - 85, - 35 - ], - [ - 84, - 36 - ], - [ - 63, - 47 - ], - [ - 96, - 21 - ], - [ - 94, - 16 - ], - [ - 48, - 10 - ], - [ - 108, - 4 - ], - [ - 102, - 17 - ], - [ - 86, - 23 - ], - [ - 85, - 29 - ], - [ - 76, - 28 - ], - [ - 97, - 37 - ], - [ - 61, - 40 - ], - [ - 66, - 36 - ], - [ - 20, - 47 - ], - [ - -73, - 28 - ], - [ - 24, - 49 - ], - [ - 47, - 38 - ], - [ - 72, - 23 - ], - [ - 77, - 29 - ], - [ - 71, - 37 - ], - [ - 54, - 47 - ], - [ - 34, - 57 - ], - [ - 51, - 33 - ], - [ - 83, - -7 - ], - [ - 34, - -40 - ], - [ - 83, - -5 - ], - [ - 3, - 45 - ], - [ - 36, - 47 - ], - [ - 75, - -12 - ], - [ - 18, - -45 - ], - [ - 83, - -7 - ], - [ - 90, - 21 - ], - [ - 87, - 14 - ], - [ - 80, - -7 - ], - [ - 30, - -49 - ], - [ - 76, - 40 - ], - [ - 71, - 21 - ], - [ - 79, - 16 - ], - [ - 78, - 17 - ], - [ - 71, - 28 - ], - [ - 78, - 19 - ], - [ - 60, - 26 - ], - [ - 42, - 42 - ], - [ - 52, - -30 - ], - [ - 72, - 16 - ], - [ - 51, - -56 - ], - [ - 40, - -43 - ], - [ - 79, - 24 - ], - [ - 31, - 47 - ], - [ - 71, - 33 - ], - [ - 92, - -7 - ], - [ - 27, - -45 - ], - [ - 57, - 45 - ], - [ - 75, - 14 - ], - [ - 82, - 4 - ], - [ - 74, - -2 - ], - [ - 78, - -14 - ], - [ - 75, - -7 - ], - [ - 33, - -40 - ], - [ - 45, - -35 - ], - [ - 76, - 21 - ], - [ - 82, - 5 - ], - [ - 79, - 0 - ], - [ - 78, - 2 - ], - [ - 70, - 16 - ], - [ - 74, - 15 - ], - [ - 61, - 32 - ], - [ - 65, - 22 - ], - [ - 71, - 11 - ], - [ - 54, - 33 - ], - [ - 38, - 66 - ], - [ - 40, - 40 - ], - [ - 72, - -19 - ], - [ - 27, - -42 - ], - [ - 60, - -28 - ], - [ - 73, - 9 - ], - [ - 49, - -42 - ], - [ - 52, - -31 - ], - [ - 71, - 28 - ], - [ - 24, - 52 - ], - [ - 63, - 21 - ], - [ - 72, - 40 - ], - [ - 69, - 17 - ], - [ - 82, - 23 - ], - [ - 54, - 26 - ], - [ - 58, - 28 - ], - [ - 54, - 26 - ], - [ - 66, - -14 - ], - [ - 63, - 42 - ], - [ - 45, - 33 - ], - [ - 65, - -2 - ], - [ - 57, - 28 - ], - [ - 14, - 42 - ], - [ - 59, - 33 - ], - [ - 57, - 24 - ], - [ - 70, - 19 - ], - [ - 64, - 9 - ], - [ - 61, - -7 - ], - [ - 66, - -12 - ], - [ - 56, - -33 - ], - [ - 7, - -51 - ], - [ - 61, - -40 - ], - [ - 42, - -33 - ], - [ - 84, - -14 - ], - [ - 46, - -33 - ], - [ - 58, - -33 - ], - [ - 66, - -7 - ], - [ - 56, - 23 - ], - [ - 60, - 50 - ], - [ - 66, - -26 - ], - [ - 68, - -14 - ], - [ - 66, - -14 - ], - [ - 68, - -10 - ], - [ - 70, - 0 - ], - [ - 57, - -124 - ], - [ - -3, - -31 - ], - [ - -8, - -54 - ], - [ - -67, - -31 - ], - [ - -54, - -44 - ], - [ - 9, - -47 - ], - [ - 78, - 2 - ], - [ - -10, - -47 - ], - [ - -35, - -45 - ], - [ - -33, - -49 - ], - [ - 53, - -38 - ], - [ - 81, - -11 - ], - [ - 81, - 21 - ], - [ - 38, - 47 - ], - [ - 23, - 45 - ], - [ - 38, - 37 - ], - [ - 44, - 35 - ], - [ - 18, - 43 - ], - [ - 36, - 58 - ], - [ - 44, - 12 - ], - [ - 79, - 5 - ], - [ - 70, - 14 - ], - [ - 71, - 19 - ], - [ - 34, - 47 - ], - [ - 21, - 45 - ], - [ - 47, - 44 - ], - [ - 69, - 31 - ], - [ - 58, - 23 - ], - [ - 39, - 40 - ], - [ - 39, - 21 - ], - [ - 51, - 19 - ], - [ - 69, - -12 - ], - [ - 63, - 12 - ], - [ - 68, - 14 - ], - [ - 77, - -7 - ], - [ - 50, - 33 - ], - [ - 36, - 80 - ], - [ - 26, - -33 - ], - [ - 33, - -56 - ], - [ - 58, - -24 - ], - [ - 67, - -9 - ], - [ - 67, - 14 - ], - [ - 71, - -9 - ], - [ - 66, - -3 - ], - [ - 43, - 12 - ], - [ - 59, - -7 - ], - [ - 53, - -26 - ], - [ - 63, - 16 - ], - [ - 75, - 0 - ], - [ - 64, - 17 - ], - [ - 73, - -17 - ], - [ - 46, - 40 - ], - [ - 36, - 40 - ], - [ - 47, - 33 - ], - [ - 88, - 90 - ], - [ - 45, - -17 - ], - [ - 53, - -33 - ], - [ - 46, - -42 - ], - [ - 89, - -73 - ], - [ - 69, - -2 - ], - [ - 64, - 0 - ], - [ - 75, - 14 - ], - [ - 75, - 16 - ], - [ - 57, - 33 - ], - [ - 48, - 35 - ], - [ - 78, - 5 - ], - [ - 52, - 26 - ], - [ - 54, - -23 - ], - [ - 36, - -38 - ], - [ - 49, - -38 - ], - [ - 76, - 5 - ], - [ - 48, - -31 - ], - [ - 83, - -30 - ], - [ - 88, - -12 - ], - [ - 72, - 10 - ], - [ - 55, - 37 - ], - [ - 46, - 38 - ], - [ - 63, - 9 - ], - [ - 63, - -16 - ], - [ - 72, - -12 - ], - [ - 66, - 19 - ], - [ - 63, - 0 - ], - [ - 61, - -12 - ], - [ - 64, - -12 - ], - [ - 63, - 21 - ], - [ - 75, - 19 - ], - [ - 71, - 5 - ], - [ - 79, - 0 - ], - [ - 64, - 12 - ], - [ - 63, - 9 - ], - [ - 19, - 59 - ], - [ - 3, - 49 - ], - [ - 44, - -33 - ], - [ - 12, - -54 - ], - [ - 23, - -49 - ], - [ - 29, - -40 - ], - [ - 59, - -21 - ], - [ - 79, - 7 - ], - [ - 91, - 2 - ], - [ - 63, - 7 - ], - [ - 92, - 0 - ], - [ - 65, - 3 - ], - [ - 92, - -5 - ], - [ - 77, - -10 - ], - [ - 50, - -37 - ], - [ - -14, - -45 - ], - [ - 45, - -35 - ], - [ - 75, - -28 - ], - [ - 78, - -31 - ], - [ - 90, - -21 - ], - [ - 94, - -19 - ], - [ - 71, - -19 - ], - [ - 79, - -2 - ], - [ - 45, - 40 - ], - [ - 62, - -33 - ], - [ - 53, - -38 - ], - [ - 62, - -28 - ], - [ - 84, - -12 - ], - [ - 81, - -14 - ], - [ - 34, - -47 - ], - [ - 79, - -28 - ], - [ - 53, - -42 - ], - [ - 78, - -19 - ], - [ - 81, - 2 - ], - [ - 75, - -7 - ], - [ - 83, - 3 - ], - [ - 83, - -10 - ], - [ - 78, - -16 - ], - [ - 73, - -28 - ], - [ - 72, - -24 - ], - [ - 49, - -35 - ], - [ - -8, - -47 - ], - [ - -37, - -42 - ], - [ - -31, - -55 - ], - [ - -25, - -42 - ], - [ - -33, - -49 - ], - [ - -91, - -19 - ], - [ - -41, - -42 - ], - [ - -90, - -26 - ], - [ - -32, - -47 - ], - [ - -47, - -45 - ], - [ - -51, - -38 - ], - [ - -29, - -49 - ], - [ - -17, - -45 - ], - [ - -7, - -54 - ], - [ - 1, - -44 - ], - [ - 40, - -47 - ], - [ - 15, - -45 - ], - [ - 32, - -42 - ], - [ - 130, - -17 - ], - [ - 27, - -51 - ], - [ - -125, - -19 - ], - [ - -107, - -26 - ], - [ - -132, - -5 - ], - [ - -59, - -68 - ], - [ - -12, - -56 - ], - [ - -30, - -45 - ], - [ - -37, - -45 - ], - [ - 93, - -40 - ], - [ - 35, - -49 - ], - [ - 60, - -45 - ], - [ - 85, - -40 - ], - [ - 97, - -37 - ], - [ - 105, - -38 - ], - [ - 160, - -38 - ], - [ - 35, - -58 - ], - [ - 201, - -26 - ], - [ - 14, - -9 - ], - [ - 52, - -36 - ], - [ - 192, - 31 - ], - [ - 160, - -38 - ], - [ - 120, - -29 - ], - [ - 0, - -634 - ], - [ - -25095, - 0 - ], - [ - 0, - 634 - ], - [ - 4, - -1 - ], - [ - 62, - 70 - ], - [ - 125, - -38 - ], - [ - 8, - 5 - ], - [ - 74, - 38 - ], - [ - 10, - -1 - ], - [ - 8, - -1 - ], - [ - 101, - -50 - ], - [ - 88, - 50 - ], - [ - 16, - 6 - ], - [ - 204, - 22 - ], - [ - 67, - -28 - ], - [ - 33, - -15 - ], - [ - 105, - -40 - ], - [ - 198, - -30 - ], - [ - 157, - -38 - ], - [ - 269, - -28 - ], - [ - 200, - 33 - ], - [ - 297, - -24 - ], - [ - 168, - -37 - ], - [ - 184, - 35 - ], - [ - 194, - 33 - ], - [ - 15, - 56 - ], - [ - -275, - 5 - ], - [ - -225, - 28 - ], - [ - -59, - 47 - ], - [ - -187, - 26 - ], - [ - 13, - 54 - ], - [ - 25, - 50 - ], - [ - 26, - 44 - ], - [ - -13, - 50 - ], - [ - -116, - 33 - ], - [ - -54, - 42 - ], - [ - -107, - 37 - ], - [ - 169, - -7 - ], - [ - 161, - 19 - ], - [ - 101, - -40 - ], - [ - 124, - 36 - ], - [ - 115, - 44 - ], - [ - 56, - 40 - ], - [ - -25, - 50 - ], - [ - -90, - 32 - ], - [ - -102, - 36 - ], - [ - -143, - 7 - ], - [ - -126, - 16 - ], - [ - -135, - 12 - ], - [ - -45, - 45 - ], - [ - -90, - 37 - ], - [ - -55, - 43 - ], - [ - -22, - 136 - ], - [ - 34, - -12 - ], - [ - 63, - -37 - ], - [ - 115, - 11 - ], - [ - 110, - 17 - ], - [ - 58, - -52 - ], - [ - 110, - 12 - ], - [ - 93, - 26 - ], - [ - 87, - 33 - ], - [ - 80, - 39 - ], - [ - 105, - 12 - ], - [ - -3, - 45 - ], - [ - -24, - 45 - ], - [ - 20, - 42 - ], - [ - 90, - 21 - ], - [ - 41, - -40 - ], - [ - 107, - 24 - ], - [ - 80, - 30 - ], - [ - 100, - 3 - ], - [ - 94, - 11 - ], - [ - 94, - 28 - ], - [ - 75, - 26 - ], - [ - 85, - 26 - ], - [ - 55, - -7 - ], - [ - 47, - -9 - ], - [ - 104, - 16 - ], - [ - 93, - -21 - ], - [ - 95, - 2 - ], - [ - 92, - 17 - ], - [ - 94, - -12 - ], - [ - 104, - -12 - ], - [ - 97, - 5 - ], - [ - 101, - -2 - ], - [ - 104, - -3 - ], - [ - 95, - 5 - ], - [ - 71, - 35 - ], - [ - 85, - 19 - ], - [ - 87, - -26 - ], - [ - 84, - 21 - ], - [ - 75, - 43 - ], - [ - 45, - -38 - ], - [ - 24, - -42 - ], - [ - 45, - -40 - ], - [ - 73, - 35 - ], - [ - 83, - -45 - ], - [ - 94, - -14 - ], - [ - 81, - -33 - ], - [ - 98, - 7 - ], - [ - 89, - 22 - ], - [ - 105, - -5 - ], - [ - 94, - -17 - ], - [ - 96, - -21 - ], - [ - 37, - 52 - ], - [ - -46, - 40 - ], - [ - -34, - 42 - ], - [ - -90, - 10 - ], - [ - -39, - 44 - ], - [ - -15, - 45 - ], - [ - -25, - 89 - ], - [ - 53, - -16 - ], - [ - 92, - -7 - ], - [ - 90, - 7 - ], - [ - 82, - -19 - ], - [ - 71, - -35 - ], - [ - 30, - -42 - ], - [ - 94, - -8 - ], - [ - 90, - 17 - ], - [ - 96, - 23 - ], - [ - 86, - 15 - ], - [ - 71, - -29 - ], - [ - 93, - 10 - ], - [ - 60, - 91 - ], - [ - 56, - -54 - ], - [ - 80, - -21 - ], - [ - 88, - 12 - ], - [ - 57, - -47 - ], - [ - 91, - -5 - ], - [ - 85, - -14 - ], - [ - 83, - -26 - ], - [ - 55, - 45 - ], - [ - 27, - 42 - ], - [ - 70, - -47 - ], - [ - 95, - 12 - ], - [ - 71, - -26 - ], - [ - 48, - -40 - ], - [ - 93, - 12 - ], - [ - 72, - 26 - ], - [ - 71, - 30 - ], - [ - 85, - 17 - ], - [ - 98, - 14 - ], - [ - 89, - 16 - ], - [ - 68, - 26 - ], - [ - 41, - 38 - ], - [ - 17, - 52 - ], - [ - -8, - 49 - ], - [ - -22, - 47 - ], - [ - -25, - 47 - ], - [ - -22, - 47 - ], - [ - -18, - 42 - ], - [ - -4, - 47 - ], - [ - 7, - 47 - ], - [ - 33, - 45 - ], - [ - 27, - 49 - ], - [ - 11, - 47 - ], - [ - -13, - 52 - ], - [ - -9, - 47 - ], - [ - 35, - 54 - ], - [ - 38, - 35 - ], - [ - 45, - 45 - ], - [ - 48, - 38 - ], - [ - 56, - 35 - ], - [ - 27, - 52 - ], - [ - 38, - 33 - ], - [ - 44, - 30 - ], - [ - 67, - 7 - ], - [ - 43, - 38 - ], - [ - 50, - 23 - ], - [ - 57, - 14 - ], - [ - 50, - 31 - ], - [ - 40, - 38 - ], - [ - 55, - 14 - ], - [ - 41, - -31 - ], - [ - -26, - -40 - ], - [ - -71, - -35 - ] - ], - [ - [ - 17353, - 4964 - ], - [ - 45, - -38 - ], - [ - 66, - -15 - ], - [ - 2, - -23 - ], - [ - -19, - -54 - ], - [ - -107, - -8 - ], - [ - -2, - 64 - ], - [ - 10, - 49 - ], - [ - 5, - 25 - ] - ], - [ - [ - 22683, - 5903 - ], - [ - 67, - -41 - ], - [ - 38, - 16 - ], - [ - 55, - 23 - ], - [ - 41, - -8 - ], - [ - 5, - -142 - ], - [ - -23, - -41 - ], - [ - -8, - -97 - ], - [ - -24, - 33 - ], - [ - -48, - -84 - ], - [ - -15, - 7 - ], - [ - -43, - 4 - ], - [ - -43, - 102 - ], - [ - -9, - 79 - ], - [ - -40, - 105 - ], - [ - 1, - 55 - ], - [ - 46, - -11 - ] - ], - [ - [ - 22555, - 9146 - ], - [ - 25, - -94 - ], - [ - 45, - 45 - ], - [ - 23, - -51 - ], - [ - 33, - -47 - ], - [ - -7, - -53 - ], - [ - 15, - -103 - ], - [ - 11, - -59 - ], - [ - 17, - -15 - ], - [ - 19, - -103 - ], - [ - -7, - -62 - ], - [ - 23, - -81 - ], - [ - 75, - -63 - ], - [ - 50, - -57 - ], - [ - 46, - -52 - ], - [ - -9, - -29 - ], - [ - 40, - -75 - ], - [ - 27, - -130 - ], - [ - 28, - 26 - ], - [ - 28, - -52 - ], - [ - 17, - 19 - ], - [ - 12, - -128 - ], - [ - 50, - -73 - ], - [ - 32, - -46 - ], - [ - 55, - -97 - ], - [ - 19, - -97 - ], - [ - 2, - -68 - ], - [ - -5, - -74 - ], - [ - 34, - -102 - ], - [ - -4, - -106 - ], - [ - -12, - -56 - ], - [ - -19, - -107 - ], - [ - 1, - -69 - ], - [ - -14, - -86 - ], - [ - -30, - -109 - ], - [ - -52, - -59 - ], - [ - -26, - -93 - ], - [ - -23, - -59 - ], - [ - -20, - -104 - ], - [ - -27, - -59 - ], - [ - -18, - -90 - ], - [ - -9, - -83 - ], - [ - 4, - -38 - ], - [ - -40, - -41 - ], - [ - -78, - -5 - ], - [ - -65, - -49 - ], - [ - -32, - -46 - ], - [ - -42, - -52 - ], - [ - -58, - 53 - ], - [ - -42, - 21 - ], - [ - 10, - 63 - ], - [ - -38, - -23 - ], - [ - -61, - -87 - ], - [ - -60, - 33 - ], - [ - -39, - 19 - ], - [ - -40, - 8 - ], - [ - -68, - 35 - ], - [ - -45, - 74 - ], - [ - -13, - 91 - ], - [ - -16, - 61 - ], - [ - -34, - 48 - ], - [ - -67, - 15 - ], - [ - 23, - 58 - ], - [ - -17, - 89 - ], - [ - -34, - -83 - ], - [ - -62, - -22 - ], - [ - 36, - 66 - ], - [ - 11, - 70 - ], - [ - 27, - 58 - ], - [ - -6, - 89 - ], - [ - -57, - -102 - ], - [ - -43, - -41 - ], - [ - -27, - -96 - ], - [ - -54, - 50 - ], - [ - 2, - 63 - ], - [ - -44, - 87 - ], - [ - -37, - 45 - ], - [ - 14, - 28 - ], - [ - -90, - 73 - ], - [ - -49, - 3 - ], - [ - -67, - 59 - ], - [ - -125, - -12 - ], - [ - -90, - -43 - ], - [ - -79, - -40 - ], - [ - -67, - 8 - ], - [ - -74, - -61 - ], - [ - -60, - -28 - ], - [ - -14, - -63 - ], - [ - -25, - -49 - ], - [ - -60, - -2 - ], - [ - -43, - -11 - ], - [ - -62, - 22 - ], - [ - -50, - -13 - ], - [ - -48, - -6 - ], - [ - -41, - -64 - ], - [ - -21, - 6 - ], - [ - -35, - -34 - ], - [ - -33, - -38 - ], - [ - -51, - 4 - ], - [ - -47, - 0 - ], - [ - -74, - 77 - ], - [ - -37, - 23 - ], - [ - 1, - 68 - ], - [ - 35, - 17 - ], - [ - 12, - 27 - ], - [ - -3, - 43 - ], - [ - 9, - 84 - ], - [ - -8, - 71 - ], - [ - -37, - 121 - ], - [ - -11, - 68 - ], - [ - 3, - 69 - ], - [ - -28, - 78 - ], - [ - -2, - 35 - ], - [ - -31, - 48 - ], - [ - -8, - 94 - ], - [ - -40, - 95 - ], - [ - -10, - 51 - ], - [ - 31, - -52 - ], - [ - -24, - 111 - ], - [ - 35, - -34 - ], - [ - 20, - -47 - ], - [ - -1, - 62 - ], - [ - -34, - 94 - ], - [ - -7, - 38 - ], - [ - -16, - 36 - ], - [ - 8, - 69 - ], - [ - 14, - 30 - ], - [ - 9, - 60 - ], - [ - -7, - 70 - ], - [ - 29, - 86 - ], - [ - 5, - -91 - ], - [ - 29, - 82 - ], - [ - 57, - 40 - ], - [ - 34, - 52 - ], - [ - 53, - 44 - ], - [ - 32, - 9 - ], - [ - 19, - -15 - ], - [ - 55, - 45 - ], - [ - 42, - 13 - ], - [ - 11, - 27 - ], - [ - 18, - 10 - ], - [ - 39, - -2 - ], - [ - 73, - 35 - ], - [ - 38, - 53 - ], - [ - 18, - 64 - ], - [ - 41, - 61 - ], - [ - 3, - 48 - ], - [ - 2, - 65 - ], - [ - 49, - 102 - ], - [ - 29, - -103 - ], - [ - 30, - 23 - ], - [ - -25, - 57 - ], - [ - 22, - 58 - ], - [ - 30, - -26 - ], - [ - 9, - 92 - ], - [ - 38, - 59 - ], - [ - 17, - 47 - ], - [ - 35, - 20 - ], - [ - 1, - 34 - ], - [ - 30, - -14 - ], - [ - 2, - 30 - ], - [ - 30, - 17 - ], - [ - 34, - 16 - ], - [ - 52, - -55 - ], - [ - 38, - -71 - ], - [ - 44, - 0 - ], - [ - 44, - -12 - ], - [ - -15, - 66 - ], - [ - 34, - 96 - ], - [ - 31, - 32 - ], - [ - -11, - 30 - ], - [ - 31, - 68 - ], - [ - 42, - 43 - ], - [ - 36, - -15 - ], - [ - 58, - 23 - ], - [ - -1, - 61 - ], - [ - -51, - 40 - ], - [ - 37, - 17 - ], - [ - 46, - -30 - ], - [ - 37, - -49 - ], - [ - 59, - -31 - ], - [ - 20, - 13 - ], - [ - 43, - -37 - ], - [ - 41, - 34 - ], - [ - 26, - -10 - ], - [ - 16, - 23 - ], - [ - 32, - -60 - ], - [ - -18, - -64 - ], - [ - -27, - -48 - ], - [ - -24, - -4 - ], - [ - 8, - -48 - ], - [ - -20, - -60 - ], - [ - -25, - -59 - ], - [ - 5, - -34 - ], - [ - 55, - -66 - ], - [ - 54, - -39 - ], - [ - 36, - -41 - ], - [ - 50, - -71 - ], - [ - 20, - 0 - ], - [ - 37, - -31 - ], - [ - 10, - -37 - ], - [ - 67, - -41 - ], - [ - 46, - 41 - ], - [ - 13, - 65 - ], - [ - 14, - 53 - ], - [ - 9, - 66 - ], - [ - 21, - 95 - ], - [ - -9, - 58 - ], - [ - 5, - 35 - ], - [ - -8, - 69 - ], - [ - 9, - 90 - ], - [ - 13, - 25 - ], - [ - -11, - 40 - ], - [ - 17, - 63 - ], - [ - 13, - 66 - ], - [ - 2, - 34 - ], - [ - 26, - 45 - ], - [ - 20, - -58 - ], - [ - 5, - -76 - ], - [ - 17, - -14 - ], - [ - 3, - -51 - ], - [ - 25, - -61 - ], - [ - 5, - -67 - ], - [ - -2, - -44 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - -5, - -50 - ], - [ - -39, - 0 - ], - [ - 13, - -26 - ], - [ - -23, - -77 - ] - ], - [ - [ - 13677, - 16418 - ], - [ - -13, - -20 - ], - [ - -61, - -3 - ], - [ - -35, - -27 - ], - [ - -58, - 9 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -100, - 31 - ], - [ - -15, - 42 - ], - [ - -69, - -21 - ], - [ - -8, - -23 - ], - [ - -43, - 17 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -35, - 3 - ], - [ - -32, - 22 - ], - [ - 11, - 29 - ], - [ - -3, - 22 - ] - ], - [ - [ - 13216, - 16499 - ], - [ - 21, - 6 - ], - [ - 36, - -33 - ], - [ - 10, - 32 - ], - [ - 61, - -5 - ], - [ - 50, - 21 - ], - [ - 33, - -4 - ], - [ - 22, - -24 - ], - [ - 7, - 20 - ], - [ - -10, - 78 - ], - [ - 25, - 16 - ], - [ - 24, - 55 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - 52, - -39 - ], - [ - 39, - 49 - ], - [ - 25, - 9 - ], - [ - 54, - -36 - ], - [ - 33, - 6 - ], - [ - 32, - -23 - ] - ], - [ - [ - 13730, - 16627 - ], - [ - -6, - -15 - ], - [ - 7, - -41 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 18, - 19 - ], - [ - 51, - -34 - ], - [ - 38, - -7 - ], - [ - 10, - 14 - ], - [ - -35, - 65 - ], - [ - 18, - 16 - ] - ], - [ - [ - 15782, - 15819 - ], - [ - 20, - -4 - ], - [ - 48, - -73 - ], - [ - 31, - -8 - ], - [ - 12, - 31 - ], - [ - 41, - 48 - ] - ], - [ - [ - 15934, - 15813 - ], - [ - 37, - -63 - ], - [ - 35, - -85 - ], - [ - 33, - -6 - ], - [ - 21, - -32 - ], - [ - -57, - -10 - ], - [ - -12, - -93 - ], - [ - -12, - -42 - ], - [ - -26, - -28 - ], - [ - 2, - -60 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - -17, - -6 - ], - [ - -44, - 63 - ], - [ - 24, - 60 - ], - [ - -20, - 35 - ], - [ - -26, - -9 - ], - [ - -83, - -89 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -48, - 16 - ], - [ - -35, - 55 - ], - [ - -11, - 46 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - -5, - 145 - ], - [ - -17, - 55 - ] - ], - [ - [ - 14571, - 10457 - ], - [ - 42, - -10 - ], - [ - 21, - 68 - ], - [ - 37, - -7 - ] - ], - [ - [ - 14671, - 10508 - ], - [ - 5, - -48 - ], - [ - 15, - -27 - ], - [ - 0, - -39 - ], - [ - -17, - -25 - ], - [ - -27, - -62 - ], - [ - -25, - -44 - ], - [ - -29, - -6 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -8, - -81 - ] - ], - [ - [ - 12969, - 16811 - ], - [ - -18, - -5 - ], - [ - -8, - -67 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - -61, - 55 - ], - [ - -36, - -9 - ], - [ - -48, - 56 - ], - [ - -33, - 48 - ], - [ - -32, - 2 - ], - [ - -10, - 42 - ] - ], - [ - [ - 12723, - 16933 - ], - [ - 56, - 24 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 51, - -9 - ], - [ - 64, - 25 - ], - [ - 44, - -53 - ], - [ - 39, - -28 - ] - ], - [ - [ - 12735, - 11548 - ], - [ - -57, - -14 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -18, - 83 - ], - [ - 4, - 275 - ], - [ - -15, - 25 - ], - [ - -2, - 59 - ], - [ - -24, - 42 - ], - [ - -22, - 35 - ], - [ - 9, - 64 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - 24, - 13 - ], - [ - 14, - 53 - ], - [ - 34, - 11 - ], - [ - 16, - 36 - ] - ], - [ - [ - 12698, - 12230 - ], - [ - 23, - 35 - ], - [ - 25, - 0 - ], - [ - 53, - -69 - ] - ], - [ - [ - 12799, - 12196 - ], - [ - -2, - -40 - ], - [ - 15, - -71 - ], - [ - -14, - -48 - ], - [ - 8, - -33 - ], - [ - -34, - -74 - ], - [ - -21, - -37 - ], - [ - -14, - -75 - ], - [ - 2, - -77 - ], - [ - -4, - -193 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - -61, - 2 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -32, - 10 - ], - [ - -23, - -20 - ], - [ - -30, - 9 - ], - [ - -121, - -6 - ], - [ - -2, - -68 - ], - [ - 9, - -90 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - -47, - 31 - ], - [ - -33, - -5 - ], - [ - -24, - -30 - ], - [ - -32, - 26 - ], - [ - -12, - 39 - ], - [ - -31, - 26 - ] - ], - [ - [ - 12171, - 12041 - ], - [ - -5, - 70 - ], - [ - 19, - 51 - ], - [ - -1, - 40 - ], - [ - 55, - 100 - ], - [ - 10, - 82 - ], - [ - 19, - 29 - ], - [ - 34, - -16 - ], - [ - 29, - 25 - ], - [ - 10, - 31 - ], - [ - 54, - 53 - ], - [ - 13, - 38 - ], - [ - 65, - 50 - ], - [ - 39, - 17 - ], - [ - 17, - -23 - ], - [ - 45, - 0 - ] - ], - [ - [ - 12574, - 12588 - ], - [ - -6, - -58 - ], - [ - 9, - -55 - ], - [ - 40, - -78 - ], - [ - 2, - -58 - ], - [ - 80, - -27 - ], - [ - -1, - -82 - ] - ], - [ - [ - 19008, - 13441 - ], - [ - -2, - -86 - ], - [ - -24, - 19 - ], - [ - 4, - -97 - ] - ], - [ - [ - 18986, - 13277 - ], - [ - -20, - 63 - ], - [ - -4, - 61 - ], - [ - -13, - 57 - ], - [ - -29, - 70 - ], - [ - -64, - 5 - ], - [ - 6, - -49 - ], - [ - -22, - -67 - ], - [ - -29, - 24 - ], - [ - -11, - -22 - ], - [ - -19, - 13 - ], - [ - -27, - 11 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -11, - 99 - ], - [ - -24, - 90 - ], - [ - 12, - 72 - ], - [ - -43, - 33 - ], - [ - 15, - 43 - ], - [ - 44, - 45 - ], - [ - -51, - 64 - ], - [ - 25, - 81 - ], - [ - 55, - -52 - ], - [ - 34, - -6 - ], - [ - 6, - -83 - ], - [ - 66, - -17 - ], - [ - 65, - 2 - ], - [ - 40, - -20 - ], - [ - -32, - -102 - ], - [ - -31, - -7 - ], - [ - -22, - -68 - ], - [ - 38, - -62 - ], - [ - 12, - 76 - ], - [ - 19, - 1 - ], - [ - 37, - -191 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - 20, - -49 - ], - [ - 27, - 8 - ], - [ - 54, - -18 - ], - [ - 102, - -7 - ], - [ - 34, - 31 - ], - [ - 83, - 28 - ], - [ - 50, - -44 - ], - [ - 41, - -12 - ] - ], - [ - [ - 14538, - 16041 - ], - [ - -36, - -50 - ], - [ - -25, - -86 - ], - [ - 22, - -68 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - -60, - 16 - ], - [ - -71, - -38 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - -1, - -60 - ], - [ - -63, - -11 - ], - [ - -49, - 42 - ], - [ - -56, - -33 - ], - [ - -52, - 3 - ] - ], - [ - [ - 14147, - 15756 - ], - [ - -4, - 80 - ], - [ - -35, - 38 - ] - ], - [ - [ - 14108, - 15874 - ], - [ - 11, - 17 - ], - [ - -7, - 15 - ], - [ - 11, - 38 - ], - [ - 27, - 37 - ], - [ - -34, - 52 - ], - [ - -6, - 44 - ], - [ - 17, - 27 - ] - ], - [ - [ - 7143, - 13648 - ], - [ - -17, - -6 - ], - [ - -18, - 69 - ], - [ - -26, - 35 - ], - [ - 15, - 76 - ], - [ - 21, - -5 - ], - [ - 24, - -100 - ], - [ - 1, - -69 - ] - ], - [ - [ - 7123, - 13986 - ], - [ - -76, - -19 - ], - [ - -5, - 44 - ], - [ - 33, - 10 - ], - [ - 46, - -4 - ], - [ - 2, - -31 - ] - ], - [ - [ - 7180, - 13987 - ], - [ - -12, - -85 - ], - [ - -13, - 15 - ], - [ - 1, - 63 - ], - [ - -31, - 47 - ], - [ - 0, - 14 - ], - [ - 55, - -54 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - -13, - -11 - ], - [ - -23, - -28 - ], - [ - -10, - -66 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -61, - 45 - ], - [ - -27, - 50 - ], - [ - -26, - 27 - ], - [ - -32, - 45 - ], - [ - -15, - 37 - ], - [ - -35, - 56 - ], - [ - 15, - 50 - ], - [ - 25, - -28 - ], - [ - 15, - 25 - ], - [ - 33, - 3 - ], - [ - 60, - -20 - ], - [ - 48, - 2 - ], - [ - 31, - -27 - ] - ], - [ - [ - 13872, - 16179 - ], - [ - 26, - 0 - ], - [ - -18, - -52 - ], - [ - 34, - -47 - ], - [ - -10, - -56 - ], - [ - -17, - -5 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - 67, - -1 - ], - [ - 76, - 45 - ], - [ - 16, - 68 - ], - [ - 57, - 39 - ], - [ - -7, - 53 - ] - ], - [ - [ - 14394, - 17469 - ], - [ - 43, - 20 - ], - [ - 75, - 47 - ] - ], - [ - [ - 14512, - 17536 - ], - [ - 73, - -30 - ], - [ - 10, - -30 - ], - [ - 37, - 14 - ], - [ - 68, - -28 - ], - [ - 6, - -57 - ], - [ - -14, - -32 - ], - [ - 43, - -79 - ], - [ - 29, - -22 - ], - [ - -5, - -21 - ], - [ - 47, - -21 - ], - [ - 21, - -32 - ], - [ - -28, - -27 - ], - [ - -56, - 5 - ], - [ - -13, - -12 - ], - [ - 16, - -39 - ], - [ - 17, - -77 - ] - ], - [ - [ - 14763, - 17048 - ], - [ - -60, - -7 - ], - [ - -21, - -27 - ], - [ - -5, - -60 - ], - [ - -27, - 12 - ], - [ - -63, - -6 - ], - [ - -18, - 28 - ], - [ - -27, - -21 - ], - [ - -26, - 17 - ], - [ - -55, - 3 - ], - [ - -78, - 28 - ], - [ - -70, - 10 - ], - [ - -54, - -3 - ], - [ - -38, - -32 - ], - [ - -33, - -5 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - -2, - 53 - ], - [ - -21, - 56 - ], - [ - 42, - 24 - ], - [ - 0, - 48 - ], - [ - -19, - 46 - ], - [ - -3, - 53 - ] - ], - [ - [ - 6333, - 12934 - ], - [ - 0, - 17 - ], - [ - 8, - 6 - ], - [ - 13, - -14 - ], - [ - 25, - 72 - ], - [ - 13, - 2 - ] - ], - [ - [ - 6392, - 13017 - ], - [ - 1, - -18 - ], - [ - 13, - -1 - ], - [ - -1, - -32 - ], - [ - -12, - -52 - ], - [ - 6, - -19 - ], - [ - -7, - -43 - ], - [ - 4, - -11 - ], - [ - -8, - -61 - ], - [ - -13, - -31 - ], - [ - -13, - -4 - ], - [ - -14, - -42 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - -21, - 0 - ], - [ - 6, - 136 - ], - [ - 0, - 95 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - -51, - -17 - ], - [ - -27, - 166 - ], - [ - -37, - 134 - ], - [ - 22, - 116 - ], - [ - -37, - 51 - ], - [ - -9, - 87 - ], - [ - -35, - 81 - ] - ], - [ - [ - 7696, - 8688 - ], - [ - 44, - 130 - ], - [ - -30, - 100 - ], - [ - 16, - 41 - ], - [ - -12, - 44 - ], - [ - 27, - 60 - ], - [ - 2, - 102 - ], - [ - 3, - 85 - ], - [ - 15, - 40 - ], - [ - -60, - 193 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - 52, - -10 - ], - [ - 35, - 3 - ], - [ - 16, - 36 - ], - [ - 61, - 49 - ], - [ - 37, - 45 - ], - [ - 91, - 20 - ], - [ - -8, - -90 - ], - [ - 9, - -46 - ], - [ - -6, - -80 - ], - [ - 76, - -108 - ], - [ - 78, - -20 - ], - [ - 28, - -44 - ], - [ - 47, - -24 - ], - [ - 29, - -35 - ], - [ - 43, - 1 - ], - [ - 41, - -35 - ], - [ - 3, - -70 - ], - [ - 14, - -35 - ], - [ - 0, - -52 - ], - [ - -20, - -2 - ], - [ - 27, - -139 - ], - [ - 134, - -5 - ], - [ - -11, - -70 - ], - [ - 8, - -47 - ], - [ - 38, - -34 - ], - [ - 16, - -74 - ], - [ - -12, - -95 - ], - [ - -19, - -52 - ], - [ - 7, - -69 - ], - [ - -22, - -24 - ] - ], - [ - [ - 8493, - 8377 - ], - [ - -1, - 37 - ], - [ - -65, - 61 - ], - [ - -65, - 2 - ], - [ - -122, - -35 - ], - [ - -33, - -106 - ], - [ - -2, - -64 - ], - [ - -27, - -144 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 13, - 70 - ], - [ - 10, - 70 - ], - [ - 0, - 66 - ], - [ - -25, - 22 - ], - [ - -26, - -19 - ], - [ - -26, - 5 - ], - [ - -9, - 46 - ], - [ - -6, - 110 - ], - [ - -13, - 36 - ], - [ - -47, - 33 - ], - [ - -29, - -24 - ], - [ - -73, - 23 - ], - [ - 4, - 163 - ], - [ - -20, - 67 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - -40, - -20 - ], - [ - -31, - 13 - ], - [ - 4, - 183 - ], - [ - -57, - -71 - ], - [ - -61, - 3 - ], - [ - -27, - 64 - ], - [ - -46, - 7 - ], - [ - 15, - 52 - ], - [ - -39, - 73 - ], - [ - -29, - 108 - ], - [ - 18, - 22 - ], - [ - 0, - 50 - ], - [ - 42, - 35 - ], - [ - -7, - 65 - ], - [ - 18, - 41 - ], - [ - 5, - 56 - ], - [ - 80, - 82 - ], - [ - 57, - 23 - ], - [ - 10, - 18 - ], - [ - 62, - -5 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - 32, - 328 - ], - [ - 1, - 53 - ], - [ - -11, - 68 - ], - [ - -31, - 44 - ], - [ - 1, - 87 - ], - [ - 39, - 20 - ], - [ - 14, - -13 - ], - [ - 2, - 46 - ], - [ - -40, - 13 - ], - [ - -1, - 75 - ], - [ - 135, - -3 - ], - [ - 24, - 42 - ], - [ - 19, - -38 - ], - [ - 14, - -71 - ], - [ - 13, - 15 - ] - ], - [ - [ - 7886, - 10948 - ], - [ - 38, - -64 - ], - [ - 54, - 8 - ], - [ - 14, - 37 - ], - [ - 52, - 28 - ], - [ - 28, - 19 - ], - [ - 8, - 51 - ], - [ - 50, - 34 - ], - [ - -4, - 25 - ], - [ - -59, - 11 - ], - [ - -9, - 75 - ], - [ - 2, - 81 - ], - [ - -31, - 31 - ], - [ - 13, - 11 - ], - [ - 52, - -15 - ], - [ - 55, - -30 - ], - [ - 21, - 28 - ], - [ - 50, - 19 - ], - [ - 78, - 44 - ], - [ - 25, - 46 - ], - [ - -9, - 34 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - 36, - 5 - ], - [ - 16, - -27 - ], - [ - -9, - -53 - ], - [ - 24, - -18 - ], - [ - 16, - -56 - ], - [ - -19, - -42 - ], - [ - -11, - -102 - ], - [ - 18, - -61 - ], - [ - 5, - -55 - ], - [ - 43, - -57 - ], - [ - 34, - -6 - ], - [ - 7, - 24 - ], - [ - 23, - 5 - ], - [ - 31, - 21 - ], - [ - 23, - 32 - ], - [ - 38, - -10 - ], - [ - 17, - 4 - ] - ], - [ - [ - 8606, - 11025 - ], - [ - 38, - -10 - ], - [ - 6, - 25 - ], - [ - -11, - 24 - ], - [ - 7, - 34 - ], - [ - 28, - -10 - ], - [ - 33, - 12 - ], - [ - 40, - -25 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 30, - -25 - ], - [ - 22, - 32 - ], - [ - 15, - -5 - ], - [ - 10, - -33 - ], - [ - 33, - 8 - ], - [ - 27, - 46 - ], - [ - 21, - 88 - ], - [ - 42, - 110 - ] - ], - [ - [ - 8947, - 11296 - ], - [ - 23, - 5 - ], - [ - 18, - -66 - ], - [ - 39, - -210 - ], - [ - 37, - -19 - ], - [ - 2, - -83 - ], - [ - -53, - -99 - ], - [ - 22, - -36 - ], - [ - 123, - -19 - ], - [ - 3, - -120 - ], - [ - 53, - 78 - ], - [ - 87, - -43 - ], - [ - 116, - -73 - ], - [ - 34, - -70 - ], - [ - -11, - -67 - ], - [ - 81, - 37 - ], - [ - 136, - -63 - ], - [ - 104, - 5 - ], - [ - 103, - -100 - ], - [ - 89, - -134 - ], - [ - 53, - -35 - ], - [ - 60, - -5 - ], - [ - 25, - -37 - ], - [ - 24, - -153 - ], - [ - 12, - -73 - ], - [ - -28, - -198 - ], - [ - -36, - -78 - ], - [ - -98, - -167 - ], - [ - -44, - -136 - ], - [ - -52, - -104 - ], - [ - -17, - -2 - ], - [ - -20, - -89 - ], - [ - 5, - -224 - ], - [ - -19, - -185 - ], - [ - -8, - -79 - ], - [ - -22, - -48 - ], - [ - -12, - -160 - ], - [ - -71, - -157 - ], - [ - -12, - -124 - ], - [ - -56, - -52 - ], - [ - -16, - -71 - ], - [ - -76, - 0 - ], - [ - -110, - -46 - ], - [ - -49, - -54 - ], - [ - -78, - -35 - ], - [ - -82, - -95 - ], - [ - -59, - -119 - ], - [ - -10, - -90 - ], - [ - 11, - -66 - ], - [ - -13, - -121 - ], - [ - -15, - -59 - ], - [ - -49, - -66 - ], - [ - -77, - -211 - ], - [ - -62, - -95 - ], - [ - -47, - -56 - ], - [ - -32, - -114 - ], - [ - -46, - -69 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -19, - 68 - ], - [ - 30, - 57 - ], - [ - -40, - 82 - ], - [ - -55, - 66 - ], - [ - -71, - 77 - ], - [ - -26, - -4 - ], - [ - -70, - 93 - ], - [ - -45, - -13 - ] - ], - [ - [ - 20508, - 11340 - ], - [ - 28, - 45 - ], - [ - 59, - 66 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - -3, - -59 - ], - [ - -4, - -77 - ], - [ - -33, - 4 - ], - [ - -15, - -41 - ], - [ - -32, - 62 - ] - ], - [ - [ - 18940, - 14129 - ], - [ - 28, - -38 - ], - [ - -5, - -74 - ], - [ - -57, - -4 - ], - [ - -59, - 8 - ], - [ - -44, - -18 - ], - [ - -63, - 45 - ], - [ - -1, - 24 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - 46, - 89 - ], - [ - 37, - 31 - ], - [ - 50, - -28 - ], - [ - 37, - -3 - ], - [ - 31, - -32 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - -98, - -88 - ], - [ - -63, - -90 - ], - [ - -23, - -80 - ], - [ - -21, - -45 - ], - [ - -38, - -10 - ], - [ - -12, - -57 - ], - [ - -7, - -37 - ], - [ - -45, - -28 - ], - [ - -57, - 6 - ], - [ - -33, - 33 - ], - [ - -29, - 15 - ], - [ - -34, - -28 - ], - [ - -18, - -58 - ], - [ - -33, - -36 - ], - [ - -34, - -53 - ], - [ - -50, - -12 - ], - [ - -16, - 42 - ], - [ - 7, - 73 - ], - [ - -42, - 114 - ], - [ - -19, - 18 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - 350 - ], - [ - 69, - 4 - ], - [ - 2, - 427 - ], - [ - 52, - 4 - ], - [ - 108, - 42 - ], - [ - 26, - -49 - ], - [ - 45, - 47 - ], - [ - 21, - 0 - ], - [ - 39, - 27 - ] - ], - [ - [ - 14296, - 8678 - ], - [ - 13, - -9 - ] - ], - [ - [ - 14309, - 8669 - ], - [ - 26, - -96 - ], - [ - 14, - -21 - ], - [ - 22, - -69 - ], - [ - 79, - -132 - ], - [ - 30, - -13 - ], - [ - 0, - -42 - ], - [ - 21, - -76 - ], - [ - 54, - -19 - ], - [ - 44, - -54 - ] - ], - [ - [ - 13613, - 11688 - ], - [ - 57, - 9 - ], - [ - 13, - 30 - ], - [ - 12, - -2 - ], - [ - 17, - -27 - ], - [ - 88, - 46 - ], - [ - 29, - 47 - ], - [ - 37, - 42 - ], - [ - -7, - 42 - ], - [ - 20, - 11 - ], - [ - 67, - -8 - ], - [ - 65, - 56 - ], - [ - 51, - 131 - ], - [ - 35, - 48 - ], - [ - 44, - 21 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 8, - -51 - ], - [ - 40, - -75 - ], - [ - 1, - -49 - ], - [ - -12, - -50 - ], - [ - 5, - -38 - ], - [ - 24, - -34 - ], - [ - 53, - -53 - ] - ], - [ - [ - 14260, - 11784 - ], - [ - 38, - -48 - ], - [ - 1, - -39 - ], - [ - 47, - -63 - ], - [ - 29, - -51 - ], - [ - 17, - -72 - ], - [ - 53, - -48 - ], - [ - 11, - -38 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - -23, - -13 - ], - [ - -45, - 3 - ], - [ - -52, - 13 - ], - [ - -26, - -11 - ], - [ - -11, - -29 - ], - [ - -22, - -3 - ], - [ - -28, - 25 - ], - [ - -77, - -60 - ], - [ - -32, - 12 - ], - [ - -10, - -9 - ], - [ - -21, - -72 - ], - [ - -52, - 23 - ], - [ - -51, - 12 - ], - [ - -44, - 44 - ], - [ - -57, - 41 - ], - [ - -38, - -39 - ], - [ - -27, - -61 - ], - [ - -6, - -83 - ] - ], - [ - [ - 13834, - 11218 - ], - [ - -45, - 6 - ], - [ - -47, - 20 - ], - [ - -42, - -63 - ], - [ - -36, - -112 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -8, - 35 - ], - [ - -3, - 55 - ], - [ - -32, - 38 - ], - [ - -25, - 62 - ], - [ - -6, - 43 - ], - [ - -33, - 63 - ], - [ - 5, - 36 - ], - [ - -7, - 50 - ], - [ - 6, - 93 - ], - [ - 17, - 22 - ], - [ - 35, - 122 - ] - ], - [ - [ - 8110, - 16382 - ], - [ - 50, - -16 - ], - [ - 65, - 3 - ], - [ - -35, - -49 - ], - [ - -25, - -8 - ], - [ - -89, - 51 - ], - [ - -17, - 40 - ], - [ - 26, - 37 - ], - [ - 25, - -58 - ] - ], - [ - [ - 8239, - 16688 - ], - [ - -34, - -2 - ], - [ - -90, - 38 - ], - [ - -65, - 56 - ], - [ - 24, - 10 - ], - [ - 92, - -30 - ], - [ - 71, - -50 - ], - [ - 2, - -22 - ] - ], - [ - [ - 3938, - 16617 - ], - [ - -35, - -17 - ], - [ - -115, - 55 - ], - [ - -21, - 42 - ], - [ - -62, - 42 - ], - [ - -13, - 34 - ], - [ - -71, - 22 - ], - [ - -27, - 65 - ], - [ - 6, - 28 - ], - [ - 73, - -26 - ], - [ - 43, - -18 - ], - [ - 65, - -13 - ], - [ - 24, - -41 - ], - [ - 34, - -57 - ], - [ - 70, - -50 - ], - [ - 29, - -66 - ] - ], - [ - [ - 8634, - 16878 - ], - [ - -46, - -105 - ], - [ - 46, - 41 - ], - [ - 47, - -26 - ], - [ - -25, - -42 - ], - [ - 62, - -33 - ], - [ - 32, - 29 - ], - [ - 70, - -36 - ], - [ - -22, - -88 - ], - [ - 49, - 20 - ], - [ - 9, - -63 - ], - [ - 21, - -75 - ], - [ - -29, - -106 - ], - [ - -31, - -4 - ], - [ - -46, - 23 - ], - [ - 15, - 98 - ], - [ - -20, - 15 - ], - [ - -80, - -104 - ], - [ - -42, - 4 - ], - [ - 49, - 56 - ], - [ - -67, - 30 - ], - [ - -75, - -8 - ], - [ - -135, - 4 - ], - [ - -11, - 36 - ], - [ - 44, - 42 - ], - [ - -30, - 32 - ], - [ - 58, - 73 - ], - [ - 72, - 191 - ], - [ - 43, - 68 - ], - [ - 61, - 41 - ], - [ - 32, - -5 - ], - [ - -13, - -32 - ], - [ - -38, - -76 - ] - ], - [ - [ - 3264, - 17296 - ], - [ - 33, - -16 - ], - [ - 66, - 10 - ], - [ - -20, - -136 - ], - [ - 60, - -97 - ], - [ - -28, - 0 - ], - [ - -42, - 55 - ], - [ - -25, - 56 - ], - [ - -36, - 37 - ], - [ - -12, - 53 - ], - [ - 4, - 38 - ] - ], - [ - [ - 7022, - 18254 - ], - [ - -27, - -63 - ], - [ - -31, - 10 - ], - [ - -18, - 36 - ], - [ - 3, - 9 - ], - [ - 27, - 36 - ], - [ - 28, - -3 - ], - [ - 18, - -25 - ] - ], - [ - [ - 6839, - 18321 - ], - [ - -82, - -67 - ], - [ - -49, - 3 - ], - [ - -16, - 33 - ], - [ - 52, - 55 - ], - [ - 96, - -1 - ], - [ - -1, - -23 - ] - ], - [ - [ - 6611, - 18674 - ], - [ - 13, - -53 - ], - [ - 36, - 19 - ], - [ - 40, - -32 - ], - [ - 77, - -41 - ], - [ - 79, - -37 - ], - [ - 7, - -57 - ], - [ - 51, - 9 - ], - [ - 50, - -40 - ], - [ - -62, - -37 - ], - [ - -109, - 28 - ], - [ - -39, - 54 - ], - [ - -69, - -63 - ], - [ - -99, - -62 - ], - [ - -24, - 70 - ], - [ - -95, - -12 - ], - [ - 61, - 59 - ], - [ - 9, - 95 - ], - [ - 24, - 110 - ], - [ - 50, - -10 - ] - ], - [ - [ - 7259, - 18853 - ], - [ - -78, - -6 - ], - [ - -18, - 59 - ], - [ - 30, - 67 - ], - [ - 64, - 17 - ], - [ - 54, - -34 - ], - [ - 1, - -51 - ], - [ - -8, - -17 - ], - [ - -45, - -35 - ] - ], - [ - [ - 5880, - 19088 - ], - [ - -43, - -42 - ], - [ - -94, - 36 - ], - [ - -57, - -13 - ], - [ - -95, - 54 - ], - [ - 61, - 37 - ], - [ - 49, - 52 - ], - [ - 74, - -34 - ], - [ - 42, - -21 - ], - [ - 21, - -23 - ], - [ - 42, - -46 - ] - ], - [ - [ - 3985, - 16676 - ], - [ - -10, - 0 - ], - [ - -135, - 118 - ], - [ - -50, - 52 - ], - [ - -126, - 49 - ], - [ - -39, - 106 - ], - [ - 10, - 74 - ], - [ - -89, - 51 - ], - [ - -12, - 97 - ], - [ - -84, - 87 - ], - [ - -2, - 62 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - 39, - 58 - ], - [ - -2, - 75 - ], - [ - -119, - 77 - ], - [ - -71, - 137 - ], - [ - -43, - 86 - ], - [ - -64, - 54 - ], - [ - -47, - 49 - ], - [ - -37, - 62 - ], - [ - -70, - -39 - ], - [ - -68, - -67 - ], - [ - -62, - 79 - ], - [ - -49, - 52 - ], - [ - -68, - 34 - ], - [ - -68, - 3 - ], - [ - 0, - 683 - ], - [ - 1, - 445 - ] - ], - [ - [ - 2720, - 19160 - ], - [ - 130, - -28 - ], - [ - 109, - -58 - ], - [ - 73, - -11 - ], - [ - 61, - 50 - ], - [ - 85, - 37 - ], - [ - 103, - -14 - ], - [ - 105, - 52 - ], - [ - 114, - 30 - ], - [ - 48, - -49 - ], - [ - 52, - 28 - ], - [ - 15, - 56 - ], - [ - 48, - -13 - ], - [ - 118, - -107 - ], - [ - 93, - 81 - ], - [ - 9, - -91 - ], - [ - 86, - 20 - ], - [ - 26, - 35 - ], - [ - 85, - -7 - ], - [ - 106, - -51 - ], - [ - 164, - -44 - ], - [ - 96, - -20 - ], - [ - 68, - 8 - ], - [ - 94, - -61 - ], - [ - -98, - -60 - ], - [ - 126, - -25 - ], - [ - 188, - 14 - ], - [ - 59, - 21 - ], - [ - 75, - -72 - ], - [ - 75, - 61 - ], - [ - -71, - 50 - ], - [ - 45, - 42 - ], - [ - 85, - 5 - ], - [ - 56, - 12 - ], - [ - 56, - -29 - ], - [ - 70, - -65 - ], - [ - 78, - 10 - ], - [ - 123, - -54 - ], - [ - 109, - 19 - ], - [ - 101, - -3 - ], - [ - -8, - 75 - ], - [ - 62, - 20 - ], - [ - 108, - -40 - ], - [ - 0, - -114 - ], - [ - 44, - 96 - ], - [ - 56, - -3 - ], - [ - 32, - 120 - ], - [ - -75, - 74 - ], - [ - -81, - 49 - ], - [ - 5, - 132 - ], - [ - 83, - 87 - ], - [ - 92, - -19 - ], - [ - 70, - -53 - ], - [ - 95, - -135 - ], - [ - -62, - -59 - ], - [ - 130, - -24 - ], - [ - -1, - -123 - ], - [ - 93, - 94 - ], - [ - 84, - -77 - ], - [ - -21, - -89 - ], - [ - 67, - -81 - ], - [ - 73, - 87 - ], - [ - 51, - 103 - ], - [ - 4, - 132 - ], - [ - 99, - -9 - ], - [ - 103, - -18 - ], - [ - 94, - -60 - ], - [ - 4, - -59 - ], - [ - -52, - -64 - ], - [ - 49, - -64 - ], - [ - -9, - -59 - ], - [ - -136, - -83 - ], - [ - -97, - -19 - ], - [ - -72, - 36 - ], - [ - -21, - -60 - ], - [ - -67, - -101 - ], - [ - -21, - -53 - ], - [ - -80, - -81 - ], - [ - -100, - -8 - ], - [ - -55, - -51 - ], - [ - -5, - -78 - ], - [ - -81, - -15 - ], - [ - -85, - -97 - ], - [ - -76, - -135 - ], - [ - -27, - -94 - ], - [ - -4, - -140 - ], - [ - 103, - -20 - ], - [ - 31, - -112 - ], - [ - 33, - -91 - ], - [ - 97, - 24 - ], - [ - 130, - -52 - ], - [ - 69, - -46 - ], - [ - 50, - -57 - ], - [ - 88, - -33 - ], - [ - 73, - -50 - ], - [ - 116, - -7 - ], - [ - 75, - -12 - ], - [ - -11, - -104 - ], - [ - 22, - -120 - ], - [ - 50, - -134 - ], - [ - 104, - -114 - ], - [ - 54, - 39 - ], - [ - 37, - 123 - ], - [ - -36, - 189 - ], - [ - -49, - 64 - ], - [ - 111, - 56 - ], - [ - 79, - 84 - ], - [ - 39, - 84 - ], - [ - -6, - 80 - ], - [ - -47, - 102 - ], - [ - -85, - 90 - ], - [ - 82, - 126 - ], - [ - -30, - 108 - ], - [ - -23, - 188 - ], - [ - 48, - 27 - ], - [ - 120, - -32 - ], - [ - 72, - -12 - ], - [ - 57, - 32 - ], - [ - 65, - -41 - ], - [ - 86, - -70 - ], - [ - 21, - -46 - ], - [ - 124, - -9 - ], - [ - -2, - -101 - ], - [ - 24, - -152 - ], - [ - 63, - -19 - ], - [ - 51, - -70 - ], - [ - 101, - 66 - ], - [ - 66, - 133 - ], - [ - 46, - 56 - ], - [ - 55, - -108 - ], - [ - 91, - -153 - ], - [ - 77, - -143 - ], - [ - -28, - -76 - ], - [ - 92, - -67 - ], - [ - 63, - -69 - ], - [ - 111, - -31 - ], - [ - 45, - -38 - ], - [ - 28, - -102 - ], - [ - 54, - -16 - ], - [ - 28, - -45 - ], - [ - 5, - -135 - ], - [ - -51, - -45 - ], - [ - -50, - -42 - ], - [ - -115, - -43 - ], - [ - -87, - -98 - ], - [ - -118, - -20 - ], - [ - -149, - 26 - ], - [ - -105, - 0 - ], - [ - -72, - -8 - ], - [ - -58, - -86 - ], - [ - -89, - -53 - ], - [ - -101, - -159 - ], - [ - -80, - -111 - ], - [ - 59, - 20 - ], - [ - 112, - 158 - ], - [ - 146, - 100 - ], - [ - 105, - 12 - ], - [ - 61, - -59 - ], - [ - -66, - -81 - ], - [ - 23, - -129 - ], - [ - 22, - -91 - ], - [ - 91, - -60 - ], - [ - 115, - 18 - ], - [ - 70, - 135 - ], - [ - 5, - -87 - ], - [ - 45, - -44 - ], - [ - -86, - -78 - ], - [ - -155, - -72 - ], - [ - -69, - -48 - ], - [ - -78, - -87 - ], - [ - -53, - 9 - ], - [ - -3, - 102 - ], - [ - 122, - 99 - ], - [ - -112, - -4 - ], - [ - -78, - -15 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - -45, - 68 - ], - [ - 0, - 164 - ], - [ - -31, - 34 - ], - [ - -47, - -20 - ], - [ - -23, - 31 - ], - [ - -53, - -90 - ], - [ - -21, - -93 - ], - [ - -25, - -55 - ], - [ - -30, - -19 - ], - [ - -22, - -6 - ], - [ - -7, - -29 - ], - [ - -128, - 0 - ], - [ - -106, - -1 - ], - [ - -32, - -22 - ], - [ - -73, - -87 - ], - [ - -9, - -9 - ], - [ - -22, - -47 - ], - [ - -64, - 0 - ], - [ - -69, - 0 - ], - [ - -31, - -19 - ], - [ - 11, - -24 - ], - [ - 6, - -36 - ], - [ - -1, - -13 - ], - [ - -91, - -59 - ], - [ - -72, - -19 - ], - [ - -81, - -64 - ], - [ - -18, - 0 - ], - [ - -23, - 19 - ], - [ - -8, - 17 - ], - [ - 1, - 12 - ], - [ - 16, - 42 - ], - [ - 32, - 66 - ], - [ - 21, - 71 - ], - [ - -14, - 105 - ], - [ - -15, - 108 - ], - [ - -73, - 57 - ], - [ - 9, - 21 - ], - [ - -10, - 15 - ], - [ - -19, - 0 - ], - [ - -14, - 19 - ], - [ - -4, - 28 - ], - [ - -13, - -12 - ], - [ - -19, - 3 - ], - [ - 4, - 12 - ], - [ - -16, - 12 - ], - [ - -7, - 32 - ], - [ - -54, - 38 - ], - [ - -57, - 40 - ], - [ - -68, - 46 - ], - [ - -65, - 44 - ], - [ - -63, - -34 - ], - [ - -22, - -1 - ], - [ - -86, - 31 - ], - [ - -57, - -16 - ], - [ - -67, - 38 - ], - [ - -71, - 19 - ], - [ - -49, - 7 - ], - [ - -22, - 20 - ], - [ - -12, - 66 - ], - [ - -24, - 0 - ], - [ - 0, - -46 - ], - [ - -144, - 0 - ], - [ - -239, - 0 - ], - [ - -237, - 0 - ], - [ - -209, - 0 - ], - [ - -209, - 0 - ], - [ - -206, - 0 - ], - [ - -212, - 0 - ], - [ - -69, - 0 - ], - [ - -207, - 0 - ], - [ - -197, - 0 - ] - ], - [ - [ - 4589, - 19569 - ], - [ - -35, - -56 - ], - [ - 155, - 37 - ], - [ - 97, - -61 - ], - [ - 79, - 61 - ], - [ - 64, - -39 - ], - [ - 57, - -118 - ], - [ - 35, - 50 - ], - [ - -50, - 123 - ], - [ - 62, - 17 - ], - [ - 69, - -19 - ], - [ - 78, - -48 - ], - [ - 44, - -117 - ], - [ - 21, - -85 - ], - [ - 118, - -59 - ], - [ - 125, - -57 - ], - [ - -7, - -53 - ], - [ - -115, - -9 - ], - [ - 45, - -47 - ], - [ - -24, - -44 - ], - [ - -126, - 19 - ], - [ - -120, - 33 - ], - [ - -81, - -8 - ], - [ - -131, - -40 - ], - [ - -176, - -18 - ], - [ - -124, - -12 - ], - [ - -38, - 57 - ], - [ - -95, - 33 - ], - [ - -62, - -14 - ], - [ - -86, - 95 - ], - [ - 46, - 13 - ], - [ - 108, - 20 - ], - [ - 98, - -5 - ], - [ - 91, - 21 - ], - [ - -135, - 28 - ], - [ - -149, - -10 - ], - [ - -98, - 3 - ], - [ - -37, - 44 - ], - [ - 161, - 48 - ], - [ - -107, - -2 - ], - [ - -122, - 32 - ], - [ - 59, - 90 - ], - [ - 48, - 48 - ], - [ - 187, - 73 - ], - [ - 71, - -24 - ] - ], - [ - [ - 5263, - 19605 - ], - [ - -61, - -79 - ], - [ - -109, - 84 - ], - [ - 24, - 17 - ], - [ - 93, - 5 - ], - [ - 53, - -27 - ] - ], - [ - [ - 7226, - 19567 - ], - [ - 6, - -33 - ], - [ - -74, - 4 - ], - [ - -75, - 2 - ], - [ - -76, - -16 - ], - [ - -21, - 7 - ], - [ - -76, - 64 - ], - [ - 3, - 43 - ], - [ - 33, - 8 - ], - [ - 160, - -13 - ], - [ - 120, - -66 - ] - ], - [ - [ - 6513, - 19574 - ], - [ - 55, - -75 - ], - [ - 65, - 97 - ], - [ - 176, - 49 - ], - [ - 120, - -124 - ], - [ - -10, - -79 - ], - [ - 138, - 35 - ], - [ - 65, - 48 - ], - [ - 155, - -61 - ], - [ - 96, - -57 - ], - [ - 9, - -52 - ], - [ - 130, - 27 - ], - [ - 72, - -77 - ], - [ - 169, - -47 - ], - [ - 60, - -48 - ], - [ - 66, - -113 - ], - [ - -128, - -56 - ], - [ - 164, - -78 - ], - [ - 111, - -26 - ], - [ - 100, - -110 - ], - [ - 110, - -8 - ], - [ - -22, - -85 - ], - [ - -122, - -139 - ], - [ - -86, - 51 - ], - [ - -110, - 116 - ], - [ - -90, - -15 - ], - [ - -9, - -69 - ], - [ - 74, - -70 - ], - [ - 94, - -55 - ], - [ - 29, - -32 - ], - [ - 46, - -119 - ], - [ - -25, - -86 - ], - [ - -87, - 33 - ], - [ - -175, - 96 - ], - [ - 98, - -104 - ], - [ - 73, - -72 - ], - [ - 11, - -42 - ], - [ - -189, - 48 - ], - [ - -149, - 70 - ], - [ - -85, - 58 - ], - [ - 24, - 34 - ], - [ - -104, - 61 - ], - [ - -101, - 59 - ], - [ - 1, - -35 - ], - [ - -202, - -19 - ], - [ - -59, - 41 - ], - [ - 46, - 88 - ], - [ - 131, - 2 - ], - [ - 144, - 16 - ], - [ - -23, - 43 - ], - [ - 24, - 59 - ], - [ - 90, - 117 - ], - [ - -19, - 53 - ], - [ - -27, - 41 - ], - [ - -107, - 59 - ], - [ - -141, - 40 - ], - [ - 45, - 31 - ], - [ - -74, - 74 - ], - [ - -62, - 7 - ], - [ - -54, - 41 - ], - [ - -38, - -35 - ], - [ - -126, - -16 - ], - [ - -254, - 27 - ], - [ - -147, - 35 - ], - [ - -113, - 18 - ], - [ - -58, - 42 - ], - [ - 73, - 55 - ], - [ - -99, - 1 - ], - [ - -23, - 121 - ], - [ - 54, - 107 - ], - [ - 72, - 49 - ], - [ - 180, - 32 - ], - [ - -52, - -77 - ] - ], - [ - [ - 5552, - 19656 - ], - [ - 83, - -25 - ], - [ - 124, - 15 - ], - [ - 18, - -35 - ], - [ - -65, - -57 - ], - [ - 106, - -52 - ], - [ - -13, - -108 - ], - [ - -114, - -46 - ], - [ - -67, - 10 - ], - [ - -48, - 46 - ], - [ - -174, - 92 - ], - [ - 2, - 39 - ], - [ - 142, - -15 - ], - [ - -77, - 78 - ], - [ - 83, - 58 - ] - ], - [ - [ - 6051, - 19528 - ], - [ - -75, - -90 - ], - [ - -79, - 4 - ], - [ - -44, - 106 - ], - [ - 1, - 59 - ], - [ - 37, - 51 - ], - [ - 69, - 33 - ], - [ - 145, - -4 - ], - [ - 133, - -29 - ], - [ - -104, - -107 - ], - [ - -83, - -23 - ] - ], - [ - [ - 4150, - 19361 - ], - [ - -183, - -58 - ], - [ - -37, - 53 - ], - [ - -161, - 63 - ], - [ - 30, - 51 - ], - [ - 48, - 88 - ], - [ - 61, - 78 - ], - [ - -68, - 74 - ], - [ - 235, - 19 - ], - [ - 100, - -25 - ], - [ - 178, - -7 - ], - [ - 68, - -35 - ], - [ - 74, - -50 - ], - [ - -87, - -30 - ], - [ - -171, - -85 - ], - [ - -87, - -84 - ], - [ - 0, - -52 - ] - ], - [ - [ - 6022, - 19792 - ], - [ - -38, - -46 - ], - [ - -101, - 9 - ], - [ - -85, - 31 - ], - [ - 37, - 54 - ], - [ - 101, - 32 - ], - [ - 60, - -42 - ], - [ - 26, - -38 - ] - ], - [ - [ - 5681, - 20001 - ], - [ - 54, - -55 - ], - [ - 2, - -62 - ], - [ - -32, - -89 - ], - [ - -115, - -12 - ], - [ - -75, - 19 - ], - [ - 2, - 70 - ], - [ - -115, - -10 - ], - [ - -4, - 93 - ], - [ - 75, - -4 - ], - [ - 105, - 41 - ], - [ - 98, - -7 - ], - [ - 5, - 16 - ] - ], - [ - [ - 5004, - 19939 - ], - [ - 28, - -43 - ], - [ - 62, - 20 - ], - [ - 73, - -5 - ], - [ - 12, - -59 - ], - [ - -42, - -57 - ], - [ - -237, - -18 - ], - [ - -175, - -52 - ], - [ - -106, - -3 - ], - [ - -9, - 39 - ], - [ - 145, - 53 - ], - [ - -315, - -14 - ], - [ - -98, - 22 - ], - [ - 95, - 117 - ], - [ - 66, - 33 - ], - [ - 196, - -40 - ], - [ - 124, - -71 - ], - [ - 122, - -9 - ], - [ - -100, - 114 - ], - [ - 64, - 44 - ], - [ - 72, - -14 - ], - [ - 23, - -57 - ] - ], - [ - [ - 5947, - 20047 - ], - [ - 78, - -39 - ], - [ - 137, - 0 - ], - [ - 60, - -39 - ], - [ - -16, - -45 - ], - [ - 80, - -27 - ], - [ - 44, - -29 - ], - [ - 94, - -5 - ], - [ - 102, - -10 - ], - [ - 111, - 26 - ], - [ - 142, - 10 - ], - [ - 113, - -8 - ], - [ - 75, - -46 - ], - [ - 15, - -49 - ], - [ - -43, - -32 - ], - [ - -104, - -26 - ], - [ - -89, - 15 - ], - [ - -200, - -19 - ], - [ - -143, - -2 - ], - [ - -113, - 15 - ], - [ - -185, - 38 - ], - [ - -24, - 66 - ], - [ - -9, - 60 - ], - [ - -70, - 52 - ], - [ - -144, - 15 - ], - [ - -81, - 37 - ], - [ - 27, - 49 - ], - [ - 143, - -7 - ] - ], - [ - [ - 4447, - 20112 - ], - [ - -9, - -92 - ], - [ - -54, - -42 - ], - [ - -65, - -5 - ], - [ - -129, - -52 - ], - [ - -112, - -18 - ], - [ - -95, - 26 - ], - [ - 119, - 90 - ], - [ - 143, - 77 - ], - [ - 107, - -1 - ], - [ - 95, - 17 - ] - ], - [ - [ - 6006, - 20097 - ], - [ - -32, - -3 - ], - [ - -130, - 7 - ], - [ - -19, - 34 - ], - [ - 140, - -2 - ], - [ - 49, - -22 - ], - [ - -8, - -14 - ] - ], - [ - [ - 4867, - 20118 - ], - [ - -130, - -34 - ], - [ - -104, - 39 - ], - [ - 57, - 38 - ], - [ - 101, - 12 - ], - [ - 99, - -19 - ], - [ - -23, - -36 - ] - ], - [ - [ - 4903, - 20227 - ], - [ - -85, - -23 - ], - [ - -116, - 0 - ], - [ - 2, - 17 - ], - [ - 71, - 36 - ], - [ - 37, - -6 - ], - [ - 91, - -24 - ] - ], - [ - [ - 5867, - 20162 - ], - [ - -103, - -25 - ], - [ - -57, - 28 - ], - [ - -29, - 45 - ], - [ - -6, - 49 - ], - [ - 90, - -4 - ], - [ - 41, - -8 - ], - [ - 83, - -42 - ], - [ - -19, - -43 - ] - ], - [ - [ - 5572, - 20194 - ], - [ - 28, - -50 - ], - [ - -114, - 13 - ], - [ - -115, - 39 - ], - [ - -155, - 4 - ], - [ - 67, - 36 - ], - [ - -84, - 29 - ], - [ - -5, - 46 - ], - [ - 137, - -16 - ], - [ - 188, - -44 - ], - [ - 53, - -57 - ] - ], - [ - [ - 6481, - 20354 - ], - [ - 85, - -39 - ], - [ - -96, - -36 - ], - [ - -129, - -90 - ], - [ - -123, - -8 - ], - [ - -145, - 15 - ], - [ - -75, - 49 - ], - [ - 1, - 43 - ], - [ - 56, - 32 - ], - [ - -128, - -1 - ], - [ - -77, - 40 - ], - [ - -44, - 55 - ], - [ - 48, - 53 - ], - [ - 49, - 37 - ], - [ - 71, - 8 - ], - [ - -30, - 27 - ], - [ - 162, - 7 - ], - [ - 89, - -65 - ], - [ - 117, - -25 - ], - [ - 114, - -23 - ], - [ - 55, - -79 - ] - ], - [ - [ - 7772, - 20767 - ], - [ - 187, - -9 - ], - [ - 149, - -15 - ], - [ - 128, - -33 - ], - [ - -3, - -32 - ], - [ - -170, - -52 - ], - [ - -169, - -24 - ], - [ - -63, - -27 - ], - [ - 152, - 0 - ], - [ - -165, - -72 - ], - [ - -113, - -34 - ], - [ - -119, - -98 - ], - [ - -144, - -20 - ], - [ - -45, - -25 - ], - [ - -211, - -13 - ], - [ - 96, - -15 - ], - [ - -48, - -21 - ], - [ - 58, - -59 - ], - [ - -66, - -41 - ], - [ - -108, - -34 - ], - [ - -33, - -47 - ], - [ - -97, - -36 - ], - [ - 9, - -27 - ], - [ - 119, - 4 - ], - [ - 2, - -29 - ], - [ - -186, - -72 - ], - [ - -182, - 33 - ], - [ - -205, - -18 - ], - [ - -104, - 14 - ], - [ - -132, - 6 - ], - [ - -8, - 58 - ], - [ - 128, - 27 - ], - [ - -34, - 87 - ], - [ - 43, - 8 - ], - [ - 186, - -52 - ], - [ - -95, - 77 - ], - [ - -113, - 23 - ], - [ - 56, - 47 - ], - [ - 124, - 28 - ], - [ - 20, - 42 - ], - [ - -99, - 47 - ], - [ - -29, - 62 - ], - [ - 190, - -5 - ], - [ - 55, - -13 - ], - [ - 109, - 43 - ], - [ - -157, - 14 - ], - [ - -244, - -7 - ], - [ - -123, - 40 - ], - [ - -58, - 49 - ], - [ - -82, - 35 - ], - [ - -15, - 41 - ], - [ - 104, - 23 - ], - [ - 81, - 4 - ], - [ - 137, - 19 - ], - [ - 102, - 45 - ], - [ - 87, - -6 - ], - [ - 75, - -34 - ], - [ - 53, - 65 - ], - [ - 92, - 19 - ], - [ - 125, - 13 - ], - [ - 213, - 5 - ], - [ - 37, - -13 - ], - [ - 202, - 21 - ], - [ - 151, - -8 - ], - [ - 150, - -8 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -5, - -49 - ], - [ - -31, - -20 - ], - [ - -51, - 15 - ], - [ - -15, - -49 - ], - [ - -34, - -4 - ], - [ - -12, - 19 - ], - [ - -39, - -40 - ], - [ - -33, - -6 - ], - [ - -30, - 26 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -24, - 52 - ], - [ - -34, - -18 - ], - [ - 1, - 54 - ], - [ - 51, - 67 - ], - [ - -2, - 31 - ], - [ - 32, - -11 - ], - [ - 19, - 20 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 59, - -1 - ], - [ - 15, - 26 - ], - [ - 74, - -36 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -23, - -48 - ], - [ - -60, - -37 - ], - [ - -34, - 3 - ], - [ - -42, - 10 - ], - [ - -50, - 36 - ], - [ - -73, - 17 - ], - [ - -88, - 67 - ], - [ - -71, - 65 - ], - [ - -96, - 134 - ], - [ - 57, - -25 - ], - [ - 98, - -80 - ], - [ - 93, - -43 - ], - [ - 36, - 55 - ], - [ - 22, - 82 - ], - [ - 65, - 50 - ], - [ - 49, - -15 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -62, - 1 - ], - [ - -33, - -30 - ], - [ - -63, - -43 - ], - [ - -11, - -112 - ], - [ - -30, - -3 - ], - [ - -78, - 39 - ], - [ - -80, - 84 - ], - [ - -87, - 68 - ], - [ - -22, - 76 - ], - [ - 20, - 71 - ], - [ - -35, - 79 - ], - [ - -9, - 205 - ], - [ - 30, - 115 - ], - [ - 73, - 93 - ], - [ - -106, - 35 - ], - [ - 67, - 106 - ], - [ - 24, - 199 - ], - [ - 77, - -42 - ], - [ - 36, - 249 - ], - [ - -46, - 31 - ], - [ - -22, - -149 - ], - [ - -44, - 17 - ], - [ - 22, - 171 - ], - [ - 24, - 222 - ], - [ - 32, - 82 - ], - [ - -20, - 117 - ], - [ - -6, - 136 - ], - [ - 29, - 3 - ], - [ - 43, - 194 - ], - [ - 48, - 192 - ], - [ - 30, - 179 - ], - [ - -16, - 180 - ], - [ - 20, - 99 - ], - [ - -8, - 148 - ], - [ - 41, - 146 - ], - [ - 12, - 232 - ], - [ - 23, - 249 - ], - [ - 22, - 269 - ], - [ - -6, - 196 - ], - [ - -14, - 169 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - 36, - 31 - ], - [ - 18, - 61 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - -10, - 69 - ], - [ - 2, - 46 - ], - [ - -42, - 28 - ], - [ - -23, - -12 - ], - [ - -18, - 111 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - 20, - 27 - ], - [ - -9, - 28 - ], - [ - 66, - 57 - ], - [ - 48, - 23 - ], - [ - 74, - -16 - ], - [ - 26, - 77 - ], - [ - 90, - 14 - ], - [ - 25, - 48 - ], - [ - 109, - 65 - ], - [ - 10, - 27 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -5, - 68 - ], - [ - 48, - 31 - ], - [ - -63, - 209 - ], - [ - 138, - 48 - ], - [ - 36, - 27 - ], - [ - 50, - 214 - ], - [ - 138, - -39 - ], - [ - 39, - 54 - ], - [ - 3, - 120 - ], - [ - 58, - 12 - ], - [ - 53, - 79 - ] - ], - [ - [ - 18637, - 16701 - ], - [ - 27, - 10 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 19, - -83 - ], - [ - 58, - -64 - ], - [ - 100, - -45 - ], - [ - 48, - -97 - ], - [ - -27, - -140 - ], - [ - 25, - -52 - ], - [ - 83, - -20 - ], - [ - 94, - -17 - ], - [ - 84, - -75 - ], - [ - 43, - -13 - ], - [ - 32, - -111 - ], - [ - 41, - -71 - ], - [ - 77, - 3 - ], - [ - 144, - -27 - ], - [ - 92, - 17 - ], - [ - 69, - -18 - ], - [ - 103, - -73 - ], - [ - 85, - 0 - ], - [ - 30, - -37 - ], - [ - 82, - 64 - ], - [ - 112, - 42 - ], - [ - 105, - 4 - ], - [ - 81, - 42 - ], - [ - 50, - 65 - ], - [ - 49, - 40 - ], - [ - -11, - 40 - ], - [ - -23, - 46 - ], - [ - 37, - 77 - ], - [ - 39, - -11 - ], - [ - 72, - -24 - ], - [ - 69, - 64 - ], - [ - 107, - 46 - ], - [ - 51, - 79 - ], - [ - 49, - 34 - ], - [ - 101, - 16 - ], - [ - 55, - -13 - ], - [ - 8, - 42 - ], - [ - -64, - 84 - ], - [ - -55, - 39 - ], - [ - -54, - -45 - ], - [ - -69, - 19 - ], - [ - -39, - -15 - ], - [ - -18, - 49 - ], - [ - 49, - 120 - ], - [ - 34, - 90 - ] - ], - [ - [ - 20681, - 16782 - ], - [ - 84, - -45 - ], - [ - 98, - 76 - ], - [ - -1, - 53 - ], - [ - 63, - 127 - ], - [ - 39, - 38 - ], - [ - -1, - 67 - ], - [ - -38, - 28 - ], - [ - 57, - 60 - ], - [ - 87, - 21 - ], - [ - 92, - 4 - ], - [ - 105, - -36 - ], - [ - 61, - -44 - ], - [ - 43, - -121 - ], - [ - 26, - -52 - ], - [ - 24, - -74 - ], - [ - 26, - -117 - ], - [ - 122, - -38 - ], - [ - 82, - -86 - ], - [ - 28, - -112 - ], - [ - 106, - -1 - ], - [ - 61, - 48 - ], - [ - 115, - 35 - ], - [ - -37, - -108 - ], - [ - -27, - -44 - ], - [ - -24, - -131 - ], - [ - -47, - -117 - ], - [ - -84, - 21 - ], - [ - -60, - -42 - ], - [ - 18, - -103 - ], - [ - -10, - -142 - ], - [ - -35, - -3 - ], - [ - 0, - -61 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - -45, - 71 - ], - [ - -28, - -67 - ], - [ - -107, - -52 - ], - [ - 11, - -63 - ], - [ - -61, - 4 - ], - [ - -33, - 38 - ], - [ - -48, - -85 - ], - [ - -76, - -65 - ], - [ - -57, - -77 - ] - ], - [ - [ - 21210, - 15587 - ], - [ - -98, - -35 - ], - [ - -51, - -56 - ], - [ - -75, - -32 - ], - [ - 37, - 55 - ], - [ - -15, - 47 - ], - [ - 56, - 81 - ], - [ - -37, - 62 - ], - [ - -61, - -42 - ], - [ - -79, - -83 - ], - [ - -43, - -78 - ], - [ - -68, - -6 - ], - [ - -35, - -55 - ], - [ - 36, - -82 - ], - [ - 57, - -19 - ], - [ - 3, - -54 - ], - [ - 55, - -35 - ], - [ - 78, - 85 - ], - [ - 62, - -46 - ], - [ - 45, - -3 - ], - [ - 11, - -63 - ], - [ - -99, - -34 - ], - [ - -32, - -65 - ], - [ - -68, - -60 - ], - [ - -36, - -84 - ], - [ - 75, - -66 - ], - [ - 28, - -118 - ], - [ - 42, - -110 - ], - [ - 48, - -92 - ], - [ - -2, - -89 - ], - [ - -43, - -33 - ], - [ - 16, - -64 - ], - [ - 41, - -37 - ], - [ - -10, - -98 - ], - [ - -18, - -95 - ], - [ - -39, - -10 - ], - [ - -51, - -130 - ], - [ - -56, - -158 - ], - [ - -65, - -143 - ], - [ - -96, - -111 - ], - [ - -97, - -101 - ], - [ - -79, - -13 - ], - [ - -42, - -54 - ], - [ - -24, - 39 - ], - [ - -40, - -59 - ], - [ - -97, - -60 - ], - [ - -74, - -19 - ], - [ - -24, - -127 - ], - [ - -38, - -7 - ], - [ - -19, - 88 - ], - [ - 17, - 46 - ], - [ - -94, - 38 - ], - [ - -33, - -19 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -70, - 31 - ], - [ - -33, - 49 - ], - [ - 11, - 69 - ], - [ - -64, - 22 - ], - [ - -33, - 45 - ], - [ - -60, - -64 - ], - [ - -67, - -14 - ], - [ - -56, - 1 - ], - [ - -37, - -30 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - -37, - -17 - ], - [ - 11, - -138 - ], - [ - -37, - 4 - ], - [ - -6, - 28 - ] - ], - [ - [ - 19601, - 13369 - ], - [ - -2, - 50 - ], - [ - -52, - -35 - ], - [ - -30, - 22 - ], - [ - -52, - 45 - ], - [ - 21, - 99 - ], - [ - -44, - 24 - ], - [ - -17, - 110 - ], - [ - -74, - -20 - ], - [ - 9, - 142 - ], - [ - 66, - 101 - ], - [ - 3, - 99 - ], - [ - -2, - 91 - ], - [ - -31, - 29 - ], - [ - -23, - 71 - ], - [ - -41, - -9 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - -75, - 18 - ], - [ - 23, - 50 - ], - [ - -32, - 75 - ], - [ - -50, - -51 - ], - [ - -58, - 30 - ], - [ - -81, - -77 - ], - [ - -63, - -89 - ], - [ - -56, - -15 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - -6, - 95 - ], - [ - -43, - -25 - ] - ], - [ - [ - 18690, - 14142 - ], - [ - -81, - 11 - ], - [ - -79, - 28 - ], - [ - -56, - 52 - ], - [ - -55, - 24 - ], - [ - -23, - 58 - ], - [ - -39, - 17 - ], - [ - -71, - 78 - ], - [ - -55, - 37 - ], - [ - -29, - -29 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -97, - 84 - ], - [ - -69, - 76 - ], - [ - -19, - 132 - ], - [ - 50, - -16 - ], - [ - 2, - 61 - ], - [ - -28, - 62 - ], - [ - 7, - 98 - ], - [ - -75, - 140 - ] - ], - [ - [ - 17973, - 15055 - ], - [ - -114, - 49 - ], - [ - -21, - 92 - ], - [ - -51, - 56 - ] - ], - [ - [ - 20239, - 13038 - ], - [ - -60, - -58 - ], - [ - -57, - 38 - ], - [ - -2, - 103 - ], - [ - 34, - 54 - ], - [ - 76, - 34 - ], - [ - 40, - -3 - ], - [ - 16, - -46 - ], - [ - -31, - -53 - ], - [ - -16, - -69 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - 19, - -171 - ], - [ - -29, - -100 - ], - [ - -19, - -136 - ], - [ - 31, - -103 - ], - [ - -4, - -48 - ] - ], - [ - [ - 12348, - 11396 - ], - [ - -31, - -1 - ], - [ - -49, - 24 - ], - [ - -45, - -2 - ], - [ - -82, - -21 - ], - [ - -49, - -34 - ], - [ - -69, - -44 - ], - [ - -13, - 3 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - 5, - 99 - ], - [ - 7, - 15 - ], - [ - -2, - 47 - ], - [ - -30, - 50 - ], - [ - -22, - 8 - ], - [ - -20, - 33 - ], - [ - 15, - 53 - ], - [ - -7, - 58 - ], - [ - 3, - 35 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - 11, - 0 - ], - [ - 4, - 53 - ], - [ - -5, - 23 - ], - [ - 7, - 17 - ], - [ - 26, - 14 - ], - [ - -18, - 96 - ], - [ - -16, - 50 - ], - [ - 6, - 40 - ], - [ - 14, - 10 - ] - ], - [ - [ - 11988, - 12022 - ], - [ - 9, - 11 - ], - [ - 19, - -18 - ], - [ - 54, - -1 - ], - [ - 13, - 35 - ], - [ - 12, - -3 - ], - [ - 20, - 14 - ], - [ - 11, - -52 - ], - [ - 16, - 16 - ], - [ - 29, - 17 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -5, - -65 - ], - [ - -56, - 29 - ], - [ - -56, - 31 - ], - [ - -88, - 5 - ] - ], - [ - [ - 13459, - 11069 - ], - [ - -9, - 7 - ], - [ - -41, - -16 - ], - [ - -42, - 16 - ], - [ - -33, - -8 - ] - ], - [ - [ - 13334, - 11068 - ], - [ - -114, - 3 - ] - ], - [ - [ - 13220, - 11071 - ], - [ - 10, - 95 - ], - [ - -27, - 79 - ], - [ - -32, - 21 - ], - [ - -14, - 53 - ], - [ - -18, - 18 - ], - [ - 1, - 33 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - 18, - 85 - ], - [ - 33, - 115 - ], - [ - 20, - 1 - ], - [ - 42, - 71 - ], - [ - 26, - 2 - ], - [ - 39, - -50 - ], - [ - 48, - 41 - ], - [ - 7, - 50 - ], - [ - 15, - 48 - ], - [ - 11, - 61 - ], - [ - 38, - 49 - ], - [ - 14, - 84 - ], - [ - 14, - 27 - ], - [ - 10, - 62 - ], - [ - 19, - 77 - ], - [ - 58, - 93 - ], - [ - 4, - 39 - ], - [ - 8, - 22 - ], - [ - -28, - 48 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - 2, - 38 - ], - [ - 20, - 7 - ] - ], - [ - [ - 13558, - 12340 - ], - [ - 28, - -77 - ], - [ - 4, - -79 - ], - [ - -2, - -80 - ], - [ - 38, - -109 - ], - [ - -39, - 1 - ], - [ - -20, - -9 - ], - [ - -32, - 12 - ], - [ - -15, - -56 - ], - [ - 41, - -70 - ], - [ - 31, - -21 - ], - [ - 10, - -49 - ], - [ - 22, - -83 - ], - [ - -11, - -32 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - -9, - 38 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - 19, - -13 - ], - [ - 24, - 46 - ], - [ - 38, - -1 - ], - [ - 4, - -34 - ], - [ - 26, - -21 - ], - [ - 41, - 75 - ], - [ - 41, - 59 - ], - [ - 17, - 38 - ], - [ - -2, - 99 - ], - [ - 30, - 116 - ], - [ - 32, - 62 - ], - [ - 46, - 58 - ], - [ - 8, - 38 - ], - [ - 2, - 44 - ], - [ - 11, - 42 - ], - [ - -3, - 68 - ], - [ - 8, - 106 - ], - [ - 14, - 75 - ], - [ - 21, - 64 - ], - [ - 4, - 73 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - 42, - -99 - ], - [ - 31, - -14 - ], - [ - 19, - 20 - ], - [ - 32, - -8 - ], - [ - 39, - 25 - ], - [ - 17, - -51 - ], - [ - 61, - -80 - ] - ], - [ - [ - 14697, - 11218 - ], - [ - -4, - -140 - ], - [ - 28, - -16 - ], - [ - -23, - -43 - ], - [ - -27, - -32 - ], - [ - -26, - -62 - ], - [ - -15, - -56 - ], - [ - -4, - -96 - ], - [ - -16, - -46 - ], - [ - -1, - -91 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - -20, - -33 - ], - [ - -2, - -72 - ], - [ - -10, - -9 - ], - [ - -6, - -65 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - 12, - -110 - ], - [ - -7, - -62 - ], - [ - 14, - -70 - ], - [ - 41, - -67 - ], - [ - 37, - -151 - ] - ], - [ - [ - 14690, - 9797 - ], - [ - -27, - 12 - ], - [ - -94, - -20 - ], - [ - -18, - -15 - ], - [ - -20, - -76 - ], - [ - 15, - -53 - ], - [ - -12, - -142 - ], - [ - -9, - -121 - ], - [ - 19, - -21 - ], - [ - 49, - -47 - ], - [ - 19, - 22 - ], - [ - 6, - -129 - ], - [ - -54, - 1 - ], - [ - -28, - 66 - ], - [ - -26, - 51 - ], - [ - -53, - 17 - ], - [ - -16, - 63 - ], - [ - -43, - -38 - ], - [ - -55, - 16 - ], - [ - -24, - 55 - ], - [ - -44, - 11 - ], - [ - -33, - -3 - ], - [ - -4, - 37 - ], - [ - -24, - 3 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - -57, - 127 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - 53, - 66 - ], - [ - -26, - 79 - ], - [ - 24, - 31 - ], - [ - 47, - 14 - ], - [ - 5, - 53 - ], - [ - 37, - -57 - ], - [ - 62, - -5 - ], - [ - 21, - 56 - ], - [ - 9, - 80 - ], - [ - -8, - 94 - ], - [ - -33, - 71 - ], - [ - 31, - 139 - ], - [ - -18, - 24 - ], - [ - -52, - -10 - ], - [ - -19, - 62 - ], - [ - 5, - 52 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - -35, - 63 - ], - [ - -20, - 3 - ], - [ - 45, - 122 - ], - [ - -54, - 56 - ], - [ - -42, - -10 - ], - [ - -25, - 21 - ], - [ - -38, - -32 - ], - [ - -52, - 15 - ], - [ - -41, - 126 - ], - [ - -32, - 31 - ], - [ - -23, - 57 - ], - [ - -46, - 56 - ], - [ - -19, - -11 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - -29, - 28 - ], - [ - -35, - 40 - ], - [ - -20, - -19 - ], - [ - -59, - 17 - ], - [ - -17, - 51 - ], - [ - -13, - -2 - ], - [ - -69, - 69 - ] - ], - [ - [ - 7051, - 10963 - ], - [ - -10, - 37 - ], - [ - 26, - 9 - ], - [ - -3, - 60 - ], - [ - 16, - 44 - ], - [ - 35, - 8 - ], - [ - 29, - 75 - ], - [ - 27, - 63 - ], - [ - -26, - 29 - ], - [ - 14, - 69 - ], - [ - -16, - 110 - ], - [ - 15, - 31 - ], - [ - -11, - 102 - ], - [ - -28, - 64 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - 8, - 58 - ], - [ - 23, - -8 - ], - [ - 13, - 35 - ], - [ - -16, - 71 - ], - [ - 8, - 17 - ] - ], - [ - [ - 7155, - 11837 - ], - [ - 36, - -3 - ], - [ - 53, - 83 - ], - [ - 28, - 13 - ], - [ - 1, - 40 - ], - [ - 13, - 101 - ], - [ - 40, - 56 - ], - [ - 44, - 2 - ], - [ - 5, - 25 - ], - [ - 55, - -10 - ], - [ - 55, - 61 - ], - [ - 27, - 26 - ], - [ - 34, - 58 - ], - [ - 24, - -7 - ], - [ - 19, - -32 - ], - [ - -14, - -40 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -45, - -20 - ], - [ - -17, - -60 - ], - [ - -27, - -35 - ], - [ - -21, - -44 - ], - [ - -8, - -86 - ], - [ - -19, - -70 - ], - [ - 36, - -8 - ], - [ - 8, - -55 - ], - [ - 16, - -26 - ], - [ - 5, - -49 - ], - [ - -8, - -44 - ], - [ - 3, - -25 - ], - [ - 17, - -10 - ], - [ - 16, - -42 - ], - [ - 90, - 12 - ], - [ - 40, - -16 - ], - [ - 49, - -103 - ], - [ - 29, - 13 - ], - [ - 50, - -7 - ], - [ - 40, - 14 - ], - [ - 24, - -21 - ], - [ - -12, - -64 - ], - [ - -16, - -40 - ], - [ - -5, - -86 - ], - [ - 14, - -80 - ], - [ - 20, - -36 - ], - [ - 2, - -27 - ], - [ - -35, - -59 - ], - [ - 25, - -27 - ], - [ - 18, - -42 - ], - [ - 22, - -119 - ] - ], - [ - [ - 6764, - 11784 - ], - [ - -38, - 27 - ], - [ - -14, - 25 - ], - [ - 8, - 21 - ], - [ - -2, - 26 - ], - [ - -20, - 29 - ], - [ - -27, - 23 - ], - [ - -24, - 16 - ], - [ - -5, - 35 - ], - [ - -18, - 21 - ], - [ - 4, - -35 - ], - [ - -13, - -28 - ], - [ - -16, - 33 - ], - [ - -23, - 12 - ], - [ - -9, - 24 - ], - [ - 0, - 37 - ], - [ - 9, - 37 - ], - [ - -19, - 17 - ], - [ - 16, - 23 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - 10, - 16 - ], - [ - 46, - -32 - ], - [ - 16, - 16 - ], - [ - 22, - -10 - ], - [ - 12, - -25 - ], - [ - 20, - -8 - ], - [ - 17, - 26 - ] - ], - [ - [ - 6716, - 12110 - ], - [ - 18, - -66 - ], - [ - 27, - -48 - ], - [ - 32, - -51 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - -27, - -11 - ], - [ - 1, - -48 - ], - [ - 14, - -18 - ], - [ - -10, - -14 - ], - [ - 3, - -22 - ], - [ - -6, - -24 - ], - [ - -4, - -24 - ] - ], - [ - [ - 6813, - 13579 - ], - [ - 60, - -8 - ], - [ - 55, - -2 - ], - [ - 65, - -41 - ], - [ - 28, - -44 - ], - [ - 65, - 14 - ], - [ - 25, - -28 - ], - [ - 59, - -75 - ], - [ - 43, - -54 - ], - [ - 23, - 2 - ], - [ - 42, - -24 - ], - [ - -5, - -34 - ], - [ - 51, - -5 - ], - [ - 53, - -49 - ], - [ - -9, - -28 - ], - [ - -46, - -16 - ], - [ - -47, - -6 - ], - [ - -48, - 10 - ], - [ - -100, - -12 - ], - [ - 47, - 67 - ], - [ - -28, - 31 - ], - [ - -45, - 8 - ], - [ - -24, - 35 - ], - [ - -17, - 68 - ], - [ - -39, - -4 - ], - [ - -65, - 32 - ], - [ - -21, - 25 - ], - [ - -91, - 19 - ], - [ - -24, - 23 - ], - [ - 26, - 30 - ], - [ - -69, - 6 - ], - [ - -50, - -62 - ], - [ - -29, - -2 - ], - [ - -10, - -29 - ], - [ - -34, - -13 - ], - [ - -30, - 11 - ], - [ - 37, - 37 - ], - [ - 15, - 43 - ], - [ - 31, - 27 - ], - [ - 36, - 23 - ], - [ - 53, - 12 - ], - [ - 17, - 13 - ] - ], - [ - [ - 14829, - 15013 - ], - [ - 5, - 1 - ], - [ - 10, - 28 - ], - [ - 50, - -1 - ], - [ - 64, - 36 - ], - [ - -47, - -51 - ], - [ - 5, - -23 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - -8, - 4 - ], - [ - -13, - -9 - ], - [ - -10, - 3 - ], - [ - -4, - -5 - ], - [ - -1, - 12 - ], - [ - -5, - 8 - ], - [ - -14, - 1 - ], - [ - -19, - -10 - ], - [ - -13, - 6 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - 2, - -10 - ], - [ - -72, - -48 - ], - [ - -34, - 15 - ], - [ - -16, - 48 - ], - [ - 33, - 5 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - -39, - 52 - ], - [ - -36, - 28 - ], - [ - -7, - 51 - ], - [ - -12, - 36 - ], - [ - 50, - 26 - ], - [ - 26, - 30 - ], - [ - 50, - 23 - ], - [ - 18, - 23 - ], - [ - 18, - -14 - ], - [ - 31, - 12 - ] - ], - [ - [ - 13594, - 16928 - ], - [ - 33, - -38 - ], - [ - 52, - -11 - ], - [ - -4, - -33 - ], - [ - 38, - -24 - ], - [ - 10, - 30 - ], - [ - 48, - -13 - ], - [ - 7, - -37 - ], - [ - 52, - -8 - ], - [ - 32, - -59 - ] - ], - [ - [ - 13862, - 16735 - ], - [ - -21, - 0 - ], - [ - -11, - -22 - ], - [ - -16, - -5 - ], - [ - -4, - -27 - ], - [ - -14, - -6 - ], - [ - -2, - -11 - ], - [ - -23, - -12 - ], - [ - -31, - 2 - ], - [ - -10, - -27 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 9, - 86 - ], - [ - 35, - 82 - ], - [ - -100, - 22 - ], - [ - -33, - 31 - ] - ], - [ - [ - 12979, - 16731 - ], - [ - 4, - 53 - ], - [ - -14, - 27 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -12, - 126 - ], - [ - 42, - 0 - ], - [ - 18, - 45 - ], - [ - 17, - 110 - ], - [ - -13, - 40 - ] - ], - [ - [ - 13029, - 17213 - ], - [ - 13, - 26 - ], - [ - 59, - 6 - ], - [ - 13, - -26 - ], - [ - 47, - 59 - ], - [ - -16, - 45 - ], - [ - -3, - 68 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - 53, - -16 - ], - [ - 44, - 18 - ] - ], - [ - [ - 13239, - 17393 - ], - [ - 1, - -46 - ], - [ - 71, - -28 - ], - [ - -1, - -42 - ], - [ - 71, - 22 - ], - [ - 39, - 33 - ], - [ - 79, - -47 - ], - [ - 33, - -39 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 16, - -61 - ], - [ - -19, - -32 - ], - [ - 25, - -42 - ], - [ - 17, - -65 - ], - [ - -5, - -41 - ], - [ - 28, - -77 - ] - ], - [ - [ - 15551, - 12321 - ], - [ - 16, - -37 - ], - [ - -2, - -50 - ], - [ - -40, - -29 - ], - [ - 30, - -33 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - -26, - -64 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - 21 - ], - [ - -17, - -8 - ], - [ - -39, - 2 - ], - [ - -1, - 36 - ], - [ - -5, - 34 - ], - [ - 23, - 56 - ], - [ - 25, - 53 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - 30, - -11 - ], - [ - 21, - 30 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - -28, - 67 - ], - [ - -3, - 122 - ], - [ - 12, - 33 - ], - [ - 20, - 36 - ], - [ - 61, - 7 - ], - [ - 25, - 33 - ], - [ - 56, - 34 - ], - [ - -2, - -62 - ], - [ - -21, - -39 - ], - [ - 8, - -33 - ], - [ - 38, - -19 - ], - [ - -17, - -45 - ], - [ - -21, - 13 - ], - [ - -50, - -86 - ], - [ - 19, - -59 - ] - ], - [ - [ - 13432, - 17469 - ], - [ - -42, - -98 - ], - [ - -73, - 68 - ], - [ - -9, - 50 - ], - [ - 102, - 40 - ], - [ - 22, - -60 - ] - ], - [ - [ - 7549, - 13162 - ], - [ - 8, - 21 - ], - [ - 55, - -1 - ], - [ - 41, - -31 - ], - [ - 18, - 3 - ], - [ - 13, - -42 - ], - [ - 38, - 2 - ], - [ - -2, - -36 - ], - [ - 31, - -4 - ], - [ - 34, - -44 - ], - [ - -26, - -49 - ], - [ - -33, - 26 - ], - [ - -32, - -5 - ], - [ - -23, - 6 - ], - [ - -12, - -22 - ], - [ - -27, - -7 - ], - [ - -11, - 29 - ], - [ - -23, - -17 - ], - [ - -28, - -83 - ], - [ - -18, - 20 - ], - [ - -3, - 34 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - 1, - 33 - ], - [ - -18, - 36 - ], - [ - 17, - 20 - ], - [ - 6, - 46 - ], - [ - -6, - 65 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - -77, - -12 - ], - [ - -1, - 77 - ], - [ - -32, - 19 - ], - [ - -44, - 35 - ], - [ - -16, - 56 - ], - [ - -236, - 262 - ], - [ - -235, - 261 - ] - ], - [ - [ - 12204, - 13793 - ], - [ - -262, - 291 - ] - ], - [ - [ - 11942, - 14084 - ], - [ - 1, - 23 - ], - [ - 0, - 8 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - 0, - 142 - ], - [ - 112, - 89 - ], - [ - 70, - 18 - ], - [ - 57, - 32 - ], - [ - 27, - 60 - ], - [ - 81, - 48 - ], - [ - 3, - 89 - ], - [ - 41, - 10 - ], - [ - 31, - 45 - ], - [ - 91, - 20 - ], - [ - 13, - 46 - ], - [ - -18, - 26 - ], - [ - -24, - 127 - ], - [ - -4, - 72 - ], - [ - -27, - 77 - ] - ], - [ - [ - 12396, - 15016 - ], - [ - 67, - 66 - ], - [ - 76, - 21 - ], - [ - 44, - 49 - ], - [ - 67, - 37 - ], - [ - 118, - 21 - ], - [ - 115, - 10 - ], - [ - 35, - -18 - ], - [ - 66, - 47 - ], - [ - 74, - 1 - ], - [ - 29, - -28 - ], - [ - 48, - 8 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - -15, - -62 - ], - [ - 11, - -114 - ], - [ - -16, - -99 - ], - [ - -43, - -67 - ], - [ - 6, - -91 - ], - [ - 57, - -71 - ], - [ - 1, - -29 - ], - [ - 43, - -48 - ], - [ - 29, - -216 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 23, - -106 - ], - [ - 4, - -56 - ], - [ - -12, - -97 - ], - [ - 5, - -55 - ], - [ - -9, - -66 - ], - [ - 6, - -75 - ], - [ - -28, - -50 - ], - [ - 41, - -88 - ], - [ - 3, - -51 - ], - [ - 25, - -67 - ], - [ - 32, - 22 - ], - [ - 55, - -56 - ], - [ - 31, - -75 - ] - ], - [ - [ - 13384, - 13613 - ], - [ - -239, - -229 - ], - [ - -202, - -235 - ], - [ - -98, - -54 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - 10, - -91 - ], - [ - -22, - -78 - ], - [ - -76, - -126 - ], - [ - -83, - -47 - ], - [ - -43, - -104 - ], - [ - -13, - -81 - ], - [ - -40, - -50 - ], - [ - -29, - 61 - ], - [ - -28, - 13 - ], - [ - -29, - -10 - ], - [ - -2, - 44 - ], - [ - 20, - 29 - ], - [ - -8, - 50 - ] - ], - [ - [ - 6950, - 10389 - ], - [ - 37, - 89 - ], - [ - -15, - 53 - ], - [ - -27, - -56 - ], - [ - -42, - 53 - ], - [ - 15, - 33 - ], - [ - -12, - 109 - ], - [ - 24, - 18 - ], - [ - 13, - 75 - ], - [ - 26, - 77 - ], - [ - -4, - 49 - ], - [ - 38, - 26 - ], - [ - 48, - 48 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - -276, - 0 - ], - [ - -271, - 0 - ], - [ - -280, - 0 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - 441 - ], - [ - 0, - 427 - ], - [ - -21, - 97 - ], - [ - 18, - 74 - ], - [ - -11, - 51 - ], - [ - 26, - 58 - ] - ], - [ - [ - 14302, - 14585 - ], - [ - 92, - 1 - ], - [ - 68, - -31 - ], - [ - 69, - -36 - ], - [ - 32, - -18 - ], - [ - 54, - 38 - ], - [ - 28, - 34 - ], - [ - 62, - 10 - ], - [ - 49, - -15 - ], - [ - 19, - -60 - ], - [ - 17, - 39 - ], - [ - 55, - -28 - ], - [ - 55, - -7 - ], - [ - 34, - 31 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 39, - -175 - ], - [ - 7, - -32 - ] - ], - [ - [ - 14982, - 14336 - ], - [ - -20, - -48 - ], - [ - -15, - -90 - ], - [ - -19, - -63 - ], - [ - -16, - -21 - ], - [ - -23, - 39 - ], - [ - -32, - 53 - ], - [ - -49, - 172 - ], - [ - -7, - -10 - ], - [ - 28, - -127 - ], - [ - 43, - -121 - ], - [ - 53, - -187 - ], - [ - 26, - -65 - ], - [ - 22, - -68 - ], - [ - 63, - -132 - ], - [ - -14, - -21 - ], - [ - 2, - -78 - ], - [ - 81, - -108 - ], - [ - 12, - -24 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - -24, - 39 - ], - [ - -29, - 70 - ], - [ - -31, - 39 - ], - [ - -18, - 41 - ], - [ - -60, - 48 - ], - [ - -48, - 2 - ], - [ - -17, - 25 - ], - [ - -41, - -29 - ], - [ - -42, - 55 - ], - [ - -22, - -90 - ], - [ - -81, - 25 - ] - ], - [ - [ - 15087, - 12527 - ], - [ - -7, - 48 - ], - [ - 30, - 177 - ], - [ - 6, - 79 - ], - [ - 22, - 37 - ], - [ - 52, - 20 - ], - [ - 35, - 68 - ] - ], - [ - [ - 15225, - 12956 - ], - [ - 40, - -138 - ], - [ - 20, - -111 - ], - [ - 38, - -58 - ], - [ - 95, - -113 - ], - [ - 39, - -69 - ], - [ - 38, - -69 - ], - [ - 21, - -41 - ], - [ - 35, - -36 - ] - ], - [ - [ - 11918, - 15822 - ], - [ - 3, - 85 - ], - [ - -28, - 52 - ], - [ - 98, - 87 - ], - [ - 86, - -22 - ], - [ - 93, - 1 - ], - [ - 74, - -21 - ], - [ - 58, - 7 - ], - [ - 113, - -4 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 28, - -47 - ], - [ - 128, - -55 - ], - [ - 25, - 26 - ], - [ - 79, - -54 - ], - [ - 81, - 16 - ] - ], - [ - [ - 12756, - 15893 - ], - [ - 3, - -70 - ], - [ - -66, - -80 - ], - [ - -89, - -25 - ], - [ - -6, - -41 - ], - [ - -43, - -66 - ], - [ - -27, - -98 - ], - [ - 27, - -68 - ], - [ - -40, - -54 - ], - [ - -15, - -78 - ], - [ - -53, - -24 - ], - [ - -49, - -92 - ], - [ - -89, - -2 - ], - [ - -66, - 2 - ], - [ - -44, - -42 - ], - [ - -26, - -45 - ], - [ - -34, - 10 - ], - [ - -26, - 40 - ], - [ - -20, - 69 - ], - [ - -65, - 19 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -6, - 39 - ], - [ - 26, - 45 - ], - [ - 10, - 33 - ], - [ - -25, - 36 - ], - [ - 20, - 79 - ], - [ - -28, - 72 - ], - [ - 30, - 9 - ], - [ - 3, - 57 - ], - [ - 11, - 18 - ], - [ - 1, - 93 - ], - [ - 32, - 33 - ], - [ - -19, - 60 - ], - [ - -41, - 4 - ], - [ - -12, - -15 - ], - [ - -41, - 0 - ], - [ - -18, - 59 - ], - [ - -28, - -18 - ], - [ - -25, - -30 - ] - ], - [ - [ - 14242, - 17731 - ], - [ - 8, - 70 - ], - [ - -25, - -15 - ], - [ - -44, - 43 - ], - [ - -7, - 69 - ], - [ - 89, - 33 - ], - [ - 87, - 18 - ], - [ - 76, - -20 - ], - [ - 72, - 3 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 11, - -21 - ], - [ - -50, - -69 - ], - [ - 21, - -112 - ], - [ - -30, - -38 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - -58, - 1 - ], - [ - -60, - 44 - ], - [ - -30, - 15 - ], - [ - -60, - -21 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - -42 - ], - [ - 26, - -66 - ], - [ - 26, - -58 - ], - [ - 26, - -43 - ], - [ - 228, - -142 - ], - [ - 59, - 0 - ] - ], - [ - [ - 15879, - 11757 - ], - [ - -197, - -360 - ], - [ - -91, - -5 - ], - [ - -62, - -85 - ], - [ - -45, - -2 - ], - [ - -19, - -38 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -47, - 0 - ], - [ - -29, - 41 - ], - [ - -63, - -50 - ], - [ - -21, - -50 - ], - [ - -46, - 9 - ], - [ - -16, - 14 - ], - [ - -16, - -3 - ], - [ - -22, - 1 - ], - [ - -88, - 102 - ], - [ - -49, - 0 - ], - [ - -24, - 39 - ], - [ - 0, - 68 - ], - [ - -36, - 20 - ] - ], - [ - [ - 15008, - 11458 - ], - [ - -41, - 130 - ], - [ - -32, - 28 - ], - [ - -12, - 48 - ], - [ - -36, - 59 - ], - [ - -42, - 8 - ], - [ - 23, - 68 - ], - [ - 37, - 3 - ], - [ - 11, - 37 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 108 - ], - [ - 21, - 125 - ], - [ - 33, - 34 - ], - [ - 7, - 49 - ], - [ - 29, - 92 - ], - [ - 42, - 59 - ], - [ - 29, - 118 - ], - [ - 11, - 103 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -24, - 47 - ], - [ - -2, - 184 - ], - [ - -108, - 82 - ], - [ - -93, - 59 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - 41, - 31 - ], - [ - 78, - -63 - ], - [ - 91, - 6 - ], - [ - 75, - -29 - ], - [ - 66, - 53 - ], - [ - 34, - 88 - ], - [ - 109, - 41 - ], - [ - 89, - -48 - ], - [ - -29, - -84 - ] - ], - [ - [ - 14541, - 19083 - ], - [ - -11, - -84 - ], - [ - 107, - -80 - ], - [ - -64, - -91 - ], - [ - 81, - -136 - ], - [ - -47, - -103 - ], - [ - 63, - -89 - ], - [ - -29, - -78 - ], - [ - 103, - -83 - ], - [ - -26, - -61 - ], - [ - -65, - -69 - ], - [ - -149, - -153 - ] - ], - [ - [ - 14504, - 18056 - ], - [ - -126, - -10 - ], - [ - -123, - -44 - ], - [ - -113, - -25 - ], - [ - -41, - 65 - ], - [ - -67, - 40 - ], - [ - 15, - 118 - ], - [ - -33, - 108 - ], - [ - 33, - 70 - ], - [ - 63, - 75 - ], - [ - 159, - 130 - ], - [ - 47, - 26 - ], - [ - -7, - 50 - ], - [ - -97, - 57 - ] - ], - [ - [ - 24982, - 8717 - ], - [ - 24, - -35 - ], - [ - -12, - -62 - ], - [ - -43, - -17 - ], - [ - -39, - 15 - ], - [ - -6, - 53 - ], - [ - 27, - 41 - ], - [ - 31, - -15 - ], - [ - 18, - 20 - ] - ], - [ - [ - 25051, - 8782 - ], - [ - -45, - -26 - ], - [ - -9, - 45 - ], - [ - 35, - 25 - ], - [ - 22, - 6 - ], - [ - 41, - 38 - ], - [ - 0, - -59 - ], - [ - -44, - -29 - ] - ], - [ - [ - 6, - 8817 - ], - [ - -6, - -6 - ], - [ - 0, - 59 - ], - [ - 14, - 5 - ], - [ - -8, - -58 - ] - ], - [ - [ - 8281, - 4577 - ], - [ - 84, - 72 - ], - [ - 59, - -30 - ], - [ - 42, - 48 - ], - [ - 56, - -54 - ], - [ - -21, - -42 - ], - [ - -94, - -36 - ], - [ - -32, - 42 - ], - [ - -59, - -54 - ], - [ - -35, - 54 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - 16, - -10 - ], - [ - 20, - 2 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -3, - -34 - ], - [ - 20, - -45 - ], - [ - -24, - -37 - ], - [ - 18, - -93 - ], - [ - 38, - -15 - ], - [ - -8, - -52 - ] - ], - [ - [ - 13066, - 16039 - ], - [ - -63, - -68 - ], - [ - -138, - 33 - ], - [ - -101, - -39 - ], - [ - -8, - -72 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 36, - 72 - ], - [ - 13, - 239 - ], - [ - -72, - 125 - ], - [ - -51, - 61 - ], - [ - -107, - 46 - ], - [ - -7, - 88 - ], - [ - 91, - 26 - ], - [ - 117, - -31 - ], - [ - -22, - 136 - ], - [ - 66, - -52 - ], - [ - 162, - 94 - ], - [ - 21, - 98 - ], - [ - 61, - 24 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 17, - 51 - ], - [ - 6, - 54 - ], - [ - 12, - 52 - ], - [ - -27, - 71 - ], - [ - -5, - 82 - ], - [ - 36, - 103 - ] - ], - [ - [ - 8786, - 11488 - ], - [ - 24, - -13 - ], - [ - 51, - -29 - ], - [ - 74, - -101 - ], - [ - 12, - -49 - ] - ], - [ - [ - 13214, - 15854 - ], - [ - -23, - -92 - ], - [ - -32, - 24 - ], - [ - -16, - 81 - ], - [ - 14, - 44 - ], - [ - 45, - 46 - ], - [ - 12, - -103 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - -72, - 121 - ], - [ - -46, - 99 - ], - [ - -42, - 124 - ], - [ - 2, - 40 - ], - [ - 15, - 38 - ], - [ - 17, - 87 - ], - [ - 14, - 89 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - 24, - 7 - ], - [ - 101, - -1 - ], - [ - 0, - 144 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - -52, - 24 - ], - [ - -43, - -1 - ], - [ - 14, - 64 - ], - [ - -14, - 64 - ] - ], - [ - [ - 12020, - 17411 - ], - [ - 58, - 5 - ], - [ - 75, - -74 - ], - [ - -38, - -82 - ] - ], - [ - [ - 12338, - 17832 - ], - [ - -74, - -130 - ], - [ - 71, - 16 - ], - [ - 76, - 0 - ], - [ - -18, - -98 - ], - [ - -63, - -108 - ], - [ - 72, - -7 - ], - [ - 6, - -13 - ], - [ - 62, - -142 - ], - [ - 47, - -19 - ], - [ - 43, - -136 - ], - [ - 20, - -48 - ], - [ - 85, - -23 - ], - [ - -9, - -76 - ], - [ - -35, - -36 - ], - [ - 28, - -62 - ], - [ - -63, - -63 - ], - [ - -93, - 2 - ], - [ - -119, - -33 - ], - [ - -33, - 23 - ], - [ - -46, - -56 - ], - [ - -64, - 14 - ], - [ - -49, - -46 - ], - [ - -37, - 24 - ], - [ - 102, - 126 - ], - [ - 62, - 26 - ], - [ - 0, - 0 - ], - [ - -109, - 20 - ], - [ - -20, - 48 - ], - [ - 73, - 37 - ], - [ - -38, - 64 - ], - [ - 13, - 79 - ], - [ - 104, - -11 - ], - [ - 10, - 70 - ], - [ - -46, - 74 - ], - [ - -2, - 1 - ], - [ - -84, - 21 - ], - [ - -17, - 33 - ], - [ - 26, - 53 - ], - [ - -23, - 34 - ], - [ - -38, - -57 - ], - [ - -4, - 115 - ], - [ - -35, - 62 - ], - [ - 25, - 124 - ], - [ - 54, - 97 - ], - [ - 56, - -10 - ], - [ - 84, - 11 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - -68, - 59 - ], - [ - -74, - -6 - ] - ], - [ - [ - 15444, - 15780 - ], - [ - 11, - 51 - ], - [ - -18, - 82 - ], - [ - -40, - 44 - ], - [ - -39, - 14 - ], - [ - -25, - 37 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - 8, - 14 - ], - [ - 59, - -20 - ], - [ - 103, - -20 - ], - [ - 95, - -57 - ], - [ - 12, - -23 - ], - [ - 42, - 19 - ], - [ - 65, - -25 - ], - [ - 21, - -49 - ], - [ - 44, - -28 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -5, - -37 - ], - [ - 29, - -62 - ], - [ - 0, - -87 - ], - [ - 7, - -95 - ], - [ - 17, - -44 - ], - [ - -15, - -108 - ], - [ - 5, - -59 - ], - [ - 19, - -76 - ], - [ - 15, - -43 - ] - ], - [ - [ - 12621, - 11508 - ], - [ - -109, - -70 - ], - [ - -39, - -41 - ], - [ - -62, - -35 - ], - [ - -63, - 34 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - -20, - 3 - ], - [ - -14, - -48 - ], - [ - -19, - 1 - ], - [ - -14, - 25 - ], - [ - 5, - 48 - ], - [ - -30, - 74 - ], - [ - -18, - -14 - ], - [ - -15, - -2 - ] - ], - [ - [ - 11834, - 11806 - ], - [ - -19, - -7 - ], - [ - 1, - 44 - ], - [ - -11, - 31 - ], - [ - 2, - 35 - ], - [ - -15, - 50 - ], - [ - -19, - 43 - ], - [ - -56, - 1 - ], - [ - -16, - -23 - ], - [ - -20, - -3 - ], - [ - -12, - -26 - ], - [ - -8, - -33 - ], - [ - -37, - -53 - ] - ], - [ - [ - 11624, - 11865 - ], - [ - -30, - 71 - ], - [ - -28, - 47 - ], - [ - -17, - 16 - ], - [ - -18, - 24 - ], - [ - -8, - 53 - ], - [ - -10, - 26 - ], - [ - -20, - 20 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - 31, - 58 - ], - [ - 21, - -2 - ], - [ - 18, - 20 - ], - [ - 15, - 0 - ], - [ - 11, - 16 - ], - [ - -5, - 40 - ], - [ - 7, - 12 - ], - [ - 1, - 41 - ] - ], - [ - [ - 11592, - 12307 - ], - [ - 34, - -1 - ], - [ - 50, - -29 - ], - [ - 16, - 2 - ], - [ - 5, - 14 - ], - [ - 38, - -10 - ], - [ - 10, - 7 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 4, - -44 - ], - [ - 11, - 0 - ], - [ - 18, - 16 - ], - [ - 12, - -4 - ], - [ - 19, - -30 - ], - [ - 30, - -10 - ], - [ - 19, - 26 - ], - [ - 23, - 16 - ], - [ - 16, - 17 - ], - [ - 14, - -3 - ], - [ - 16, - -27 - ], - [ - 8, - -33 - ], - [ - 29, - -50 - ], - [ - -15, - -31 - ], - [ - -2, - -39 - ], - [ - 14, - 12 - ], - [ - 9, - -14 - ], - [ - -4, - -36 - ], - [ - 22, - -34 - ] - ], - [ - [ - 11374, - 12375 - ], - [ - 8, - 53 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - 76, - 4 - ], - [ - 16, - 28 - ], - [ - 22, - 2 - ], - [ - 28, - -30 - ], - [ - 21, - 0 - ], - [ - 23, - 20 - ], - [ - 14, - -35 - ], - [ - -30, - -27 - ], - [ - -30, - 3 - ], - [ - -30, - 25 - ], - [ - -26, - -28 - ], - [ - -12, - -1 - ], - [ - -17, - -17 - ], - [ - -63, - 3 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - -37, - 50 - ], - [ - -30, - 8 - ], - [ - -16, - 34 - ], - [ - 1, - 18 - ], - [ - -22, - 25 - ], - [ - -4, - 26 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - 37, - 20 - ], - [ - 23, - -4 - ], - [ - 19, - 13 - ], - [ - 128, - -5 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - -13, - 18 - ], - [ - 24, - 135 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - 45, - 11 - ], - [ - 27, - 26 - ], - [ - 38, - -2 - ], - [ - 11, - 20 - ], - [ - 13, - 4 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - 34, - -32 - ], - [ - -22, - -75 - ], - [ - -16, - -13 - ] - ], - [ - [ - 14364, - 15695 - ], - [ - -43, - 3 - ], - [ - -36, - 12 - ], - [ - -84, - -32 - ], - [ - 48, - -67 - ], - [ - -35, - -20 - ], - [ - -39, - 0 - ], - [ - -37, - 62 - ], - [ - -13, - -26 - ], - [ - 15, - -72 - ], - [ - 35, - -56 - ], - [ - -26, - -27 - ], - [ - 39, - -55 - ], - [ - 34, - -35 - ], - [ - 1, - -67 - ], - [ - -64, - 31 - ], - [ - 20, - -61 - ], - [ - -44, - -12 - ], - [ - 27, - -106 - ], - [ - -47, - -2 - ], - [ - -57, - 52 - ], - [ - -26, - 96 - ], - [ - -12, - 80 - ], - [ - -27, - 55 - ], - [ - -36, - 69 - ], - [ - -5, - 34 - ] - ], - [ - [ - 14200, - 15081 - ], - [ - 38, - -41 - ], - [ - 54, - 7 - ], - [ - 52, - -8 - ], - [ - -2, - -21 - ], - [ - 38, - 14 - ], - [ - -9, - -35 - ], - [ - -100, - -10 - ], - [ - 1, - 19 - ], - [ - -85, - 24 - ], - [ - 13, - 51 - ] - ], - [ - [ - 9288, - 20710 - ], - [ - 234, - 72 - ], - [ - 244, - -6 - ], - [ - 89, - 44 - ], - [ - 247, - 12 - ], - [ - 556, - -15 - ], - [ - 436, - -95 - ], - [ - -128, - -46 - ], - [ - -267, - -6 - ], - [ - -375, - -11 - ], - [ - 35, - -22 - ], - [ - 247, - 13 - ], - [ - 210, - -41 - ], - [ - 135, - 37 - ], - [ - 58, - -43 - ], - [ - -77, - -70 - ], - [ - 178, - 45 - ], - [ - 338, - 46 - ], - [ - 209, - -23 - ], - [ - 39, - -51 - ], - [ - -284, - -86 - ], - [ - -39, - -27 - ], - [ - -223, - -21 - ], - [ - 162, - -6 - ], - [ - -82, - -87 - ], - [ - -56, - -78 - ], - [ - 2, - -134 - ], - [ - 84, - -78 - ], - [ - -109, - -5 - ], - [ - -115, - -38 - ], - [ - 129, - -63 - ], - [ - 16, - -102 - ], - [ - -74, - -11 - ], - [ - 90, - -104 - ], - [ - -155, - -8 - ], - [ - 81, - -49 - ], - [ - -23, - -42 - ], - [ - -98, - -19 - ], - [ - -97, - 0 - ], - [ - 87, - -82 - ], - [ - 1, - -53 - ], - [ - -138, - 50 - ], - [ - -36, - -32 - ], - [ - 94, - -30 - ], - [ - 92, - -74 - ], - [ - 26, - -96 - ], - [ - -124, - -23 - ], - [ - -54, - 46 - ], - [ - -86, - 69 - ], - [ - 24, - -82 - ], - [ - -81, - -63 - ], - [ - 184, - -5 - ], - [ - 96, - -6 - ], - [ - -187, - -105 - ], - [ - -190, - -94 - ], - [ - -204, - -42 - ], - [ - -77, - 0 - ], - [ - -72, - -47 - ], - [ - -97, - -126 - ], - [ - -150, - -84 - ], - [ - -48, - -5 - ], - [ - -93, - -30 - ], - [ - -100, - -28 - ], - [ - -59, - -74 - ], - [ - -1, - -84 - ], - [ - -36, - -79 - ], - [ - -113, - -96 - ], - [ - 28, - -94 - ], - [ - -32, - -99 - ], - [ - -35, - -117 - ], - [ - -99, - -7 - ], - [ - -102, - 98 - ], - [ - -140, - 0 - ], - [ - -67, - 66 - ], - [ - -47, - 117 - ], - [ - -121, - 149 - ], - [ - -35, - 79 - ], - [ - -10, - 107 - ], - [ - -96, - 111 - ], - [ - 25, - 88 - ], - [ - -47, - 43 - ], - [ - 69, - 140 - ], - [ - 105, - 45 - ], - [ - 28, - 50 - ], - [ - 14, - 94 - ], - [ - -79, - -43 - ], - [ - -38, - -18 - ], - [ - -63, - -17 - ], - [ - -85, - 39 - ], - [ - -5, - 82 - ], - [ - 27, - 64 - ], - [ - 65, - 1 - ], - [ - 142, - -32 - ], - [ - -120, - 77 - ], - [ - -62, - 41 - ], - [ - -69, - -17 - ], - [ - -59, - 29 - ], - [ - 78, - 112 - ], - [ - -42, - 45 - ], - [ - -56, - 83 - ], - [ - -83, - 127 - ], - [ - -89, - 47 - ], - [ - 1, - 50 - ], - [ - -187, - 70 - ], - [ - -148, - 9 - ], - [ - -187, - -5 - ], - [ - -170, - -9 - ], - [ - -81, - 38 - ], - [ - -121, - 76 - ], - [ - 183, - 38 - ], - [ - 140, - 6 - ], - [ - -298, - 31 - ], - [ - -157, - 49 - ], - [ - 10, - 47 - ], - [ - 264, - 57 - ], - [ - 255, - 58 - ], - [ - 27, - 44 - ], - [ - -188, - 43 - ], - [ - 60, - 48 - ], - [ - 242, - 83 - ], - [ - 101, - 13 - ], - [ - -29, - 54 - ], - [ - 165, - 32 - ], - [ - 215, - 19 - ], - [ - 214, - 1 - ], - [ - 76, - -38 - ], - [ - 185, - 66 - ], - [ - 166, - -45 - ], - [ - 98, - -9 - ], - [ - 145, - -39 - ], - [ - -166, - 65 - ], - [ - 10, - 51 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - 23, - -22 - ], - [ - 6, - 18 - ], - [ - 20, - -15 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - -31, - -46 - ], - [ - -33, - -33 - ], - [ - -5, - -23 - ], - [ - 5, - -24 - ], - [ - -14, - -30 - ] - ], - [ - [ - 6319, - 12528 - ], - [ - -16, - -8 - ], - [ - 3, - -14 - ], - [ - -13, - -13 - ], - [ - -24, - -30 - ], - [ - -2, - -18 - ] - ], - [ - [ - 6267, - 12445 - ], - [ - -36, - 21 - ], - [ - -43, - 2 - ], - [ - -32, - 24 - ], - [ - -38, - 49 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - 2, - 35 - ], - [ - 8, - 28 - ], - [ - -10, - 23 - ], - [ - 34, - 98 - ], - [ - 89, - 0 - ], - [ - 2, - 41 - ], - [ - -11, - 7 - ], - [ - -8, - 26 - ], - [ - -26, - 28 - ], - [ - -26, - 40 - ], - [ - 32, - 0 - ], - [ - 0, - 68 - ], - [ - 65, - 0 - ], - [ - 64, - -1 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - -47, - 91 - ], - [ - 19, - 33 - ], - [ - -2, - 56 - ], - [ - 43, - 19 - ], - [ - 17, - 22 - ], - [ - -23, - 45 - ], - [ - 6, - 44 - ], - [ - 55, - 70 - ] - ], - [ - [ - 8382, - 11801 - ], - [ - 46, - -44 - ], - [ - 43, - -78 - ], - [ - 2, - -62 - ], - [ - 26, - -3 - ], - [ - 37, - -58 - ], - [ - 28, - -42 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - -11, - -108 - ], - [ - -43, - -31 - ], - [ - 4, - -29 - ], - [ - -13, - -62 - ], - [ - 31, - -87 - ], - [ - 23, - 0 - ], - [ - 9, - -68 - ], - [ - 42, - -104 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - 8, - -5 - ], - [ - 15, - 21 - ], - [ - 20, - 2 - ], - [ - 6, - -10 - ], - [ - 11, - 6 - ], - [ - 33, - -10 - ], - [ - 32, - 3 - ], - [ - 22, - 13 - ], - [ - 8, - 13 - ], - [ - 23, - -6 - ], - [ - 16, - -8 - ], - [ - 19, - 3 - ], - [ - 13, - 10 - ], - [ - 32, - -16 - ], - [ - 11, - -3 - ], - [ - 22, - -23 - ], - [ - 20, - -26 - ], - [ - 25, - -19 - ], - [ - 18, - -33 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -23, - 3 - ], - [ - -10, - -17 - ], - [ - -24, - -15 - ], - [ - -18, - 0 - ], - [ - -15, - -16 - ], - [ - -14, - 6 - ], - [ - -12, - 18 - ], - [ - -7, - -3 - ], - [ - -9, - -29 - ], - [ - -7, - 1 - ], - [ - -1, - -25 - ], - [ - -25, - -33 - ], - [ - -12, - -14 - ], - [ - -8, - -15 - ], - [ - -20, - 24 - ], - [ - -15, - -32 - ], - [ - -15, - 1 - ], - [ - -16, - -3 - ], - [ - 1, - -59 - ], - [ - -10, - -1 - ], - [ - -9, - -27 - ], - [ - -21, - -5 - ] - ], - [ - [ - 6461, - 12355 - ], - [ - -12, - 37 - ], - [ - -21, - 11 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - 4, - 48 - ], - [ - -9, - 13 - ], - [ - -14, - 9 - ], - [ - -31, - -15 - ], - [ - -3, - 16 - ], - [ - -21, - 20 - ], - [ - -15, - 24 - ], - [ - -20, - 10 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -7, - -21 - ] - ], - [ - [ - 13834, - 15893 - ], - [ - -66, - 45 - ], - [ - -40, - 43 - ], - [ - -64, - 36 - ], - [ - -59, - 88 - ], - [ - 14, - 9 - ], - [ - -31, - 50 - ], - [ - -2, - 41 - ], - [ - -45, - 19 - ], - [ - -21, - -52 - ], - [ - -20, - 40 - ], - [ - 1, - 42 - ], - [ - 3, - 2 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 48, - -4 - ], - [ - 13, - 20 - ], - [ - 24, - -20 - ], - [ - 27, - -2 - ], - [ - 0, - 34 - ], - [ - 24, - 12 - ], - [ - 7, - 48 - ], - [ - 55, - 32 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - 22, - -15 - ], - [ - 52, - -51 - ], - [ - 58, - -23 - ], - [ - 26, - 18 - ] - ], - [ - [ - 13860, - 16305 - ], - [ - 17, - -47 - ], - [ - 22, - -34 - ], - [ - -27, - -45 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - -46, - 20 - ], - [ - -33, - -8 - ], - [ - -43, - 9 - ], - [ - -33, - -23 - ], - [ - -37, - 38 - ], - [ - 6, - 38 - ], - [ - 64, - -16 - ], - [ - 53, - -10 - ], - [ - 25, - 27 - ], - [ - -32, - 52 - ], - [ - 1, - 46 - ], - [ - -44, - 18 - ], - [ - 16, - 33 - ], - [ - 42, - -5 - ], - [ - 61, - -19 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - 36, - -31 - ], - [ - 25, - -13 - ], - [ - 59, - 14 - ], - [ - 5, - 25 - ], - [ - 28, - 3 - ], - [ - 34, - 19 - ], - [ - 8, - -8 - ], - [ - 32, - 15 - ], - [ - 17, - 28 - ], - [ - 23, - 8 - ], - [ - 74, - -37 - ], - [ - 15, - 12 - ] - ], - [ - [ - 14087, - 16606 - ], - [ - 39, - -32 - ], - [ - 5, - -32 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - -43, - -26 - ], - [ - -33, - -81 - ], - [ - -42, - -81 - ], - [ - -56, - -23 - ] - ], - [ - [ - 13957, - 16331 - ], - [ - -43, - 5 - ], - [ - -54, - -31 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - -13, - 41 - ], - [ - -12, - 1 - ] - ], - [ - [ - 20962, - 9569 - ], - [ - -29, - -3 - ], - [ - -92, - 85 - ], - [ - 65, - 23 - ], - [ - 36, - -36 - ], - [ - 25, - -37 - ], - [ - -5, - -32 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 7, - -23 - ], - [ - 1, - -37 - ] - ], - [ - [ - 21267, - 9670 - ], - [ - -45, - -89 - ], - [ - -60, - -27 - ], - [ - -8, - 15 - ], - [ - 6, - 40 - ], - [ - 30, - 74 - ], - [ - 69, - 47 - ] - ], - [ - [ - 20766, - 9826 - ], - [ - 25, - -32 - ], - [ - 43, - 10 - ], - [ - 18, - -51 - ], - [ - -81, - -24 - ], - [ - -48, - -16 - ], - [ - -38, - 1 - ], - [ - 24, - 69 - ], - [ - 38, - 1 - ], - [ - 19, - 42 - ] - ], - [ - [ - 21115, - 9826 - ], - [ - -10, - -67 - ], - [ - -105, - -34 - ], - [ - -93, - 15 - ], - [ - 0, - 44 - ], - [ - 55, - 25 - ], - [ - 44, - -36 - ], - [ - 46, - 9 - ], - [ - 63, - 44 - ] - ], - [ - [ - 20119, - 9984 - ], - [ - 134, - -12 - ], - [ - 15, - 50 - ], - [ - 130, - -58 - ], - [ - 25, - -78 - ], - [ - 105, - -22 - ], - [ - 85, - -71 - ], - [ - -79, - -46 - ], - [ - -77, - 49 - ], - [ - -63, - -4 - ], - [ - -72, - 9 - ], - [ - -66, - 22 - ], - [ - -80, - 46 - ], - [ - -52, - 11 - ], - [ - -29, - -15 - ], - [ - -127, - 50 - ], - [ - -12, - 51 - ], - [ - -64, - 9 - ], - [ - 48, - 115 - ], - [ - 85, - -7 - ], - [ - 56, - -47 - ], - [ - 29, - -9 - ], - [ - 9, - -43 - ] - ], - [ - [ - 21939, - 10052 - ], - [ - -36, - -82 - ], - [ - -7, - 90 - ], - [ - 13, - 43 - ], - [ - 14, - 41 - ], - [ - 16, - -35 - ], - [ - 0, - -57 - ] - ], - [ - [ - 21418, - 10382 - ], - [ - -26, - -40 - ], - [ - -48, - 22 - ], - [ - -14, - 52 - ], - [ - 71, - 6 - ], - [ - 17, - -40 - ] - ], - [ - [ - 21642, - 10426 - ], - [ - 26, - -92 - ], - [ - -59, - 50 - ], - [ - -58, - 10 - ], - [ - -40, - -8 - ], - [ - -48, - 4 - ], - [ - 17, - 66 - ], - [ - 86, - 5 - ], - [ - 76, - -35 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 2, - -391 - ], - [ - 1, - -391 - ] - ], - [ - [ - 22379, - 9703 - ], - [ - -62, - 99 - ], - [ - -71, - 24 - ], - [ - -17, - -34 - ], - [ - -89, - -4 - ], - [ - 30, - 98 - ], - [ - 44, - 33 - ], - [ - -18, - 130 - ], - [ - -34, - 101 - ], - [ - -135, - 102 - ], - [ - -57, - 10 - ], - [ - -105, - 111 - ], - [ - -21, - -59 - ], - [ - -26, - -10 - ], - [ - -16, - 44 - ], - [ - 0, - 52 - ], - [ - -54, - 59 - ], - [ - 75, - 43 - ], - [ - 50, - -2 - ], - [ - -6, - 32 - ], - [ - -102, - 0 - ], - [ - -27, - 71 - ], - [ - -63, - 22 - ], - [ - -29, - 60 - ], - [ - 94, - 29 - ], - [ - 35, - 39 - ], - [ - 112, - -49 - ], - [ - 11, - -45 - ], - [ - 20, - -194 - ], - [ - 72, - -72 - ], - [ - 58, - 127 - ], - [ - 80, - 73 - ], - [ - 62, - 0 - ], - [ - 60, - -42 - ], - [ - 52, - -43 - ], - [ - 74, - -23 - ] - ], - [ - [ - 21278, - 10968 - ], - [ - -56, - -119 - ], - [ - -53, - -24 - ], - [ - -67, - 24 - ], - [ - -116, - -6 - ], - [ - -61, - -17 - ], - [ - -10, - -91 - ], - [ - 63, - -107 - ], - [ - 37, - 55 - ], - [ - 130, - 40 - ], - [ - -5, - -55 - ], - [ - -31, - 18 - ], - [ - -30, - -71 - ], - [ - -61, - -46 - ], - [ - 66, - -154 - ], - [ - -13, - -41 - ], - [ - 63, - -139 - ], - [ - -1, - -79 - ], - [ - -37, - -35 - ], - [ - -28, - 42 - ], - [ - 34, - 99 - ], - [ - -68, - -47 - ], - [ - -18, - 33 - ], - [ - 9, - 47 - ], - [ - -50, - 70 - ], - [ - 5, - 117 - ], - [ - -46, - -37 - ], - [ - 6, - -139 - ], - [ - 3, - -172 - ], - [ - -45, - -17 - ], - [ - -30, - 35 - ], - [ - 20, - 110 - ], - [ - -10, - 116 - ], - [ - -30, - 1 - ], - [ - -21, - 82 - ], - [ - 28, - 79 - ], - [ - 10, - 95 - ], - [ - 35, - 181 - ], - [ - 15, - 49 - ], - [ - 59, - 89 - ], - [ - 55, - -35 - ], - [ - 88, - -17 - ], - [ - 80, - 5 - ], - [ - 69, - 87 - ], - [ - 12, - -26 - ] - ], - [ - [ - 21518, - 10933 - ], - [ - -4, - -105 - ], - [ - -35, - 12 - ], - [ - -11, - -73 - ], - [ - 29, - -63 - ], - [ - -20, - -15 - ], - [ - -28, - 76 - ], - [ - -21, - 154 - ], - [ - 14, - 95 - ], - [ - 23, - 44 - ], - [ - 5, - -65 - ], - [ - 42, - -11 - ], - [ - 6, - -49 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 12, - -80 - ], - [ - 47, - -68 - ], - [ - 45, - 24 - ], - [ - 45, - -8 - ], - [ - 40, - 60 - ], - [ - 34, - 11 - ], - [ - 66, - -34 - ], - [ - 57, - 26 - ], - [ - 35, - 167 - ], - [ - 27, - 41 - ], - [ - 24, - 137 - ], - [ - 80, - 0 - ], - [ - 61, - -20 - ] - ], - [ - [ - 20765, - 11294 - ], - [ - -40, - -109 - ], - [ - 51, - -113 - ], - [ - -12, - -56 - ], - [ - 79, - -111 - ], - [ - -83, - -14 - ], - [ - -23, - -82 - ], - [ - 3, - -108 - ], - [ - -67, - -82 - ], - [ - -2, - -120 - ], - [ - -27, - -183 - ], - [ - -10, - 42 - ], - [ - -79, - -54 - ], - [ - -28, - 74 - ], - [ - -50, - 7 - ], - [ - -35, - 38 - ], - [ - -82, - -43 - ], - [ - -26, - 58 - ], - [ - -46, - -7 - ], - [ - -57, - 14 - ], - [ - -11, - 161 - ], - [ - -34, - 33 - ], - [ - -34, - 103 - ], - [ - -10, - 105 - ], - [ - 9, - 111 - ], - [ - 41, - 80 - ] - ], - [ - [ - 19924, - 10095 - ], - [ - -77, - -2 - ], - [ - -59, - 100 - ], - [ - -90, - 98 - ], - [ - -29, - 73 - ], - [ - -53, - 97 - ], - [ - -35, - 90 - ], - [ - -53, - 168 - ], - [ - -61, - 100 - ], - [ - -20, - 103 - ], - [ - -26, - 94 - ], - [ - -63, - 75 - ], - [ - -36, - 103 - ], - [ - -53, - 67 - ], - [ - -73, - 133 - ], - [ - -6, - 61 - ], - [ - 45, - -5 - ], - [ - 108, - -23 - ], - [ - 62, - -118 - ], - [ - 54, - -81 - ], - [ - 38, - -50 - ], - [ - 66, - -129 - ], - [ - 71, - -2 - ], - [ - 58, - -82 - ], - [ - 41, - -100 - ], - [ - 53, - -55 - ], - [ - -28, - -98 - ], - [ - 40, - -42 - ], - [ - 25, - -3 - ], - [ - 12, - -84 - ], - [ - 24, - -67 - ], - [ - 51, - -10 - ], - [ - 34, - -76 - ], - [ - -17, - -149 - ], - [ - -3, - -186 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -10, - -44 - ], - [ - -48, - 2 - ], - [ - -86, - -25 - ], - [ - 4, - -90 - ], - [ - -37, - -71 - ], - [ - -100, - -81 - ], - [ - -78, - -141 - ], - [ - -53, - -76 - ], - [ - -69, - -78 - ], - [ - 0, - -56 - ], - [ - -35, - -29 - ], - [ - -63, - -43 - ], - [ - -32, - -6 - ], - [ - -21, - -92 - ], - [ - 14, - -156 - ], - [ - 4, - -99 - ], - [ - -29, - -114 - ], - [ - -1, - -204 - ], - [ - -36, - -6 - ], - [ - -32, - -92 - ], - [ - 22, - -39 - ], - [ - -64, - -34 - ], - [ - -23, - -82 - ], - [ - -28, - -34 - ], - [ - -66, - 112 - ], - [ - -33, - 168 - ], - [ - -26, - 121 - ], - [ - -25, - 57 - ], - [ - -37, - 115 - ], - [ - -17, - 150 - ], - [ - -12, - 75 - ], - [ - -64, - 165 - ], - [ - -28, - 232 - ], - [ - -21, - 154 - ], - [ - 0, - 145 - ], - [ - -14, - 112 - ], - [ - -101, - -72 - ], - [ - -49, - 15 - ], - [ - -91, - 145 - ], - [ - 33, - 44 - ], - [ - -20, - 47 - ], - [ - -82, - 101 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - 46, - 81 - ], - [ - 154, - -1 - ], - [ - -14, - 103 - ], - [ - -39, - 61 - ], - [ - -8, - 92 - ], - [ - -46, - 54 - ], - [ - 77, - 126 - ], - [ - 81, - -9 - ], - [ - 73, - 126 - ], - [ - 44, - 121 - ], - [ - 67, - 121 - ], - [ - -1, - 85 - ], - [ - 60, - 70 - ], - [ - -57, - 59 - ], - [ - -24, - 81 - ], - [ - -25, - 105 - ], - [ - 35, - 52 - ], - [ - 105, - -29 - ], - [ - 78, - 18 - ], - [ - 67, - 100 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -45, - -54 - ], - [ - -27, - -112 - ], - [ - 68, - -46 - ], - [ - 66, - -59 - ], - [ - 91, - -67 - ], - [ - 95, - -15 - ], - [ - 40, - -61 - ], - [ - 54, - -12 - ], - [ - 84, - -28 - ], - [ - 58, - 2 - ], - [ - 8, - 48 - ], - [ - -9, - 76 - ], - [ - 5, - 52 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - 5, - -46 - ], - [ - -24, - -22 - ], - [ - 6, - -74 - ], - [ - -50, - 22 - ], - [ - -91, - -83 - ], - [ - 3, - -68 - ], - [ - -39, - -101 - ], - [ - -3, - -59 - ], - [ - -31, - -98 - ], - [ - -55, - 27 - ], - [ - -3, - -124 - ], - [ - -15, - -41 - ], - [ - 7, - -51 - ], - [ - -34, - -29 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - 12, - -86 - ], - [ - -53, - -107 - ], - [ - -123, - -71 - ], - [ - -99, - 18 - ], - [ - 57, - 125 - ], - [ - -37, - 122 - ], - [ - 95, - 94 - ], - [ - 53, - 56 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 34, - -63 - ], - [ - 29, - -73 - ], - [ - 66, - -53 - ], - [ - 2, - -105 - ], - [ - 33, - -20 - ], - [ - 6, - -55 - ], - [ - -100, - -62 - ], - [ - -27, - -139 - ] - ], - [ - [ - 16834, - 13806 - ], - [ - -131, - 36 - ], - [ - -76, - 28 - ], - [ - -78, - 15 - ], - [ - -30, - 147 - ], - [ - -34, - 22 - ], - [ - -53, - -22 - ], - [ - -70, - -58 - ], - [ - -86, - 40 - ], - [ - -70, - 92 - ], - [ - -67, - 34 - ], - [ - -47, - 114 - ], - [ - -51, - 160 - ], - [ - -38, - -19 - ], - [ - -44, - 39 - ], - [ - -26, - -47 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -38, - 64 - ], - [ - -1, - 63 - ], - [ - -22, - 0 - ], - [ - 11, - 87 - ], - [ - -36, - 91 - ], - [ - -85, - 66 - ], - [ - -49, - 114 - ], - [ - 17, - 94 - ], - [ - 35, - 41 - ], - [ - -6, - 70 - ], - [ - -45, - 36 - ], - [ - -45, - 143 - ] - ], - [ - [ - 15669, - 15256 - ], - [ - -39, - 97 - ], - [ - 14, - 37 - ], - [ - -22, - 137 - ], - [ - 48, - 35 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - 22, - -88 - ], - [ - 66, - -25 - ], - [ - 49, - -60 - ], - [ - 99, - -21 - ], - [ - 109, - 32 - ], - [ - 6, - 28 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - 62, - 23 - ], - [ - 49, - 69 - ], - [ - 47, - -4 - ], - [ - 30, - 23 - ], - [ - 50, - -11 - ], - [ - 77, - -61 - ], - [ - 56, - -13 - ], - [ - 79, - -107 - ], - [ - 52, - -4 - ], - [ - 6, - -101 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -41, - 6 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - -47, - 10 - ], - [ - -51, - -115 - ] - ], - [ - [ - 15794, - 14288 - ], - [ - -130, - 10 - ], - [ - -196, - 241 - ], - [ - -104, - 84 - ], - [ - -84, - 33 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -28, - 146 - ] - ], - [ - [ - 15252, - 14802 - ], - [ - 154, - 124 - ], - [ - 26, - 145 - ], - [ - -6, - 88 - ], - [ - 38, - 30 - ], - [ - 36, - 75 - ] - ], - [ - [ - 15500, - 15264 - ], - [ - 30, - 18 - ], - [ - 81, - -15 - ], - [ - 24, - -31 - ], - [ - 34, - 20 - ] - ], - [ - [ - 11536, - 18770 - ], - [ - -16, - -78 - ], - [ - 79, - -82 - ], - [ - -91, - -91 - ], - [ - -201, - -82 - ], - [ - -60, - -22 - ], - [ - -92, - 17 - ], - [ - -194, - 38 - ], - [ - 68, - 53 - ], - [ - -151, - 59 - ], - [ - 123, - 23 - ], - [ - -3, - 36 - ], - [ - -146, - 27 - ], - [ - 47, - 79 - ], - [ - 106, - 17 - ], - [ - 108, - -81 - ], - [ - 106, - 65 - ], - [ - 88, - -34 - ], - [ - 113, - 64 - ], - [ - 116, - -8 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 20, - 39 - ], - [ - -4, - 7 - ], - [ - 18, - 56 - ], - [ - 14, - 90 - ], - [ - 10, - 31 - ], - [ - 2, - 1 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 23, - 0 - ], - [ - 7, - 21 - ], - [ - 19, - 1 - ] - ], - [ - [ - 15045, - 14789 - ], - [ - 1, - -49 - ], - [ - -10, - -18 - ], - [ - 1, - -1 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - -12, - -38 - ] - ], - [ - [ - 15025, - 14683 - ], - [ - -25, - 17 - ], - [ - -14, - -80 - ], - [ - 17, - -13 - ], - [ - -18, - -17 - ], - [ - -3, - -31 - ], - [ - 33, - 16 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 2, - -47 - ], - [ - -35, - -192 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -8, - -59 - ], - [ - 17, - -51 - ] - ], - [ - [ - 13519, - 16267 - ], - [ - -55, - 17 - ], - [ - -57, - -42 - ], - [ - 4, - -60 - ], - [ - -9, - -34 - ], - [ - 23, - -61 - ], - [ - 65, - -61 - ], - [ - 35, - -99 - ], - [ - 78, - -96 - ], - [ - 55, - 0 - ], - [ - 17, - -26 - ], - [ - -20, - -24 - ], - [ - 63, - -44 - ], - [ - 51, - -36 - ], - [ - 60, - -62 - ], - [ - 7, - -23 - ], - [ - -13, - -43 - ], - [ - -39, - 56 - ], - [ - -61, - 20 - ], - [ - -29, - -78 - ], - [ - 50, - -44 - ], - [ - -8, - -63 - ], - [ - -29, - -7 - ], - [ - -37, - -103 - ], - [ - -29, - -9 - ], - [ - 0, - 37 - ], - [ - 14, - 64 - ], - [ - 15, - 26 - ], - [ - -27, - 69 - ], - [ - -21, - 61 - ], - [ - -29, - 15 - ], - [ - -21, - 51 - ], - [ - -44, - 22 - ], - [ - -31, - 49 - ], - [ - -51, - 7 - ], - [ - -55, - 54 - ], - [ - -63, - 79 - ], - [ - -48, - 69 - ], - [ - -21, - 118 - ], - [ - -35, - 14 - ], - [ - -57, - 40 - ], - [ - -32, - -16 - ], - [ - -40, - -56 - ], - [ - -29, - -9 - ] - ], - [ - [ - 13629, - 15384 - ], - [ - -25, - -95 - ], - [ - 11, - -37 - ], - [ - -15, - -62 - ], - [ - -53, - 46 - ], - [ - -36, - 13 - ], - [ - -97, - 61 - ], - [ - 10, - 61 - ], - [ - 81, - -11 - ], - [ - 71, - 13 - ], - [ - 53, - 11 - ] - ], - [ - [ - 13190, - 15741 - ], - [ - 41, - -85 - ], - [ - -9, - -159 - ], - [ - -32, - 8 - ], - [ - -29, - -40 - ], - [ - -26, - 32 - ], - [ - -3, - 144 - ], - [ - -16, - 69 - ], - [ - 39, - -6 - ], - [ - 35, - 37 - ] - ], - [ - [ - 7140, - 13015 - ], - [ - 47, - -10 - ], - [ - 37, - -29 - ], - [ - 12, - -33 - ], - [ - -49, - -2 - ], - [ - -21, - -20 - ], - [ - -39, - 19 - ], - [ - -40, - 44 - ], - [ - 8, - 27 - ], - [ - 29, - 9 - ], - [ - 16, - -5 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -14, - -19 - ], - [ - -139, - -60 - ], - [ - 69, - -120 - ], - [ - -23, - -20 - ], - [ - -11, - -40 - ], - [ - -53, - -17 - ], - [ - -17, - -43 - ], - [ - -30, - -37 - ], - [ - -78, - 19 - ] - ], - [ - [ - 14984, - 14319 - ], - [ - -2, - 17 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 10, - 35 - ], - [ - 0, - 73 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - 78, - -47 - ], - [ - 137, - 128 - ] - ], - [ - [ - 21933, - 14894 - ], - [ - 9, - -41 - ], - [ - -39, - -73 - ], - [ - -29, - 39 - ], - [ - -36, - -28 - ], - [ - -18, - -70 - ], - [ - -46, - 34 - ], - [ - 1, - 57 - ], - [ - 38, - 71 - ], - [ - 40, - -14 - ], - [ - 29, - 51 - ], - [ - 51, - -26 - ] - ], - [ - [ - 22375, - 15253 - ], - [ - -27, - -96 - ], - [ - 13, - -60 - ], - [ - -37, - -84 - ], - [ - -89, - -57 - ], - [ - -122, - -7 - ], - [ - -100, - -137 - ], - [ - -46, - 46 - ], - [ - -3, - 90 - ], - [ - -122, - -27 - ], - [ - -82, - -56 - ], - [ - -82, - -3 - ], - [ - 71, - -88 - ], - [ - -47, - -204 - ], - [ - -45, - -50 - ], - [ - -33, - 46 - ], - [ - 17, - 109 - ], - [ - -44, - 34 - ], - [ - -29, - 83 - ], - [ - 66, - 37 - ], - [ - 37, - 75 - ], - [ - 70, - 62 - ], - [ - 51, - 82 - ], - [ - 139, - 36 - ], - [ - 74, - -25 - ], - [ - 73, - 214 - ], - [ - 47, - -58 - ], - [ - 102, - 120 - ], - [ - 40, - 47 - ], - [ - 43, - 147 - ], - [ - -11, - 135 - ], - [ - 29, - 75 - ], - [ - 74, - 22 - ], - [ - 38, - -166 - ], - [ - -2, - -97 - ], - [ - -64, - -121 - ], - [ - 1, - -124 - ] - ], - [ - [ - 22579, - 16097 - ], - [ - 49, - -26 - ], - [ - 50, - 51 - ], - [ - 15, - -135 - ], - [ - -103, - -33 - ], - [ - -61, - -119 - ], - [ - -110, - 82 - ], - [ - -38, - -131 - ], - [ - -77, - -2 - ], - [ - -10, - 120 - ], - [ - 34, - 92 - ], - [ - 75, - 6 - ], - [ - 20, - 166 - ], - [ - 21, - 94 - ], - [ - 82, - -125 - ], - [ - 53, - -40 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -43, - 17 - ], - [ - -35, - 44 - ], - [ - -103, - 12 - ], - [ - -116, - 3 - ], - [ - -25, - -13 - ], - [ - -99, - 51 - ], - [ - -40, - -25 - ], - [ - -11, - -71 - ], - [ - -114, - 41 - ], - [ - -46, - -17 - ], - [ - -16, - -52 - ] - ], - [ - [ - 17494, - 15868 - ], - [ - -40, - -22 - ], - [ - -92, - -84 - ], - [ - -30, - -86 - ], - [ - -26, - -1 - ], - [ - -19, - 57 - ], - [ - -89, - 4 - ], - [ - -14, - 98 - ], - [ - -34, - 1 - ], - [ - 5, - 121 - ], - [ - -83, - 87 - ], - [ - -120, - -9 - ], - [ - -82, - -18 - ], - [ - -66, - 109 - ], - [ - -57, - 45 - ], - [ - -108, - 86 - ], - [ - -13, - 10 - ], - [ - -180, - -71 - ], - [ - 3, - -442 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - -36, - -6 - ], - [ - -49, - 94 - ], - [ - -47, - 34 - ], - [ - -79, - -25 - ], - [ - -31, - -40 - ] - ], - [ - [ - 16207, - 15810 - ], - [ - -4, - 29 - ], - [ - 18, - 50 - ], - [ - -14, - 42 - ], - [ - -81, - 41 - ], - [ - -31, - 108 - ], - [ - -38, - 30 - ], - [ - -3, - 39 - ], - [ - 68, - -11 - ], - [ - 3, - 87 - ], - [ - 59, - 20 - ], - [ - 61, - -18 - ], - [ - 12, - 117 - ], - [ - -12, - 74 - ], - [ - -70, - -6 - ], - [ - -59, - 30 - ], - [ - -81, - -53 - ], - [ - -65, - -25 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -35, - 19 - ], - [ - 7, - 62 - ], - [ - -45, - 80 - ], - [ - -51, - -3 - ], - [ - -59, - 81 - ], - [ - 40, - 91 - ], - [ - -21, - 24 - ], - [ - 56, - 132 - ], - [ - 72, - -69 - ], - [ - 8, - 87 - ], - [ - 144, - 131 - ], - [ - 109, - 3 - ], - [ - 154, - -83 - ], - [ - 82, - -49 - ], - [ - 74, - 51 - ], - [ - 111, - 2 - ], - [ - 89, - -62 - ], - [ - 20, - 36 - ], - [ - 98, - -6 - ], - [ - 18, - 57 - ], - [ - -113, - 83 - ], - [ - 67, - 58 - ], - [ - -13, - 33 - ], - [ - 67, - 31 - ], - [ - -51, - 82 - ], - [ - 32, - 41 - ], - [ - 261, - 42 - ], - [ - 34, - 30 - ], - [ - 174, - 44 - ], - [ - 63, - 50 - ], - [ - 125, - -26 - ], - [ - 22, - -125 - ], - [ - 73, - 30 - ], - [ - 90, - -41 - ], - [ - -6, - -66 - ], - [ - 67, - 7 - ], - [ - 174, - 113 - ], - [ - -25, - -37 - ], - [ - 89, - -93 - ], - [ - 156, - -305 - ], - [ - 37, - 63 - ], - [ - 96, - -69 - ], - [ - 100, - 31 - ], - [ - 38, - -22 - ], - [ - 34, - -69 - ], - [ - 49, - -23 - ], - [ - 29, - -51 - ], - [ - 90, - 16 - ], - [ - 37, - -74 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -61, - -136 - ], - [ - 1, - -437 - ], - [ - 41, - -99 - ] - ], - [ - [ - 15446, - 10595 - ], - [ - -48, - -48 - ], - [ - -18, - -50 - ], - [ - -26, - -8 - ], - [ - -10, - -85 - ], - [ - -22, - -48 - ], - [ - -14, - -80 - ], - [ - -28, - -40 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -100, - 120 - ], - [ - -5, - 70 - ], - [ - -252, - 244 - ], - [ - -12, - 13 - ] - ], - [ - [ - 14911, - 10683 - ], - [ - -1, - 127 - ], - [ - 20, - 49 - ], - [ - 34, - 79 - ], - [ - 26, - 88 - ], - [ - -31, - 138 - ], - [ - -8, - 60 - ], - [ - -33, - 83 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - 43, - 72 - ], - [ - 47, - 79 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - -132, - -18 - ], - [ - -86, - 38 - ], - [ - -75, - -9 - ], - [ - 6, - 69 - ], - [ - 76, - -20 - ], - [ - 26, - 37 - ] - ], - [ - [ - 17498, - 15625 - ], - [ - 53, - -12 - ], - [ - 89, - 87 - ], - [ - -83, - 63 - ], - [ - -49, - -30 - ], - [ - -52, - 45 - ], - [ - 59, - 78 - ], - [ - -21, - 12 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -17, - 145 - ], - [ - 45, - 100 - ], - [ - 90, - 23 - ], - [ - 65, - -17 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 58, - -48 - ], - [ - 31, - 83 - ], - [ - 62, - -44 - ] - ], - [ - [ - 20033, - 12501 - ], - [ - 16, - -80 - ], - [ - -8, - -144 - ], - [ - -118, - -92 - ], - [ - 31, - -73 - ], - [ - -73, - -8 - ], - [ - -61, - -49 - ] - ], - [ - [ - 19820, - 12055 - ], - [ - -58, - 18 - ], - [ - -28, - 62 - ], - [ - -35, - 124 - ] - ], - [ - [ - 21495, - 15429 - ], - [ - 60, - -141 - ], - [ - 17, - -78 - ], - [ - 1, - -138 - ], - [ - -27, - -66 - ], - [ - -63, - -23 - ], - [ - -56, - -50 - ], - [ - -62, - -10 - ], - [ - -8, - 65 - ], - [ - 13, - 90 - ], - [ - -31, - 125 - ], - [ - 52, - 20 - ], - [ - -48, - 103 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - 4, - 11 - ], - [ - 31, - -4 - ], - [ - 28, - 54 - ], - [ - 49, - 6 - ], - [ - 30, - 7 - ], - [ - 10, - 29 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 13, - 26 - ] - ], - [ - [ - 13960, - 15933 - ], - [ - 16, - 9 - ], - [ - 10, - 40 - ], - [ - 12, - 6 - ], - [ - 10, - -16 - ], - [ - 13, - -8 - ], - [ - 9, - -19 - ], - [ - 12, - -6 - ], - [ - 14, - -22 - ], - [ - 9, - 1 - ], - [ - -7, - -29 - ], - [ - -9, - -15 - ], - [ - 3, - -9 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - -16, - -4 - ], - [ - -41, - -19 - ], - [ - -3, - -24 - ], - [ - -9, - 1 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - 14, - -53 - ], - [ - -6, - -27 - ], - [ - 23, - -90 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - -50, - -4 - ], - [ - -17, - 58 - ], - [ - -62, - 11 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - 40, - -94 - ], - [ - 32, - -109 - ], - [ - 85, - -1 - ], - [ - 28, - -105 - ], - [ - -45, - -31 - ], - [ - -20, - -44 - ], - [ - 83, - -71 - ], - [ - 58, - -142 - ], - [ - 44, - -106 - ], - [ - 53, - -83 - ], - [ - 18, - -85 - ], - [ - -13, - -120 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 23, - 54 - ], - [ - 3, - 101 - ], - [ - -57, - 105 - ], - [ - -4, - 118 - ], - [ - -53, - 98 - ], - [ - -53, - 8 - ], - [ - -14, - -42 - ], - [ - -40, - -3 - ], - [ - -21, - 21 - ], - [ - -74, - -72 - ], - [ - -1, - 108 - ], - [ - 17, - 126 - ], - [ - -47, - 6 - ], - [ - -4, - 72 - ], - [ - -31, - 37 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - 15, - 44 - ], - [ - 60, - 78 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 25, - 98 - ], - [ - 35, - 84 - ], - [ - 1, - 5 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - 31, - -7 - ], - [ - 12, - -47 - ], - [ - -38, - -45 - ], - [ - -17, - -66 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - -18, - -1 - ], - [ - -72, - 57 - ], - [ - -64, - 91 - ], - [ - -59, - 66 - ], - [ - -47, - 77 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - 17, - 39 - ], - [ - 3, - 35 - ], - [ - 32, - 65 - ], - [ - 32, - 56 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 34, - 28 - ], - [ - 7, - 51 - ], - [ - -8, - 49 - ], - [ - 48, - 47 - ], - [ - 21, - 38 - ], - [ - 34, - 34 - ], - [ - 4, - 93 - ] - ], - [ - [ - 13348, - 14773 - ], - [ - 82, - -42 - ], - [ - 30, - 11 - ], - [ - 58, - -20 - ], - [ - 92, - -54 - ], - [ - 33, - -107 - ], - [ - 62, - -23 - ], - [ - 99, - -50 - ], - [ - 74, - -60 - ], - [ - 34, - 31 - ], - [ - 33, - 56 - ], - [ - -16, - 91 - ], - [ - 22, - 59 - ], - [ - 50, - 56 - ], - [ - 48, - 16 - ], - [ - 95, - -24 - ], - [ - 23, - -54 - ], - [ - 26, - 0 - ], - [ - 22, - -21 - ], - [ - 70, - -14 - ], - [ - 17, - -39 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - -240 - ], - [ - -80, - 0 - ], - [ - -1, - -51 - ] - ], - [ - [ - 14209, - 13146 - ], - [ - -278, - 230 - ], - [ - -278, - 230 - ], - [ - -70, - -66 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - -50, - -45 - ], - [ - -39, - 66 - ], - [ - -110, - 52 - ] - ], - [ - [ - 18249, - 11700 - ], - [ - -11, - -125 - ], - [ - -29, - -34 - ], - [ - -61, - -28 - ], - [ - -33, - 96 - ], - [ - -12, - 172 - ], - [ - 31, - 195 - ], - [ - 49, - -67 - ], - [ - 32, - -84 - ], - [ - 34, - -125 - ] - ], - [ - [ - 14568, - 7323 - ], - [ - 24, - -36 - ], - [ - -22, - -58 - ], - [ - -12, - -39 - ], - [ - -38, - -19 - ], - [ - -13, - -38 - ], - [ - -25, - -12 - ], - [ - -52, - 92 - ], - [ - 37, - 76 - ], - [ - 38, - 47 - ], - [ - 32, - 24 - ], - [ - 31, - -37 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - -17, - 37 - ], - [ - -36, - 13 - ] - ], - [ - [ - 14132, - 17315 - ], - [ - -6, - 30 - ], - [ - 8, - 33 - ], - [ - -31, - 19 - ], - [ - -73, - 21 - ] - ], - [ - [ - 14030, - 17418 - ], - [ - -15, - 101 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 80, - 37 - ], - [ - 117, - -8 - ], - [ - 68, - 12 - ], - [ - 10, - -25 - ], - [ - 37, - -8 - ], - [ - 67, - -58 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 3, - 90 - ], - [ - 34, - 76 - ], - [ - 66, - 41 - ], - [ - 55, - -90 - ], - [ - 56, - 2 - ], - [ - 13, - 93 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - 33, - -27 - ], - [ - 6, - -58 - ], - [ - 23, - -71 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - -10, - 0 - ], - [ - 1, - -64 - ], - [ - -43, - -4 - ], - [ - -22, - -27 - ], - [ - -32, - 0 - ], - [ - -25, - 15 - ], - [ - -59, - -13 - ], - [ - -22, - -93 - ], - [ - -22, - -9 - ], - [ - -33, - -151 - ], - [ - -97, - -130 - ], - [ - -23, - -165 - ], - [ - -28, - -54 - ], - [ - -9, - -43 - ], - [ - -157, - -10 - ], - [ - -1, - 0 - ] - ], - [ - [ - 11361, - 13367 - ], - [ - 3, - 56 - ], - [ - 27, - 32 - ], - [ - 23, - 63 - ], - [ - -5, - 41 - ], - [ - 24, - 84 - ], - [ - 39, - 77 - ], - [ - 24, - 19 - ], - [ - 18, - 70 - ], - [ - 2, - 64 - ], - [ - 25, - 74 - ], - [ - 46, - 44 - ], - [ - 45, - 122 - ], - [ - 1, - 2 - ], - [ - 35, - 46 - ], - [ - 65, - 13 - ], - [ - 55, - 82 - ], - [ - 35, - 32 - ], - [ - 58, - 100 - ], - [ - -18, - 150 - ], - [ - 27, - 103 - ], - [ - 9, - 63 - ], - [ - 45, - 81 - ], - [ - 70, - 55 - ], - [ - 52, - 49 - ], - [ - 46, - 125 - ], - [ - 22, - 73 - ], - [ - 51, - 0 - ], - [ - 42, - -51 - ], - [ - 67, - 8 - ], - [ - 72, - -26 - ], - [ - 30, - -2 - ] - ], - [ - [ - 14403, - 16582 - ], - [ - 17, - 18 - ], - [ - 46, - 12 - ], - [ - 51, - -38 - ], - [ - 29, - -4 - ], - [ - 32, - -32 - ], - [ - -5, - -41 - ], - [ - 25, - -20 - ], - [ - 10, - -50 - ], - [ - 24, - -30 - ], - [ - -5, - -18 - ], - [ - 13, - -12 - ], - [ - -18, - -9 - ], - [ - -41, - 3 - ], - [ - -7, - 17 - ], - [ - -15, - -10 - ], - [ - 5, - -21 - ], - [ - -19, - -38 - ], - [ - -12, - -42 - ], - [ - -17, - -13 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - -13, - 55 - ], - [ - 7, - 51 - ], - [ - -2, - 53 - ], - [ - -40, - 71 - ], - [ - -22, - 51 - ], - [ - -22, - 35 - ], - [ - -21, - 12 - ] - ], - [ - [ - 16001, - 9301 - ], - [ - 19, - -51 - ], - [ - 17, - -79 - ], - [ - 11, - -144 - ], - [ - 18, - -57 - ], - [ - -7, - -57 - ], - [ - -12, - -35 - ], - [ - -24, - 70 - ], - [ - -13, - -36 - ], - [ - 13, - -88 - ], - [ - -6, - -51 - ], - [ - -19, - -28 - ], - [ - -4, - -102 - ], - [ - -28, - -139 - ], - [ - -34, - -166 - ], - [ - -43, - -227 - ], - [ - -27, - -167 - ], - [ - -32, - -139 - ], - [ - -56, - -28 - ], - [ - -61, - -51 - ], - [ - -40, - 30 - ], - [ - -56, - 43 - ], - [ - -19, - 64 - ], - [ - -4, - 106 - ], - [ - -25, - 96 - ], - [ - -6, - 86 - ], - [ - 12, - 86 - ], - [ - 32, - 21 - ], - [ - 0, - 40 - ], - [ - 34, - 91 - ], - [ - 6, - 77 - ], - [ - -16, - 56 - ], - [ - -13, - 76 - ], - [ - -6, - 111 - ], - [ - 24, - 67 - ], - [ - 10, - 76 - ], - [ - 35, - 4 - ], - [ - 38, - 25 - ], - [ - 26, - 21 - ], - [ - 31, - 2 - ], - [ - 40, - 68 - ], - [ - 57, - 74 - ], - [ - 21, - 61 - ], - [ - -10, - 51 - ], - [ - 30, - -14 - ], - [ - 38, - 83 - ], - [ - 2, - 72 - ], - [ - 23, - 54 - ], - [ - 24, - -52 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - -78, - 130 - ], - [ - -36, - 39 - ], - [ - -57, - 31 - ], - [ - -39, - -9 - ], - [ - -56, - -45 - ], - [ - -35, - -12 - ], - [ - -50, - 32 - ], - [ - -52, - 23 - ], - [ - -65, - 55 - ], - [ - -52, - 16 - ], - [ - -79, - 56 - ], - [ - -58, - 58 - ], - [ - -18, - 32 - ], - [ - -39, - 7 - ], - [ - -71, - 38 - ], - [ - -29, - 54 - ], - [ - -75, - 69 - ], - [ - -35, - 75 - ], - [ - -17, - 59 - ], - [ - 23, - 11 - ], - [ - -7, - 35 - ], - [ - 16, - 31 - ], - [ - 1, - 41 - ], - [ - -24, - 54 - ], - [ - -6, - 48 - ], - [ - -24, - 60 - ], - [ - -61, - 120 - ], - [ - -70, - 93 - ], - [ - -34, - 75 - ], - [ - -60, - 49 - ], - [ - -13, - 29 - ], - [ - 11, - 75 - ], - [ - -36, - 28 - ], - [ - -41, - 58 - ], - [ - -17, - 84 - ], - [ - -38, - 9 - ], - [ - -40, - 63 - ], - [ - -33, - 59 - ], - [ - -3, - 37 - ], - [ - -37, - 91 - ], - [ - -25, - 92 - ], - [ - 1, - 46 - ], - [ - -50, - 47 - ], - [ - -24, - -5 - ], - [ - -39, - 33 - ], - [ - -12, - -49 - ], - [ - 12, - -57 - ], - [ - 7, - -90 - ], - [ - 24, - -50 - ], - [ - 51, - -82 - ], - [ - 12, - -29 - ], - [ - 10, - -8 - ], - [ - 10, - -41 - ], - [ - 12, - 1 - ], - [ - 14, - -77 - ], - [ - 21, - -31 - ], - [ - 15, - -42 - ], - [ - 44, - -61 - ], - [ - 23, - -112 - ], - [ - 21, - -52 - ], - [ - 19, - -56 - ], - [ - 4, - -64 - ], - [ - 34, - -4 - ], - [ - 27, - -54 - ], - [ - 26, - -54 - ], - [ - -2, - -21 - ], - [ - -29, - -44 - ], - [ - -13, - 0 - ], - [ - -18, - 73 - ], - [ - -46, - 69 - ], - [ - -50, - 58 - ], - [ - -36, - 30 - ], - [ - 3, - 88 - ], - [ - -11, - 65 - ], - [ - -33, - 37 - ], - [ - -48, - 54 - ], - [ - -9, - -16 - ], - [ - -18, - 31 - ], - [ - -43, - 29 - ], - [ - -41, - 70 - ], - [ - 5, - 9 - ], - [ - 29, - -7 - ], - [ - 26, - 45 - ], - [ - 2, - 54 - ], - [ - -53, - 86 - ], - [ - -41, - 33 - ], - [ - -26, - 75 - ], - [ - -26, - 79 - ], - [ - -32, - 95 - ], - [ - -28, - 108 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - 79, - 10 - ], - [ - 88, - 13 - ], - [ - -6, - -24 - ], - [ - 105, - -58 - ], - [ - 159, - -85 - ], - [ - 139, - 1 - ], - [ - 55, - 0 - ], - [ - 0, - 50 - ], - [ - 121, - 0 - ], - [ - 25, - -43 - ], - [ - 36, - -38 - ], - [ - 42, - -52 - ], - [ - 23, - -63 - ], - [ - 17, - -66 - ], - [ - 36, - -36 - ], - [ - 58, - -36 - ], - [ - 44, - 94 - ], - [ - 57, - 3 - ], - [ - 49, - -48 - ], - [ - 35, - -82 - ], - [ - 24, - -70 - ], - [ - 41, - -69 - ], - [ - 15, - -84 - ], - [ - 20, - -56 - ], - [ - 54, - -37 - ], - [ - 50, - -27 - ], - [ - 27, - 4 - ] - ], - [ - [ - 5776, - 13901 - ], - [ - -27, - -106 - ], - [ - -12, - -86 - ], - [ - -5, - -161 - ], - [ - -7, - -58 - ], - [ - 12, - -66 - ], - [ - 22, - -58 - ], - [ - 14, - -93 - ], - [ - 46, - -90 - ], - [ - 16, - -68 - ], - [ - 27, - -59 - ], - [ - 74, - -32 - ], - [ - 29, - -50 - ], - [ - 61, - 33 - ], - [ - 54, - 13 - ], - [ - 52, - 21 - ], - [ - 44, - 21 - ], - [ - 44, - 49 - ], - [ - 17, - 70 - ], - [ - 5, - 100 - ], - [ - 12, - 36 - ], - [ - 48, - 31 - ], - [ - 73, - 28 - ], - [ - 62, - -4 - ], - [ - 42, - 10 - ], - [ - 17, - -26 - ], - [ - -2, - -57 - ], - [ - -38, - -72 - ], - [ - -16, - -73 - ], - [ - 12, - -21 - ], - [ - -10, - -52 - ], - [ - -17, - -93 - ], - [ - -18, - 31 - ], - [ - -15, - -2 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - 23, - 7 - ], - [ - 33, - 2 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 3, - 37 - ], - [ - -6, - 47 - ], - [ - -26, - 33 - ], - [ - -14, - 69 - ], - [ - -3, - 75 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - 24, - 22 - ], - [ - 11, - 70 - ], - [ - 22, - 3 - ], - [ - 49, - -33 - ], - [ - 39, - 23 - ], - [ - 27, - -8 - ], - [ - 11, - 27 - ], - [ - 279, - 2 - ], - [ - 16, - 84 - ], - [ - -12, - 15 - ], - [ - -34, - 517 - ], - [ - -33, - 518 - ], - [ - 106, - 2 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - 0, - -276 - ], - [ - -38, - -80 - ], - [ - -6, - -74 - ], - [ - -62, - -19 - ], - [ - -95, - -10 - ], - [ - -26, - -43 - ], - [ - -44, - -5 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - -40, - -28 - ], - [ - -40, - -52 - ], - [ - -49, - -5 - ], - [ - -32, - -130 - ], - [ - -30, - -22 - ], - [ - 34, - -105 - ], - [ - 44, - -88 - ], - [ - 29, - -79 - ], - [ - -26, - -104 - ], - [ - -24, - -22 - ], - [ - 17, - -61 - ], - [ - 46, - -95 - ], - [ - 8, - -67 - ], - [ - -1, - -56 - ], - [ - 28, - -109 - ], - [ - -39, - -112 - ], - [ - -33, - -123 - ] - ], - [ - [ - 19418, - 11989 - ], - [ - -7, - 89 - ], - [ - 21, - 92 - ], - [ - -23, - 71 - ], - [ - 5, - 130 - ], - [ - -28, - 63 - ], - [ - -23, - 143 - ], - [ - -12, - 152 - ], - [ - -30, - 99 - ], - [ - -46, - -60 - ], - [ - -79, - -86 - ], - [ - -40, - 11 - ], - [ - -43, - 28 - ], - [ - 24, - 149 - ], - [ - -14, - 112 - ], - [ - -55, - 139 - ], - [ - 9, - 43 - ], - [ - -41, - 15 - ], - [ - -50, - 98 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -15, - 9 - ], - [ - -19, - 40 - ], - [ - -30, - 23 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - 19, - -21 - ], - [ - 10, - -17 - ], - [ - 23, - -12 - ], - [ - 26, - -25 - ], - [ - -5, - -11 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 74, - 21 - ], - [ - 133, - 103 - ], - [ - 106, - 57 - ], - [ - 61, - -37 - ], - [ - 72, - -2 - ], - [ - 47, - -56 - ], - [ - 70, - -4 - ], - [ - 100, - -30 - ], - [ - 68, - 83 - ], - [ - -28, - 71 - ], - [ - 72, - 124 - ], - [ - 78, - -49 - ], - [ - 63, - -14 - ], - [ - 82, - -31 - ], - [ - 14, - -90 - ], - [ - 99, - -51 - ], - [ - 65, - 23 - ], - [ - 89, - 15 - ], - [ - 70, - -15 - ], - [ - 68, - -58 - ], - [ - 42, - -61 - ], - [ - 65, - 1 - ], - [ - 88, - -20 - ], - [ - 64, - 30 - ], - [ - 91, - 20 - ], - [ - 103, - 84 - ], - [ - 41, - -13 - ], - [ - 37, - -40 - ], - [ - 83, - 10 - ] - ], - [ - [ - 14957, - 9415 - ], - [ - 52, - 10 - ], - [ - 84, - -34 - ], - [ - 18, - 15 - ], - [ - 49, - 3 - ], - [ - 24, - 36 - ], - [ - 42, - -2 - ], - [ - 76, - 47 - ], - [ - 56, - 69 - ] - ], - [ - [ - 15358, - 9559 - ], - [ - 11, - -53 - ], - [ - -3, - -120 - ], - [ - 9, - -105 - ], - [ - 3, - -188 - ], - [ - 12, - -58 - ], - [ - -21, - -86 - ], - [ - -27, - -83 - ], - [ - -44, - -75 - ], - [ - -64, - -45 - ], - [ - -79, - -59 - ], - [ - -78, - -128 - ], - [ - -27, - -22 - ], - [ - -49, - -86 - ], - [ - -29, - -27 - ], - [ - -5, - -86 - ], - [ - 33, - -91 - ], - [ - 13, - -70 - ], - [ - 1, - -36 - ], - [ - 13, - 6 - ], - [ - -2, - -118 - ], - [ - -12, - -55 - ], - [ - 17, - -21 - ], - [ - -11, - -50 - ], - [ - -29, - -42 - ], - [ - -57, - -41 - ], - [ - -84, - -65 - ], - [ - -31, - -44 - ], - [ - 6, - -51 - ], - [ - 18, - -8 - ], - [ - -6, - -63 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -53, - 1 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -6, - 53 - ], - [ - -10, - 54 - ] - ], - [ - [ - 14767, - 7697 - ], - [ - -6, - 43 - ], - [ - 12, - 134 - ], - [ - -18, - 85 - ], - [ - -33, - 169 - ] - ], - [ - [ - 14722, - 8128 - ], - [ - 73, - 136 - ], - [ - 19, - 86 - ], - [ - 10, - 11 - ], - [ - 8, - 71 - ], - [ - -11, - 35 - ], - [ - 3, - 90 - ], - [ - 13, - 83 - ], - [ - 0, - 152 - ], - [ - -36, - 39 - ], - [ - -33, - 8 - ], - [ - -15, - 30 - ], - [ - -32, - 25 - ], - [ - -59, - -2 - ], - [ - -4, - 45 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -7, - 85 - ], - [ - 212, - 99 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - 40, - -58 - ], - [ - 19, - 11 - ], - [ - 28, - -30 - ], - [ - 4, - -48 - ], - [ - -15, - -56 - ], - [ - 5, - -84 - ], - [ - 46, - -74 - ], - [ - 21, - 83 - ], - [ - 30, - 25 - ], - [ - -6, - 154 - ], - [ - -29, - 87 - ], - [ - -25, - 39 - ], - [ - -24, - -2 - ], - [ - -20, - 156 - ], - [ - 20, - 91 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - -46, - 82 - ], - [ - -42, - 88 - ], - [ - -46, - 32 - ], - [ - -34, - 35 - ], - [ - -39, - -1 - ], - [ - -34, - -26 - ], - [ - -34, - 10 - ], - [ - -24, - -38 - ] - ], - [ - [ - 11400, - 12733 - ], - [ - -6, - 65 - ], - [ - 19, - 59 - ], - [ - 9, - 113 - ], - [ - -8, - 118 - ], - [ - -8, - 60 - ], - [ - 7, - 60 - ], - [ - -18, - 57 - ], - [ - -37, - 52 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 15, - 40 - ], - [ - 273, - -1 - ], - [ - -13, - 173 - ], - [ - 17, - 62 - ], - [ - 65, - 10 - ], - [ - -2, - 307 - ], - [ - 229, - -6 - ], - [ - 0, - 182 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - -37, - 31 - ], - [ - 21, - 112 - ], - [ - 22, - 41 - ], - [ - -13, - 100 - ], - [ - 14, - 97 - ], - [ - 12, - 32 - ], - [ - -18, - 102 - ], - [ - -33, - 54 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - 68, - -23 - ], - [ - 14, - -33 - ], - [ - 24, - -56 - ], - [ - 20, - -163 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - 54, - 83 - ], - [ - 35, - 94 - ], - [ - 28, - 0 - ], - [ - 36, - -60 - ], - [ - 3, - -52 - ], - [ - 46, - -34 - ], - [ - 58, - -36 - ], - [ - -4, - -47 - ], - [ - -47, - -6 - ], - [ - 12, - -59 - ], - [ - -51, - -40 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 51, - -41 - ], - [ - 54, - 22 - ], - [ - 14, - 102 - ], - [ - 30, - 22 - ], - [ - 83, - 26 - ], - [ - 50, - 95 - ], - [ - 34, - 76 - ] - ], - [ - [ - 19668, - 11544 - ], - [ - 16, - -12 - ], - [ - 41, - -72 - ], - [ - 29, - -80 - ], - [ - 4, - -81 - ], - [ - -7, - -55 - ], - [ - 6, - -41 - ], - [ - 5, - -71 - ], - [ - 25, - -33 - ], - [ - 27, - -106 - ], - [ - -1, - -41 - ], - [ - -49, - -8 - ], - [ - -66, - 89 - ], - [ - -83, - 95 - ], - [ - -8, - 62 - ], - [ - -40, - 80 - ], - [ - -10, - 99 - ], - [ - -25, - 66 - ], - [ - 8, - 87 - ], - [ - -16, - 51 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - 12, - 21 - ], - [ - 57, - -52 - ], - [ - 6, - -62 - ], - [ - 46, - 14 - ], - [ - 23, - 50 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - 57, - 27 - ], - [ - 45, - -7 - ], - [ - 28, - -27 - ], - [ - 0, - -10 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - -443 - ], - [ - -62, - -62 - ], - [ - -37, - -8 - ], - [ - -44, - 22 - ], - [ - -31, - 9 - ], - [ - -12, - 51 - ], - [ - -28, - 33 - ], - [ - -33, - -59 - ] - ], - [ - [ - 13687, - 7369 - ], - [ - -52, - 91 - ], - [ - -27, - 87 - ], - [ - -16, - 117 - ], - [ - -17, - 87 - ], - [ - -23, - 185 - ], - [ - -2, - 143 - ], - [ - -9, - 66 - ], - [ - -27, - 49 - ], - [ - -36, - 99 - ], - [ - -36, - 144 - ], - [ - -16, - 75 - ], - [ - -56, - 117 - ], - [ - -5, - 93 - ] - ], - [ - [ - 24104, - 8268 - ], - [ - 57, - -74 - ], - [ - 36, - -55 - ], - [ - -26, - -29 - ], - [ - -39, - 32 - ], - [ - -50, - 54 - ], - [ - -44, - 64 - ], - [ - -47, - 84 - ], - [ - -9, - 41 - ], - [ - 30, - -2 - ], - [ - 39, - -40 - ], - [ - 30, - -41 - ], - [ - 23, - -34 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - 17, - -186 - ], - [ - 26, - -32 - ], - [ - 1, - -38 - ], - [ - 29, - -41 - ], - [ - -15, - -52 - ], - [ - -27, - -243 - ], - [ - -4, - -156 - ], - [ - -89, - -113 - ], - [ - -30, - -158 - ], - [ - 29, - -45 - ], - [ - 0, - -77 - ], - [ - 45, - -3 - ], - [ - -7, - -56 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - -13, - -3 - ], - [ - -47, - 132 - ], - [ - -16, - 4 - ], - [ - -55, - -67 - ], - [ - -54, - 35 - ], - [ - -37, - 7 - ], - [ - -21, - -17 - ], - [ - -40, - 4 - ], - [ - -42, - -51 - ], - [ - -35, - -3 - ], - [ - -84, - 62 - ], - [ - -33, - -29 - ], - [ - -36, - 2 - ], - [ - -26, - 45 - ], - [ - -70, - 45 - ], - [ - -75, - -15 - ], - [ - -18, - -25 - ], - [ - -10, - -69 - ], - [ - -20, - -49 - ], - [ - -5, - -107 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - -72, - -43 - ], - [ - -27, - 6 - ], - [ - -27, - -27 - ], - [ - -55, - 3 - ], - [ - -38, - 75 - ], - [ - -23, - 86 - ], - [ - -49, - 79 - ], - [ - -52, - -1 - ], - [ - -62, - 0 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - -24, - 38 - ], - [ - -33, - 49 - ], - [ - -15, - 40 - ], - [ - -30, - 38 - ], - [ - -35, - 54 - ], - [ - 8, - 19 - ], - [ - 12, - -19 - ], - [ - 5, - 9 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -6, - -11 - ], - [ - -3, - -27 - ], - [ - 7, - -44 - ], - [ - -16, - -41 - ], - [ - -8, - -48 - ], - [ - -2, - -53 - ], - [ - 4, - -31 - ], - [ - 2, - -54 - ], - [ - -11, - -12 - ], - [ - -6, - -51 - ], - [ - 4, - -32 - ], - [ - -14, - -30 - ], - [ - 3, - -33 - ], - [ - 11, - -19 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 36, - 33 - ], - [ - 61, - 177 - ], - [ - 95, - 50 - ], - [ - 58, - -4 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - -44, - -5 - ], - [ - -10, - -79 - ], - [ - -131, - 19 - ], - [ - -19, - -67 - ], - [ - -67, - 1 - ], - [ - -46, - -86 - ], - [ - -69, - -133 - ], - [ - -109, - -168 - ], - [ - 26, - -41 - ], - [ - -24, - -48 - ], - [ - -70, - 2 - ], - [ - -45, - -112 - ], - [ - 4, - -160 - ], - [ - 45, - -60 - ], - [ - -23, - -142 - ], - [ - -58, - -82 - ], - [ - -31, - -69 - ] - ], - [ - [ - 13316, - 17858 - ], - [ - -47, - 74 - ], - [ - -137, - -139 - ], - [ - -93, - -28 - ], - [ - -97, - 61 - ], - [ - -24, - 129 - ], - [ - -23, - 277 - ], - [ - 65, - 77 - ], - [ - 184, - 101 - ], - [ - 137, - 124 - ], - [ - 128, - 167 - ], - [ - 167, - 231 - ], - [ - 117, - 91 - ], - [ - 192, - 150 - ], - [ - 153, - 53 - ], - [ - 114, - -7 - ], - [ - 107, - 100 - ], - [ - 127, - -6 - ], - [ - 125, - 24 - ], - [ - 218, - -88 - ], - [ - -90, - -32 - ], - [ - 77, - -75 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - -119, - -48 - ], - [ - -56, - -11 - ] - ], - [ - [ - 14271, - 20137 - ], - [ - -156, - -49 - ], - [ - -123, - 28 - ], - [ - 48, - 31 - ], - [ - -42, - 38 - ], - [ - 145, - 24 - ], - [ - 27, - -45 - ], - [ - 101, - -27 - ] - ], - [ - [ - 13820, - 20359 - ], - [ - 229, - -90 - ], - [ - -175, - -47 - ], - [ - -39, - -88 - ], - [ - -61, - -23 - ], - [ - -33, - -99 - ], - [ - -84, - -5 - ], - [ - -150, - 73 - ], - [ - 63, - 43 - ], - [ - -104, - 35 - ], - [ - -136, - 101 - ], - [ - -54, - 94 - ], - [ - 190, - 43 - ], - [ - 38, - -42 - ], - [ - 99, - 2 - ], - [ - 27, - 41 - ], - [ - 102, - 4 - ], - [ - 88, - -42 - ] - ], - [ - [ - 14321, - 20444 - ], - [ - 137, - -43 - ], - [ - -103, - -64 - ], - [ - -203, - -14 - ], - [ - -205, - 20 - ], - [ - -12, - 33 - ], - [ - -101, - 2 - ], - [ - -76, - 55 - ], - [ - 215, - 33 - ], - [ - 102, - -28 - ], - [ - 70, - 36 - ], - [ - 176, - -30 - ] - ], - [ - [ - 24608, - 5888 - ], - [ - 16, - -49 - ], - [ - 50, - 48 - ], - [ - 20, - -50 - ], - [ - 0, - -51 - ], - [ - -26, - -55 - ], - [ - -45, - -89 - ], - [ - -36, - -48 - ], - [ - 26, - -58 - ], - [ - -54, - -1 - ], - [ - -60, - -46 - ], - [ - -18, - -78 - ], - [ - -40, - -121 - ], - [ - -55, - -54 - ], - [ - -35, - -34 - ], - [ - -64, - 2 - ], - [ - -45, - 40 - ], - [ - -76, - 8 - ], - [ - -11, - 44 - ], - [ - 37, - 89 - ], - [ - 88, - 119 - ], - [ - 45, - 22 - ], - [ - 50, - 46 - ], - [ - 60, - 63 - ], - [ - 41, - 62 - ], - [ - 31, - 89 - ], - [ - 27, - 31 - ], - [ - 10, - 67 - ], - [ - 49, - 55 - ], - [ - 15, - -51 - ] - ], - [ - [ - 24719, - 6460 - ], - [ - 51, - -127 - ], - [ - 1, - 82 - ], - [ - 32, - -33 - ], - [ - 10, - -90 - ], - [ - 56, - -39 - ], - [ - 47, - -10 - ], - [ - 40, - 46 - ], - [ - 36, - -14 - ], - [ - -17, - -107 - ], - [ - -21, - -70 - ], - [ - -54, - 3 - ], - [ - -18, - -37 - ], - [ - 6, - -51 - ], - [ - -10, - -22 - ], - [ - -26, - -65 - ], - [ - -35, - -82 - ], - [ - -54, - -48 - ], - [ - -12, - 31 - ], - [ - -29, - 18 - ], - [ - 40, - 98 - ], - [ - -23, - 66 - ], - [ - -75, - 48 - ], - [ - 2, - 44 - ], - [ - 51, - 42 - ], - [ - 12, - 92 - ], - [ - -4, - 78 - ], - [ - -28, - 80 - ], - [ - 2, - 21 - ], - [ - -33, - 50 - ], - [ - -55, - 106 - ], - [ - -29, - 85 - ], - [ - 26, - 9 - ], - [ - 37, - -66 - ], - [ - 55, - -32 - ], - [ - 19, - -106 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 20, - 41 - ], - [ - 9, - -11 - ], - [ - -7, - -49 - ], - [ - -9, - -22 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - 31, - -82 - ], - [ - 39, - -43 - ], - [ - 51, - -16 - ], - [ - 41, - -22 - ], - [ - 32, - -68 - ], - [ - 19, - -40 - ], - [ - 25, - -15 - ], - [ - -1, - -27 - ], - [ - -25, - -72 - ], - [ - -11, - -33 - ], - [ - -29, - -39 - ], - [ - -26, - -82 - ], - [ - -32, - 6 - ], - [ - -15, - -28 - ], - [ - -11, - -61 - ], - [ - 9, - -80 - ], - [ - -7, - -15 - ], - [ - -32, - 0 - ], - [ - -43, - -44 - ], - [ - -7, - -59 - ], - [ - -16, - -25 - ], - [ - -43, - 1 - ], - [ - -28, - -30 - ], - [ - 1, - -49 - ], - [ - -34, - -33 - ], - [ - -39, - 11 - ], - [ - -46, - -40 - ], - [ - -32, - -7 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -23, - 84 - ], - [ - -55, - 198 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - 209, - 120 - ], - [ - 47, - 240 - ], - [ - -32, - 84 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - -51, - 31 - ], - [ - -21, - 86 - ], - [ - -54, - 91 - ], - [ - -128, - -22 - ], - [ - -113, - -2 - ], - [ - -99, - -17 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - -24, - 34 - ], - [ - -15, - 65 - ], - [ - 18, - 32 - ], - [ - -18, - 8 - ], - [ - -13, - 40 - ], - [ - -35, - 33 - ], - [ - -30, - -7 - ], - [ - -14, - -42 - ], - [ - -29, - -30 - ], - [ - -15, - -4 - ], - [ - -7, - -25 - ], - [ - 34, - -65 - ], - [ - -19, - -16 - ], - [ - -11, - -17 - ], - [ - -32, - -7 - ], - [ - -12, - 72 - ], - [ - -9, - -20 - ], - [ - -23, - 7 - ], - [ - -14, - 48 - ], - [ - -29, - 8 - ], - [ - -18, - 14 - ], - [ - -30, - 0 - ], - [ - -2, - -26 - ], - [ - -8, - 18 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - 25, - -43 - ], - [ - -1, - -26 - ], - [ - 28, - -5 - ], - [ - 6, - 10 - ], - [ - 20, - -30 - ], - [ - 34, - 9 - ], - [ - 29, - 30 - ], - [ - 43, - 24 - ], - [ - 24, - 36 - ], - [ - 38, - -7 - ], - [ - -3, - -12 - ], - [ - 39, - -4 - ], - [ - 31, - -20 - ], - [ - 23, - -36 - ], - [ - 26, - -34 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - -70, - 69 - ], - [ - -6, - 49 - ], - [ - -138, - 121 - ], - [ - -125, - 131 - ], - [ - -54, - 74 - ], - [ - -29, - 99 - ], - [ - 12, - 34 - ], - [ - -59, - 158 - ], - [ - -69, - 221 - ], - [ - -66, - 239 - ], - [ - -29, - 55 - ], - [ - -21, - 88 - ], - [ - -55, - 78 - ], - [ - -49, - 49 - ], - [ - 22, - 54 - ], - [ - -34, - 114 - ], - [ - 22, - 84 - ], - [ - 56, - 76 - ] - ], - [ - [ - 21357, - 11807 - ], - [ - 7, - -80 - ], - [ - 4, - -67 - ], - [ - -24, - -110 - ], - [ - -25, - 122 - ], - [ - -33, - -61 - ], - [ - 23, - -88 - ], - [ - -20, - -56 - ], - [ - -82, - 69 - ], - [ - -20, - 87 - ], - [ - 21, - 57 - ], - [ - -44, - 57 - ], - [ - -22, - -50 - ], - [ - -33, - 5 - ], - [ - -51, - -67 - ], - [ - -12, - 35 - ], - [ - 28, - 101 - ], - [ - 44, - 34 - ], - [ - 38, - 45 - ], - [ - 24, - -54 - ], - [ - 53, - 33 - ], - [ - 12, - 53 - ], - [ - 49, - 3 - ], - [ - -4, - 93 - ], - [ - 56, - -57 - ], - [ - 6, - -60 - ], - [ - 5, - -44 - ] - ], - [ - [ - 21190, - 12030 - ], - [ - -25, - -39 - ], - [ - -22, - -76 - ], - [ - -22, - -35 - ], - [ - -43, - 82 - ], - [ - 15, - 33 - ], - [ - 17, - 33 - ], - [ - 8, - 75 - ], - [ - 38, - 7 - ], - [ - -11, - -81 - ], - [ - 52, - 116 - ], - [ - -7, - -115 - ] - ], - [ - [ - 20808, - 11915 - ], - [ - -92, - -114 - ], - [ - 34, - 84 - ], - [ - 50, - 74 - ], - [ - 42, - 83 - ], - [ - 36, - 119 - ], - [ - 13, - -98 - ], - [ - -46, - -66 - ], - [ - -37, - -82 - ] - ], - [ - [ - 21044, - 12224 - ], - [ - 42, - -37 - ], - [ - 44, - 0 - ], - [ - -1, - -50 - ], - [ - -33, - -51 - ], - [ - -44, - -36 - ], - [ - -2, - 56 - ], - [ - 5, - 61 - ], - [ - -11, - 57 - ] - ], - [ - [ - 21296, - 12256 - ], - [ - 20, - -134 - ], - [ - -54, - 32 - ], - [ - 1, - -40 - ], - [ - 17, - -74 - ], - [ - -33, - -27 - ], - [ - -3, - 84 - ], - [ - -21, - 7 - ], - [ - -11, - 72 - ], - [ - 41, - -9 - ], - [ - 0, - 45 - ], - [ - -43, - 92 - ], - [ - 67, - -3 - ], - [ - 19, - -45 - ] - ], - [ - [ - 21019, - 12365 - ], - [ - -19, - -104 - ], - [ - -29, - 60 - ], - [ - -36, - 92 - ], - [ - 60, - -5 - ], - [ - 24, - -43 - ] - ], - [ - [ - 21005, - 13017 - ], - [ - 43, - -34 - ], - [ - 21, - 31 - ], - [ - 6, - -30 - ], - [ - -11, - -50 - ], - [ - 24, - -86 - ], - [ - -18, - -100 - ], - [ - -42, - -40 - ], - [ - -11, - -96 - ], - [ - 16, - -96 - ], - [ - 37, - -13 - ], - [ - 31, - 14 - ], - [ - 87, - -66 - ], - [ - -7, - -66 - ], - [ - 23, - -29 - ], - [ - -7, - -55 - ], - [ - -55, - 59 - ], - [ - -25, - 63 - ], - [ - -18, - -44 - ], - [ - -45, - 72 - ], - [ - -63, - -18 - ], - [ - -35, - 27 - ], - [ - 4, - 49 - ], - [ - 22, - 31 - ], - [ - -21, - 28 - ], - [ - -9, - -44 - ], - [ - -35, - 69 - ], - [ - -10, - 52 - ], - [ - -3, - 115 - ], - [ - 28, - -39 - ], - [ - 8, - 188 - ], - [ - 22, - 108 - ], - [ - 43, - 0 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 121, - -82 - ], - [ - 129, - -69 - ], - [ - 48, - -62 - ], - [ - 39, - -60 - ], - [ - 11, - -71 - ], - [ - 116, - -74 - ], - [ - 17, - -63 - ], - [ - -64, - -13 - ], - [ - 15, - -80 - ], - [ - 62, - -79 - ], - [ - 46, - -127 - ], - [ - 39, - 4 - ], - [ - -2, - -53 - ], - [ - 53, - -21 - ], - [ - -20, - -22 - ], - [ - 74, - -51 - ], - [ - -8, - -34 - ], - [ - -46, - -9 - ], - [ - -17, - 31 - ], - [ - -60, - 14 - ], - [ - -71, - 18 - ], - [ - -54, - 76 - ], - [ - -39, - 66 - ], - [ - -37, - 105 - ], - [ - -91, - 53 - ], - [ - -59, - -34 - ], - [ - -42, - -40 - ], - [ - 9, - -88 - ], - [ - -55, - -42 - ], - [ - -39, - 20 - ], - [ - -72, - 5 - ] - ], - [ - [ - 23414, - 9979 - ], - [ - -20, - -12 - ], - [ - -30, - 46 - ], - [ - -31, - 76 - ], - [ - -15, - 92 - ], - [ - 10, - 11 - ], - [ - 8, - -35 - ], - [ - 21, - -28 - ], - [ - 33, - -76 - ], - [ - 33, - -40 - ], - [ - -9, - -34 - ] - ], - [ - [ - 23142, - 10140 - ], - [ - -37, - -10 - ], - [ - -11, - -34 - ], - [ - -38, - -29 - ], - [ - -35, - -28 - ], - [ - -37, - 0 - ], - [ - -58, - 35 - ], - [ - -39, - 34 - ], - [ - 5, - 37 - ], - [ - 63, - -18 - ], - [ - 38, - 10 - ], - [ - 10, - 57 - ], - [ - 10, - 3 - ], - [ - 7, - -64 - ], - [ - 40, - 10 - ], - [ - 20, - 41 - ], - [ - 39, - 42 - ], - [ - -8, - 71 - ], - [ - 42, - 2 - ], - [ - 14, - -19 - ], - [ - -2, - -67 - ], - [ - -23, - -73 - ] - ], - [ - [ - 23223, - 10257 - ], - [ - -22, - -32 - ], - [ - -13, - 71 - ], - [ - -17, - 47 - ], - [ - -31, - 39 - ], - [ - -40, - 51 - ], - [ - -50, - 35 - ], - [ - 19, - 29 - ], - [ - 38, - -33 - ], - [ - 24, - -27 - ], - [ - 29, - -29 - ], - [ - 28, - -50 - ], - [ - 26, - -38 - ], - [ - 9, - -63 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - 35, - -105 - ], - [ - -8, - -33 - ], - [ - -34, - -14 - ], - [ - -64, - -100 - ], - [ - 18, - -54 - ], - [ - -15, - 7 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -66, - 46 - ], - [ - -50, - -17 - ], - [ - -33, - 12 - ], - [ - -42, - -25 - ], - [ - -35, - 42 - ], - [ - -28, - -16 - ], - [ - -4, - 7 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 47, - 36 - ], - [ - 109, - 55 - ], - [ - 88, - 41 - ], - [ - 70, - -21 - ], - [ - 5, - -29 - ], - [ - 67, - -1 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 86, - -14 - ], - [ - 128, - 2 - ] - ], - [ - [ - 7927, - 13018 - ], - [ - 36, - -10 - ], - [ - 12, - -24 - ], - [ - -18, - -30 - ], - [ - -52, - 0 - ], - [ - -41, - -4 - ], - [ - -4, - 52 - ], - [ - 10, - 17 - ], - [ - 57, - -1 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - 10, - -21 - ] - ], - [ - [ - 21664, - 15862 - ], - [ - -27, - 7 - ], - [ - -30, - -40 - ], - [ - -21, - -41 - ], - [ - 3, - -86 - ], - [ - -36, - -27 - ], - [ - -12, - -21 - ], - [ - -27, - -35 - ], - [ - -46, - -20 - ], - [ - -30, - -32 - ], - [ - -3, - -52 - ], - [ - -8, - -13 - ], - [ - 28, - -20 - ], - [ - 40, - -53 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - -34, - 23 - ], - [ - -8, - -23 - ], - [ - -21, - -10 - ], - [ - -2, - 23 - ], - [ - -18, - 11 - ], - [ - -19, - 19 - ], - [ - 19, - 53 - ], - [ - 17, - 14 - ], - [ - -7, - 22 - ], - [ - 18, - 65 - ], - [ - -5, - 19 - ], - [ - -40, - 13 - ], - [ - -33, - 32 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -28, - -31 - ], - [ - -37, - 17 - ], - [ - -36, - -14 - ], - [ - 11, - 94 - ], - [ - -7, - 74 - ], - [ - -31, - 11 - ], - [ - -17, - 45 - ], - [ - 6, - 79 - ], - [ - 28, - 44 - ], - [ - 5, - 48 - ], - [ - 14, - 72 - ], - [ - -1, - 51 - ], - [ - -14, - 43 - ], - [ - -3, - 41 - ] - ], - [ - [ - 16089, - 13767 - ], - [ - -4, - 87 - ], - [ - 19, - 63 - ], - [ - 19, - 13 - ], - [ - 21, - -37 - ], - [ - 1, - -71 - ], - [ - -15, - -70 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - -20, - -9 - ], - [ - -21, - 24 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - -13, - 21 - ], - [ - 16, - 20 - ], - [ - -17, - 15 - ], - [ - -22, - -27 - ], - [ - -40, - 35 - ], - [ - -6, - 50 - ], - [ - -42, - 28 - ], - [ - -8, - 38 - ], - [ - -38, - 47 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - 30, - 25 - ], - [ - 43, - -13 - ], - [ - 45, - 0 - ], - [ - 32, - -30 - ], - [ - 24, - 19 - ], - [ - 51, - 11 - ], - [ - 18, - 28 - ], - [ - 29, - 0 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - 31, - -22 - ], - [ - 32, - 20 - ], - [ - 32, - -21 - ] - ], - [ - [ - 14611, - 16231 - ], - [ - 2, - -31 - ], - [ - -34, - -26 - ], - [ - -21, - 11 - ], - [ - -20, - -144 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - -89, - 101 - ], - [ - -80, - 46 - ], - [ - -60, - 70 - ], - [ - 51, - 19 - ], - [ - 58, - 101 - ], - [ - -39, - 47 - ], - [ - 102, - 49 - ], - [ - -1, - 26 - ], - [ - -63, - -19 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - 2, - 53 - ], - [ - 36, - 34 - ], - [ - 68, - 9 - ], - [ - 11, - 40 - ], - [ - -16, - 66 - ], - [ - 28, - 63 - ], - [ - 0, - 35 - ], - [ - -103, - 39 - ], - [ - -41, - -1 - ], - [ - -43, - 56 - ], - [ - -53, - -19 - ], - [ - -89, - 42 - ], - [ - 2, - 23 - ], - [ - -25, - 53 - ], - [ - -56, - 5 - ], - [ - -6, - 38 - ], - [ - 18, - 24 - ], - [ - -45, - 68 - ], - [ - -72, - -12 - ], - [ - -21, - 6 - ], - [ - -18, - -27 - ], - [ - -26, - 5 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 79, - 67 - ], - [ - -73, - 57 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - 71, - 42 - ], - [ - 115, - -73 - ], - [ - 191, - -28 - ], - [ - 263, - -136 - ], - [ - 54, - -57 - ], - [ - 4, - -80 - ], - [ - -77, - -63 - ], - [ - -114, - -32 - ], - [ - -311, - 91 - ], - [ - -51, - -15 - ], - [ - 113, - -88 - ], - [ - 5, - -56 - ], - [ - 4, - -122 - ], - [ - 90, - -37 - ], - [ - 55, - -31 - ], - [ - 9, - 58 - ], - [ - -42, - 52 - ], - [ - 44, - 45 - ], - [ - 168, - -74 - ], - [ - 59, - 29 - ], - [ - -47, - 88 - ], - [ - 163, - 117 - ], - [ - 64, - -7 - ], - [ - 65, - -42 - ], - [ - 41, - 83 - ], - [ - -58, - 71 - ], - [ - 34, - 72 - ], - [ - -51, - 75 - ], - [ - 195, - -39 - ], - [ - 39, - -67 - ], - [ - -88, - -15 - ], - [ - 1, - -67 - ], - [ - 54, - -41 - ], - [ - 108, - 26 - ], - [ - 17, - 77 - ], - [ - 146, - 57 - ], - [ - 243, - 103 - ], - [ - 53, - -6 - ], - [ - -69, - -73 - ], - [ - 86, - -12 - ], - [ - 50, - 41 - ], - [ - 131, - 3 - ], - [ - 103, - 50 - ], - [ - 80, - -73 - ], - [ - 79, - 80 - ], - [ - -73, - 69 - ], - [ - 36, - 40 - ], - [ - 206, - -36 - ], - [ - 97, - -38 - ], - [ - 252, - -137 - ], - [ - 47, - 63 - ], - [ - -71, - 63 - ], - [ - -2, - 26 - ], - [ - -84, - 12 - ], - [ - 23, - 56 - ], - [ - -37, - 94 - ], - [ - -2, - 38 - ], - [ - 128, - 109 - ], - [ - 46, - 109 - ], - [ - 52, - 24 - ], - [ - 184, - -32 - ], - [ - 15, - -67 - ], - [ - -66, - -97 - ], - [ - 43, - -38 - ], - [ - 23, - -84 - ], - [ - -16, - -164 - ], - [ - 77, - -74 - ], - [ - -30, - -80 - ], - [ - -137, - -170 - ], - [ - 80, - -18 - ], - [ - 28, - 43 - ], - [ - 76, - 31 - ], - [ - 19, - 59 - ], - [ - 60, - 57 - ], - [ - -40, - 69 - ], - [ - 32, - 79 - ], - [ - -76, - 10 - ], - [ - -17, - 66 - ], - [ - 56, - 121 - ], - [ - -91, - 98 - ], - [ - 125, - 80 - ], - [ - -16, - 86 - ], - [ - 35, - 3 - ], - [ - 36, - -67 - ], - [ - -27, - -116 - ], - [ - 74, - -22 - ], - [ - -31, - 87 - ], - [ - 116, - 47 - ], - [ - 145, - 6 - ], - [ - 129, - -68 - ], - [ - -62, - 100 - ], - [ - -7, - 128 - ], - [ - 121, - 24 - ], - [ - 168, - -5 - ], - [ - 151, - 15 - ], - [ - -57, - 63 - ], - [ - 81, - 79 - ], - [ - 80, - 3 - ], - [ - 135, - 60 - ], - [ - 184, - 16 - ], - [ - 24, - 32 - ], - [ - 183, - 12 - ], - [ - 57, - -27 - ], - [ - 156, - 63 - ], - [ - 128, - -2 - ], - [ - 20, - 52 - ], - [ - 66, - 51 - ], - [ - 165, - 50 - ], - [ - 119, - -39 - ], - [ - -95, - -30 - ], - [ - 158, - -18 - ], - [ - 19, - -60 - ], - [ - 64, - 30 - ], - [ - 204, - -2 - ], - [ - 157, - -59 - ], - [ - 56, - -44 - ], - [ - -18, - -63 - ], - [ - -77, - -35 - ], - [ - -183, - -67 - ], - [ - -52, - -36 - ], - [ - 86, - -16 - ], - [ - 103, - -31 - ], - [ - 63, - 23 - ], - [ - 35, - -77 - ], - [ - 31, - 31 - ], - [ - 112, - 19 - ], - [ - 223, - -20 - ], - [ - 17, - -56 - ], - [ - 292, - -18 - ], - [ - 4, - 92 - ], - [ - 148, - -21 - ], - [ - 111, - 1 - ], - [ - 112, - -63 - ], - [ - 32, - -77 - ], - [ - -41, - -50 - ], - [ - 88, - -95 - ], - [ - 109, - -49 - ], - [ - 68, - 126 - ], - [ - 111, - -54 - ], - [ - 119, - 33 - ], - [ - 135, - -37 - ], - [ - 52, - 33 - ], - [ - 114, - -16 - ], - [ - -51, - 111 - ], - [ - 92, - 52 - ], - [ - 630, - -78 - ], - [ - 59, - -71 - ], - [ - 183, - -92 - ], - [ - 281, - 23 - ], - [ - 139, - -20 - ], - [ - 58, - -50 - ], - [ - -8, - -87 - ], - [ - 85, - -34 - ], - [ - 94, - 24 - ], - [ - 123, - 3 - ], - [ - 132, - -23 - ], - [ - 132, - 13 - ], - [ - 121, - -107 - ], - [ - 87, - 39 - ], - [ - -57, - 76 - ], - [ - 32, - 54 - ], - [ - 222, - -34 - ], - [ - 145, - 7 - ], - [ - 200, - -57 - ], - [ - 98, - -52 - ], - [ - 0, - -478 - ], - [ - -1, - -1 - ], - [ - -89, - -53 - ], - [ - -90, - 9 - ], - [ - 62, - -64 - ], - [ - 42, - -99 - ], - [ - 32, - -32 - ], - [ - 8, - -49 - ], - [ - -18, - -32 - ], - [ - -130, - 26 - ], - [ - -195, - -90 - ], - [ - -62, - -14 - ], - [ - -106, - -85 - ], - [ - -101, - -73 - ], - [ - -26, - -55 - ], - [ - -100, - 83 - ], - [ - -181, - -94 - ], - [ - -32, - 45 - ], - [ - -67, - -52 - ], - [ - -93, - 17 - ], - [ - -23, - -79 - ], - [ - -84, - -116 - ], - [ - 3, - -49 - ], - [ - 79, - -27 - ], - [ - -9, - -174 - ], - [ - -65, - -5 - ], - [ - -30, - -100 - ], - [ - 29, - -52 - ], - [ - -121, - -61 - ], - [ - -25, - -137 - ], - [ - -104, - -29 - ], - [ - -20, - -122 - ], - [ - -101, - -112 - ], - [ - -26, - 83 - ], - [ - -30, - 175 - ], - [ - -38, - 266 - ], - [ - 33, - 167 - ], - [ - 59, - 71 - ], - [ - 3, - 56 - ], - [ - 109, - 27 - ], - [ - 124, - 151 - ], - [ - 120, - 123 - ], - [ - 126, - 96 - ], - [ - 56, - 169 - ], - [ - -85, - -10 - ], - [ - -42, - -99 - ], - [ - -177, - -131 - ], - [ - -57, - 147 - ], - [ - -180, - -41 - ], - [ - -174, - -201 - ], - [ - 57, - -73 - ], - [ - -155, - -32 - ], - [ - -108, - -12 - ], - [ - 5, - 87 - ], - [ - -108, - 18 - ], - [ - -87, - -59 - ], - [ - -213, - 21 - ], - [ - -229, - -36 - ], - [ - -226, - -234 - ], - [ - -267, - -283 - ], - [ - 110, - -15 - ], - [ - 34, - -75 - ], - [ - 68, - -27 - ], - [ - 44, - 60 - ], - [ - 77, - -8 - ], - [ - 100, - -132 - ], - [ - 3, - -102 - ], - [ - -55, - -120 - ], - [ - -6, - -143 - ], - [ - -31, - -192 - ], - [ - -105, - -173 - ], - [ - -23, - -83 - ], - [ - -95, - -140 - ], - [ - -94, - -138 - ], - [ - -45, - -71 - ], - [ - -93, - -71 - ], - [ - -44, - -1 - ], - [ - -44, - 58 - ], - [ - -93, - -88 - ], - [ - -11, - -40 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -32, - -71 - ], - [ - -67, - -20 - ], - [ - -69, - -124 - ], - [ - 63, - -114 - ], - [ - -7, - -81 - ], - [ - 76, - -141 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 16, - 52 - ], - [ - 96, - 39 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - 19, - 30 - ], - [ - 49, - -26 - ], - [ - 23, - -4 - ], - [ - 9, - -24 - ], - [ - 10, - -4 - ] - ], - [ - [ - 14988, - 16284 - ], - [ - 1, - -10 - ], - [ - 34, - -29 - ], - [ - 71, - 7 - ], - [ - -14, - -43 - ], - [ - -76, - -20 - ], - [ - -95, - -70 - ], - [ - -38, - 25 - ], - [ - 15, - 56 - ], - [ - -76, - 35 - ], - [ - 12, - 23 - ], - [ - 67, - 40 - ], - [ - -11, - 14 - ] - ], - [ - [ - 22561, - 16885 - ], - [ - 70, - -212 - ], - [ - -103, - 39 - ], - [ - -43, - -173 - ], - [ - 68, - -123 - ], - [ - -2, - -84 - ], - [ - -53, - 73 - ], - [ - -46, - -93 - ], - [ - -12, - 100 - ], - [ - 7, - 117 - ], - [ - -8, - 130 - ], - [ - 17, - 90 - ], - [ - 3, - 161 - ], - [ - -41, - 118 - ], - [ - 6, - 164 - ], - [ - 64, - 55 - ], - [ - -27, - 56 - ], - [ - 31, - 16 - ], - [ - 18, - -79 - ], - [ - 24, - -116 - ], - [ - -2, - -118 - ], - [ - 29, - -121 - ] - ], - [ - [ - 348, - 18785 - ], - [ - 47, - -30 - ], - [ - -17, - 88 - ], - [ - 190, - -18 - ], - [ - 136, - -113 - ], - [ - -69, - -52 - ], - [ - -114, - -12 - ], - [ - -2, - -118 - ], - [ - -28, - -24 - ], - [ - -65, - 3 - ], - [ - -53, - 42 - ], - [ - -93, - 35 - ], - [ - -16, - 52 - ], - [ - -70, - 20 - ], - [ - -80, - -16 - ], - [ - -38, - 42 - ], - [ - 16, - 45 - ], - [ - -84, - -29 - ], - [ - 32, - -56 - ], - [ - -40, - -51 - ], - [ - 0, - 478 - ], - [ - 171, - -92 - ], - [ - 183, - -119 - ], - [ - -6, - -75 - ] - ], - [ - [ - 25095, - 19295 - ], - [ - -76, - -6 - ], - [ - -13, - 38 - ], - [ - 89, - 50 - ], - [ - 0, - -82 - ] - ], - [ - [ - 91, - 19302 - ], - [ - -91, - -7 - ], - [ - 0, - 82 - ], - [ - 9, - 5 - ], - [ - 59, - 0 - ], - [ - 101, - -35 - ], - [ - -6, - -16 - ], - [ - -72, - -29 - ] - ], - [ - [ - 22558, - 19580 - ], - [ - -106, - 0 - ], - [ - -143, - 13 - ], - [ - -12, - 6 - ], - [ - 66, - 48 - ], - [ - 87, - 11 - ], - [ - 99, - -46 - ], - [ - 9, - -32 - ] - ], - [ - [ - 23055, - 19805 - ], - [ - -81, - -47 - ], - [ - -111, - 10 - ], - [ - -130, - 48 - ], - [ - 17, - 38 - ], - [ - 130, - -18 - ], - [ - 175, - -31 - ] - ], - [ - [ - 22661, - 19862 - ], - [ - -55, - -89 - ], - [ - -257, - 4 - ], - [ - -115, - -29 - ], - [ - -138, - 78 - ], - [ - 37, - 83 - ], - [ - 92, - 22 - ], - [ - 184, - -5 - ], - [ - 252, - -64 - ] - ], - [ - [ - 16558, - 19281 - ], - [ - -41, - -10 - ], - [ - -228, - 16 - ], - [ - -18, - 53 - ], - [ - -126, - 32 - ], - [ - -11, - 65 - ], - [ - 72, - 25 - ], - [ - -3, - 66 - ], - [ - 139, - 102 - ], - [ - -65, - 15 - ], - [ - 167, - 105 - ], - [ - -18, - 55 - ], - [ - 155, - 63 - ], - [ - 231, - 77 - ], - [ - 232, - 22 - ], - [ - 119, - 45 - ], - [ - 136, - 16 - ], - [ - 48, - -48 - ], - [ - -47, - -37 - ], - [ - -247, - -60 - ], - [ - -213, - -57 - ], - [ - -216, - -114 - ], - [ - -104, - -117 - ], - [ - -109, - -116 - ], - [ - 14, - -99 - ], - [ - 133, - -99 - ] - ], - [ - [ - 19872, - 20192 - ], - [ - -393, - -47 - ], - [ - 128, - 158 - ], - [ - 57, - 13 - ], - [ - 52, - -8 - ], - [ - 177, - -68 - ], - [ - -21, - -48 - ] - ], - [ - [ - 16112, - 20460 - ], - [ - -93, - -15 - ], - [ - -63, - -10 - ], - [ - -10, - -19 - ], - [ - -81, - -20 - ], - [ - -76, - 28 - ], - [ - 40, - 38 - ], - [ - -155, - 3 - ], - [ - 136, - 22 - ], - [ - 106, - 2 - ], - [ - 14, - -33 - ], - [ - 40, - 29 - ], - [ - 66, - 20 - ], - [ - 103, - -26 - ], - [ - -27, - -19 - ] - ], - [ - [ - 19514, - 20260 - ], - [ - -152, - -15 - ], - [ - -194, - 35 - ], - [ - -116, - 46 - ], - [ - -53, - 86 - ], - [ - -95, - 24 - ], - [ - 181, - 82 - ], - [ - 150, - 27 - ], - [ - 136, - -61 - ], - [ - 160, - -116 - ], - [ - -17, - -108 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - 17, - -12 - ], - [ - 42, - 37 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 28, - -68 - ], - [ - -4, - -70 - ], - [ - -21, - -15 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 3, - 50 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - -201, - -46 - ], - [ - -65, - -54 - ], - [ - -50, - -126 - ], - [ - -32, - -20 - ], - [ - -18, - 40 - ], - [ - -26, - -6 - ], - [ - -68, - 12 - ], - [ - -13, - 12 - ], - [ - -80, - -3 - ], - [ - -19, - -11 - ], - [ - -28, - 31 - ], - [ - -19, - -59 - ], - [ - 7, - -50 - ], - [ - -30, - -39 - ] - ], - [ - [ - 15530, - 12758 - ], - [ - -9, - 52 - ], - [ - -21, - 36 - ], - [ - -6, - 48 - ], - [ - -36, - 43 - ], - [ - -37, - 100 - ], - [ - -20, - 98 - ], - [ - -48, - 83 - ], - [ - -31, - 19 - ], - [ - -46, - 115 - ], - [ - -8, - 83 - ], - [ - 3, - 71 - ], - [ - -40, - 133 - ], - [ - -33, - 47 - ], - [ - -38, - 25 - ], - [ - -22, - 68 - ], - [ - 3, - 28 - ], - [ - -19, - 62 - ], - [ - -20, - 27 - ], - [ - -28, - 89 - ], - [ - -42, - 97 - ], - [ - -36, - 82 - ], - [ - -34, - -1 - ], - [ - 10, - 66 - ], - [ - 4, - 42 - ], - [ - 8, - 48 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - 27, - -104 - ], - [ - 34, - -27 - ], - [ - 12, - -42 - ], - [ - 48, - -51 - ], - [ - 4, - -49 - ], - [ - -7, - -40 - ], - [ - 9, - -41 - ], - [ - 20, - -33 - ], - [ - 9, - -40 - ], - [ - 10, - -29 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - 13, - -46 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 1, - 29 - ], - [ - -25, - 35 - ], - [ - -1, - 70 - ], - [ - -15, - 46 - ], - [ - -24, - -7 - ], - [ - 7, - 44 - ], - [ - 18, - 50 - ], - [ - -8, - 50 - ], - [ - 23, - 37 - ], - [ - -15, - 28 - ], - [ - 19, - 74 - ], - [ - 32, - 88 - ], - [ - 60, - -8 - ], - [ - -4, - 476 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - 23, - -118 - ], - [ - -15, - -22 - ], - [ - 10, - -123 - ], - [ - 25, - -144 - ], - [ - 27, - -29 - ], - [ - 38, - -45 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 94 - ], - [ - -10, - 2 - ], - [ - 2, - 60 - ], - [ - -9, - 41 - ], - [ - -36, - 47 - ], - [ - -8, - 87 - ], - [ - 8, - 88 - ], - [ - -32, - 9 - ], - [ - -5, - -27 - ], - [ - -42, - -6 - ], - [ - 17, - -35 - ], - [ - 6, - -72 - ], - [ - -38, - -66 - ], - [ - -35, - -87 - ], - [ - -36, - -12 - ], - [ - -58, - 70 - ], - [ - -27, - -25 - ], - [ - -7, - -35 - ], - [ - -36, - -23 - ], - [ - -2, - -24 - ], - [ - -70, - 0 - ], - [ - -9, - 24 - ], - [ - -51, - 5 - ], - [ - -25, - -21 - ], - [ - -19, - 10 - ], - [ - -36, - 70 - ], - [ - -12, - 33 - ], - [ - -50, - -16 - ], - [ - -19, - -56 - ], - [ - -18, - -107 - ], - [ - -24, - -23 - ], - [ - -21, - -13 - ], - [ - 47, - -47 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - -43, - -55 - ], - [ - -49, - 0 - ], - [ - -56, - -28 - ], - [ - -44, - 27 - ], - [ - -29, - -33 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - -11, - 92 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - -28, - 94 - ], - [ - -35, - 42 - ], - [ - 31, - 23 - ], - [ - 33, - 84 - ], - [ - 17, - 62 - ] - ], - [ - [ - 23849, - 9540 - ], - [ - 19, - -42 - ], - [ - -49, - 1 - ], - [ - -26, - 74 - ], - [ - 41, - -29 - ], - [ - 15, - -4 - ] - ], - [ - [ - 23760, - 9613 - ], - [ - -27, - -3 - ], - [ - -43, - 12 - ], - [ - -14, - 19 - ], - [ - 4, - 47 - ], - [ - 46, - -19 - ], - [ - 23, - -25 - ], - [ - 11, - -31 - ] - ], - [ - [ - 23818, - 9645 - ], - [ - -11, - -22 - ], - [ - -51, - 104 - ], - [ - -15, - 72 - ], - [ - 24, - 0 - ], - [ - 25, - -96 - ], - [ - 28, - -58 - ] - ], - [ - [ - 23692, - 9797 - ], - [ - 3, - -24 - ], - [ - -55, - 51 - ], - [ - -38, - 43 - ], - [ - -26, - 40 - ], - [ - 11, - 12 - ], - [ - 32, - -29 - ], - [ - 57, - -55 - ], - [ - 16, - -38 - ] - ], - [ - [ - 23529, - 9916 - ], - [ - -14, - -7 - ], - [ - -30, - 27 - ], - [ - -29, - 49 - ], - [ - 4, - 20 - ], - [ - 41, - -50 - ], - [ - 28, - -39 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - -19, - 9 - ], - [ - -50, - 49 - ], - [ - -36, - 64 - ], - [ - -12, - 44 - ], - [ - -9, - 88 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - -8, - -28 - ], - [ - -41, - 1 - ], - [ - -25, - 12 - ], - [ - -28, - 24 - ], - [ - -39, - 7 - ], - [ - -20, - 26 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - 23, - -22 - ], - [ - 13, - -49 - ], - [ - 32, - -51 - ], - [ - 34, - 0 - ], - [ - 66, - 31 - ], - [ - 76, - 14 - ], - [ - 61, - 37 - ], - [ - 35, - 8 - ], - [ - 25, - 22 - ], - [ - 40, - 4 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - -1, - -2 - ], - [ - 0, - -49 - ], - [ - 0, - -121 - ], - [ - 0, - -63 - ], - [ - -32, - -74 - ], - [ - -48, - -100 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - 22, - 2 - ], - [ - 32, - 18 - ], - [ - 37, - 12 - ], - [ - 33, - 41 - ], - [ - 26, - 1 - ], - [ - 2, - -33 - ], - [ - -6, - -70 - ], - [ - 0, - -63 - ], - [ - -15, - -44 - ], - [ - -20, - -129 - ], - [ - -33, - -134 - ], - [ - -43, - -153 - ], - [ - -60, - -176 - ], - [ - -60, - -135 - ], - [ - -82, - -163 - ], - [ - -69, - -97 - ], - [ - -105, - -120 - ], - [ - -65, - -91 - ], - [ - -76, - -145 - ], - [ - -16, - -63 - ], - [ - -16, - -29 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - 83, - -24 - ], - [ - 8, - 21 - ], - [ - 56, - 9 - ], - [ - 75, - -32 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -19, - -31 - ], - [ - -14, - -49 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 15, - 11 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -120, - -34 - ], - [ - -68, - -84 - ], - [ - 11, - -73 - ], - [ - -111, - -97 - ], - [ - -134, - -103 - ], - [ - -51, - -169 - ], - [ - 49, - -84 - ], - [ - 67, - -67 - ], - [ - -64, - -135 - ], - [ - -72, - -28 - ], - [ - -27, - -202 - ], - [ - -40, - -112 - ], - [ - -84, - 12 - ], - [ - -40, - -96 - ], - [ - -80, - -5 - ], - [ - -22, - 113 - ], - [ - -59, - 136 - ], - [ - -53, - 170 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -14, - -53 - ], - [ - -41, - -13 - ], - [ - -41, - 65 - ], - [ - -1, - 41 - ], - [ - 19, - 45 - ], - [ - 7, - 35 - ], - [ - 20, - 9 - ], - [ - 35, - -22 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - -7, - 91 - ], - [ - 17, - 50 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - 19, - 26 - ], - [ - 19, - 26 - ], - [ - 4, - 67 - ], - [ - 22, - -23 - ], - [ - 77, - 33 - ], - [ - 37, - -22 - ], - [ - 58, - 0 - ], - [ - 80, - 45 - ], - [ - 37, - -2 - ], - [ - 80, - 19 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -57, - -26 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -63, - 55 - ], - [ - -60, - -2 - ], - [ - 11, - 94 - ], - [ - -62, - 0 - ], - [ - -5, - -132 - ], - [ - -38, - -176 - ], - [ - -23, - -106 - ], - [ - 5, - -86 - ], - [ - 46, - -4 - ], - [ - 28, - -110 - ], - [ - 12, - -103 - ], - [ - 39, - -69 - ], - [ - 42, - -14 - ], - [ - 37, - -62 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - -27, - 46 - ], - [ - -12, - 59 - ], - [ - -37, - 68 - ], - [ - -34, - 57 - ], - [ - -11, - -71 - ], - [ - -14, - 67 - ], - [ - 8, - 75 - ], - [ - 21, - 115 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 39, - 122 - ], - [ - -15, - 89 - ], - [ - -51, - 29 - ], - [ - 18, - 53 - ], - [ - 58, - -6 - ], - [ - 33, - 66 - ], - [ - 22, - 77 - ], - [ - 94, - 28 - ], - [ - -15, - -55 - ], - [ - 10, - -34 - ], - [ - 29, - 3 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - -13, - 85 - ], - [ - 10, - 125 - ], - [ - -54, - 41 - ], - [ - 18, - 82 - ], - [ - -46, - 7 - ], - [ - 15, - 101 - ], - [ - 66, - -29 - ], - [ - 61, - 38 - ], - [ - -51, - 72 - ], - [ - -20, - 69 - ], - [ - -56, - -31 - ], - [ - -7, - -88 - ], - [ - -22, - 78 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - 79, - 2 - ], - [ - -12, - 60 - ], - [ - 60, - 41 - ], - [ - 58, - 70 - ], - [ - 94, - -63 - ], - [ - 8, - -96 - ], - [ - 26, - -25 - ], - [ - 76, - 6 - ], - [ - 23, - -22 - ], - [ - 35, - -124 - ], - [ - 79, - -82 - ], - [ - 46, - -57 - ], - [ - 73, - -59 - ], - [ - 92, - -51 - ], - [ - -2, - -73 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 8, - 29 - ], - [ - 60, - 27 - ], - [ - 49, - 4 - ], - [ - 21, - 15 - ], - [ - 27, - -15 - ], - [ - -26, - -33 - ], - [ - -72, - -52 - ], - [ - -59, - -35 - ] - ], - [ - [ - 8248, - 12088 - ], - [ - 40, - 16 - ], - [ - 15, - -5 - ], - [ - -3, - -89 - ], - [ - -58, - -13 - ], - [ - -13, - 11 - ], - [ - 20, - 33 - ], - [ - -1, - 47 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - 75, - 48 - ], - [ - 49, - -14 - ], - [ - -2, - -61 - ], - [ - 59, - 44 - ], - [ - 5, - -23 - ], - [ - -35, - -59 - ], - [ - 0, - -55 - ], - [ - 24, - -30 - ], - [ - -9, - -104 - ], - [ - -46, - -60 - ], - [ - 13, - -66 - ], - [ - 36, - -2 - ], - [ - 18, - -57 - ], - [ - 26, - -18 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - -25, - 54 - ], - [ - 26, - 45 - ], - [ - -42, - -10 - ], - [ - -59, - 28 - ], - [ - -48, - -70 - ], - [ - -105, - -13 - ], - [ - -57, - 64 - ], - [ - -75, - 4 - ], - [ - -16, - -49 - ], - [ - -48, - -15 - ], - [ - -68, - 64 - ], - [ - -76, - -2 - ], - [ - -41, - 119 - ], - [ - -51, - 67 - ], - [ - 34, - 93 - ], - [ - -44, - 58 - ], - [ - 77, - 114 - ], - [ - 107, - 5 - ], - [ - 30, - 91 - ], - [ - 133, - -16 - ], - [ - 83, - 78 - ], - [ - 82, - 34 - ], - [ - 115, - 3 - ], - [ - 122, - -85 - ], - [ - 100, - -46 - ], - [ - 81, - 18 - ], - [ - 60, - -10 - ], - [ - 82, - 62 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - 8, - -46 - ], - [ - 61, - -39 - ], - [ - -12, - -29 - ], - [ - -83, - -7 - ], - [ - -30, - -37 - ], - [ - -58, - -65 - ], - [ - -22, - 56 - ], - [ - 1, - 25 - ] - ], - [ - [ - 21036, - 13724 - ], - [ - -42, - -193 - ], - [ - -29, - -98 - ], - [ - -37, - 101 - ], - [ - -8, - 89 - ], - [ - 41, - 118 - ], - [ - 56, - 91 - ], - [ - 32, - -36 - ], - [ - -13, - -72 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 24, - 14 - ], - [ - 77, - -1 - ], - [ - 142, - 9 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -32, - -148 - ], - [ - 4, - -68 - ], - [ - 45, - -43 - ], - [ - 2, - -32 - ], - [ - -19, - -72 - ], - [ - 4, - -36 - ], - [ - -5, - -58 - ], - [ - 24, - -75 - ], - [ - 29, - -118 - ], - [ - 26, - -27 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - -39, - 36 - ], - [ - -45, - 20 - ], - [ - -28, - 20 - ], - [ - -29, - 31 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - -56, - -10 - ], - [ - -46, - -38 - ], - [ - -65, - -7 - ], - [ - -60, - -44 - ], - [ - 3, - -65 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - -9, - 13 - ], - [ - -109, - 31 - ], - [ - -4, - 44 - ], - [ - -65, - -14 - ], - [ - -26, - -66 - ], - [ - -54, - -89 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -30, - -75 - ], - [ - -79, - -67 - ], - [ - -51, - 24 - ], - [ - -38, - -13 - ], - [ - -65, - 52 - ], - [ - -47, - -4 - ], - [ - -42, - 66 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - 13, - -39 - ], - [ - -75, - -58 - ], - [ - -72, - -42 - ], - [ - -73, - -35 - ], - [ - -37, - -71 - ], - [ - -12, - -27 - ], - [ - -1, - -64 - ], - [ - 23, - -64 - ], - [ - 29, - -3 - ], - [ - -7, - 44 - ], - [ - 21, - -26 - ], - [ - -6, - -35 - ], - [ - -47, - -19 - ], - [ - -33, - 2 - ], - [ - -52, - -21 - ], - [ - -30, - -6 - ], - [ - -41, - -6 - ], - [ - -58, - -34 - ], - [ - 103, - 22 - ], - [ - 20, - -22 - ], - [ - -97, - -36 - ], - [ - -45, - -1 - ], - [ - 2, - 15 - ], - [ - -21, - -33 - ], - [ - 21, - -6 - ], - [ - -15, - -86 - ], - [ - -51, - -92 - ], - [ - -5, - 31 - ], - [ - -16, - 6 - ], - [ - -22, - 30 - ], - [ - 14, - -65 - ], - [ - 17, - -21 - ], - [ - 1, - -46 - ], - [ - -22, - -46 - ], - [ - -39, - -96 - ], - [ - -7, - 5 - ], - [ - 22, - 81 - ], - [ - -36, - 46 - ], - [ - -8, - 100 - ], - [ - -13, - -52 - ], - [ - 15, - -76 - ], - [ - -46, - 19 - ], - [ - 48, - -39 - ], - [ - 3, - -114 - ], - [ - 20, - -8 - ], - [ - 7, - -42 - ], - [ - 10, - -120 - ], - [ - -45, - -89 - ], - [ - -72, - -35 - ], - [ - -46, - -71 - ], - [ - -34, - -8 - ], - [ - -36, - -44 - ], - [ - -10, - -40 - ], - [ - -76, - -78 - ], - [ - -39, - -57 - ], - [ - -33, - -71 - ], - [ - -11, - -85 - ], - [ - 12, - -83 - ], - [ - 24, - -103 - ], - [ - 30, - -85 - ], - [ - 1, - -52 - ], - [ - 33, - -139 - ], - [ - -2, - -81 - ], - [ - -3, - -47 - ], - [ - -18, - -73 - ], - [ - -21, - -15 - ], - [ - -34, - 15 - ], - [ - -11, - 52 - ], - [ - -26, - 28 - ], - [ - -37, - 103 - ], - [ - -33, - 92 - ], - [ - -10, - 47 - ], - [ - 14, - 79 - ], - [ - -19, - 66 - ], - [ - -55, - 101 - ], - [ - -27, - 18 - ], - [ - -70, - -54 - ], - [ - -13, - 6 - ], - [ - -34, - 56 - ], - [ - -43, - 29 - ], - [ - -79, - -15 - ], - [ - -62, - 13 - ], - [ - -53, - -8 - ], - [ - -29, - -19 - ], - [ - 13, - -31 - ], - [ - -2, - -49 - ], - [ - 15, - -24 - ], - [ - -13, - -16 - ], - [ - -26, - 18 - ], - [ - -26, - -23 - ], - [ - -51, - 4 - ], - [ - -52, - 64 - ], - [ - -60, - -15 - ], - [ - -51, - 27 - ], - [ - -44, - -8 - ], - [ - -58, - -28 - ], - [ - -64, - -89 - ], - [ - -69, - -52 - ], - [ - -38, - -57 - ], - [ - -16, - -54 - ], - [ - -1, - -83 - ], - [ - 4, - -57 - ], - [ - 13, - -41 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - -12, - 62 - ], - [ - -45, - 69 - ], - [ - -33, - 14 - ], - [ - -7, - 34 - ], - [ - -39, - 6 - ], - [ - -25, - 33 - ], - [ - -65, - 12 - ], - [ - -18, - 19 - ], - [ - -8, - 66 - ], - [ - -68, - 120 - ], - [ - -58, - 167 - ], - [ - 2, - 28 - ], - [ - -30, - 40 - ], - [ - -54, - 100 - ], - [ - -10, - 98 - ], - [ - -37, - 66 - ], - [ - 15, - 99 - ], - [ - -2, - 103 - ], - [ - -22, - 92 - ], - [ - 27, - 113 - ], - [ - 8, - 109 - ], - [ - 9, - 109 - ], - [ - -13, - 161 - ], - [ - -22, - 102 - ], - [ - -20, - 56 - ], - [ - 8, - 23 - ], - [ - 101, - -41 - ], - [ - 37, - -113 - ], - [ - 17, - 32 - ], - [ - -11, - 98 - ], - [ - -23, - 99 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - -38, - 45 - ], - [ - -62, - 38 - ], - [ - -19, - 105 - ], - [ - -90, - 97 - ], - [ - -38, - 113 - ], - [ - -67, - 8 - ], - [ - -111, - 3 - ], - [ - -81, - 34 - ], - [ - -144, - 125 - ], - [ - -67, - 23 - ], - [ - -122, - 42 - ], - [ - -97, - -10 - ], - [ - -137, - 55 - ], - [ - -83, - 51 - ], - [ - -77, - -25 - ], - [ - 14, - -83 - ], - [ - -38, - -8 - ], - [ - -81, - -25 - ], - [ - -61, - -40 - ], - [ - -77, - -26 - ], - [ - -10, - 71 - ], - [ - 31, - 117 - ], - [ - 74, - 37 - ], - [ - -19, - 30 - ], - [ - -89, - -66 - ], - [ - -47, - -80 - ], - [ - -101, - -86 - ], - [ - 51, - -58 - ], - [ - -66, - -86 - ], - [ - -75, - -50 - ], - [ - -69, - -37 - ], - [ - -18, - -53 - ], - [ - -109, - -62 - ], - [ - -22, - -56 - ], - [ - -81, - -52 - ], - [ - -48, - 10 - ], - [ - -65, - -34 - ], - [ - -71, - -41 - ], - [ - -58, - -40 - ], - [ - -119, - -34 - ], - [ - -11, - 20 - ], - [ - 76, - 56 - ], - [ - 68, - 37 - ], - [ - 74, - 66 - ], - [ - 87, - 13 - ], - [ - 34, - 50 - ], - [ - 97, - 71 - ], - [ - 15, - 24 - ], - [ - 52, - 43 - ], - [ - 12, - 91 - ], - [ - 35, - 71 - ], - [ - -80, - -37 - ], - [ - -22, - 21 - ], - [ - -38, - -44 - ], - [ - -46, - 61 - ], - [ - -19, - -43 - ], - [ - -26, - 60 - ], - [ - -69, - -48 - ], - [ - -43, - 0 - ], - [ - -6, - 71 - ], - [ - 13, - 44 - ], - [ - -45, - 43 - ], - [ - -91, - -23 - ], - [ - -59, - 56 - ], - [ - -48, - 29 - ], - [ - 0, - 68 - ], - [ - -54, - 51 - ], - [ - 27, - 69 - ], - [ - 57, - 67 - ], - [ - 25, - 62 - ], - [ - 57, - 9 - ], - [ - 47, - -20 - ], - [ - 57, - 58 - ], - [ - 50, - -10 - ], - [ - 53, - 37 - ], - [ - -13, - 55 - ], - [ - -39, - 22 - ], - [ - 52, - 46 - ], - [ - -43, - -2 - ], - [ - -74, - -26 - ], - [ - -21, - -26 - ], - [ - -55, - 26 - ], - [ - -99, - -13 - ], - [ - -102, - 29 - ], - [ - -29, - 48 - ], - [ - -88, - 70 - ], - [ - 98, - 50 - ], - [ - 155, - 58 - ], - [ - 58, - 0 - ], - [ - -10, - -60 - ], - [ - 147, - 5 - ], - [ - -56, - 74 - ], - [ - -86, - 46 - ], - [ - -50, - 60 - ], - [ - -67, - 51 - ], - [ - -95, - 38 - ], - [ - 39, - 63 - ], - [ - 123, - 4 - ], - [ - 88, - 55 - ], - [ - 17, - 58 - ], - [ - 71, - 57 - ], - [ - 68, - 14 - ], - [ - 132, - 53 - ], - [ - 64, - -8 - ], - [ - 108, - 64 - ], - [ - 105, - -25 - ], - [ - 50, - -54 - ], - [ - 31, - 23 - ], - [ - 118, - -7 - ], - [ - -4, - -28 - ], - [ - 107, - -20 - ], - [ - 71, - 12 - ], - [ - 147, - -38 - ], - [ - 134, - -12 - ], - [ - 53, - -15 - ], - [ - 93, - 19 - ], - [ - 106, - -36 - ], - [ - 76, - -17 - ] - ], - [ - [ - 1705, - 13087 - ], - [ - -10, - -20 - ], - [ - -18, - 17 - ], - [ - 2, - 33 - ], - [ - -11, - 44 - ], - [ - 3, - 13 - ], - [ - 12, - 20 - ], - [ - -4, - 23 - ], - [ - 4, - 12 - ], - [ - 5, - -3 - ], - [ - 27, - -20 - ], - [ - 12, - -10 - ], - [ - 11, - -16 - ], - [ - 18, - -42 - ], - [ - -2, - -7 - ], - [ - -27, - -26 - ], - [ - -22, - -18 - ] - ], - [ - [ - 1667, - 13274 - ], - [ - -23, - -9 - ], - [ - -12, - 26 - ], - [ - -8, - 9 - ], - [ - -1, - 8 - ], - [ - 7, - 10 - ], - [ - 25, - -11 - ], - [ - 18, - -19 - ], - [ - -6, - -14 - ] - ], - [ - [ - 1620, - 13338 - ], - [ - -2, - -13 - ], - [ - -37, - 3 - ], - [ - 5, - 15 - ], - [ - 34, - -5 - ] - ], - [ - [ - 1558, - 13355 - ], - [ - -4, - -7 - ], - [ - -5, - 2 - ], - [ - -24, - 4 - ], - [ - -9, - 27 - ], - [ - -3, - 5 - ], - [ - 19, - 17 - ], - [ - 6, - -8 - ], - [ - 20, - -40 - ] - ], - [ - [ - 1440, - 13434 - ], - [ - -8, - -12 - ], - [ - -24, - 22 - ], - [ - 4, - 9 - ], - [ - 10, - 12 - ], - [ - 16, - -3 - ], - [ - 2, - -28 - ] - ], - [ - [ - 1882, - 17649 - ], - [ - -70, - -45 - ], - [ - -36, - 31 - ], - [ - -10, - 56 - ], - [ - 63, - 42 - ], - [ - 37, - 19 - ], - [ - 46, - -8 - ], - [ - 30, - -38 - ], - [ - -60, - -57 - ] - ], - [ - [ - 1005, - 17985 - ], - [ - -43, - -19 - ], - [ - -45, - 22 - ], - [ - -43, - 33 - ], - [ - 69, - 20 - ], - [ - 56, - -10 - ], - [ - 6, - -46 - ] - ], - [ - [ - 576, - 18449 - ], - [ - 43, - -23 - ], - [ - 44, - 13 - ], - [ - 56, - -32 - ], - [ - 69, - -16 - ], - [ - -5, - -13 - ], - [ - -53, - -26 - ], - [ - -53, - 27 - ], - [ - -27, - 21 - ], - [ - -61, - -7 - ], - [ - -17, - 11 - ], - [ - 4, - 45 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -2, - -28 - ], - [ - -41, - -14 - ], - [ - 23, - -55 - ], - [ - -1, - -63 - ], - [ - -31, - -69 - ], - [ - 27, - -95 - ], - [ - 30, - 7 - ], - [ - 15, - 87 - ], - [ - -21, - 42 - ], - [ - -4, - 91 - ], - [ - 87, - 49 - ], - [ - -10, - 56 - ], - [ - 25, - 38 - ], - [ - 25, - -84 - ], - [ - 49, - -2 - ], - [ - 45, - -67 - ], - [ - 3, - -40 - ], - [ - 62, - -1 - ], - [ - 75, - 13 - ], - [ - 40, - -54 - ], - [ - 53, - -15 - ], - [ - 39, - 38 - ], - [ - 1, - 30 - ], - [ - 86, - 7 - ], - [ - 84, - 2 - ], - [ - -59, - -36 - ], - [ - 24, - -56 - ], - [ - 55, - -9 - ], - [ - 53, - -59 - ], - [ - 11, - -96 - ], - [ - 37, - 2 - ], - [ - 27, - -28 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -93, - -103 - ], - [ - -58, - -113 - ], - [ - -15, - -83 - ], - [ - 53, - -127 - ], - [ - 66, - -157 - ], - [ - 63, - -74 - ], - [ - 42, - -96 - ], - [ - 32, - -222 - ], - [ - -9, - -211 - ], - [ - -58, - -79 - ], - [ - -80, - -77 - ], - [ - -57, - -100 - ], - [ - -87, - -112 - ], - [ - -25, - 77 - ], - [ - 19, - 81 - ], - [ - -52, - 68 - ] - ], - [ - [ - 24248, - 8822 - ], - [ - -23, - -16 - ], - [ - -24, - 52 - ], - [ - 3, - 33 - ], - [ - 44, - -69 - ] - ], - [ - [ - 24196, - 9006 - ], - [ - 12, - -97 - ], - [ - -19, - 15 - ], - [ - -15, - -7 - ], - [ - -10, - 34 - ], - [ - -1, - 91 - ], - [ - 33, - -36 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -51, - -32 - ], - [ - -13, - -54 - ], - [ - -2, - -41 - ], - [ - -69, - -50 - ], - [ - -112, - -56 - ], - [ - -62, - -85 - ], - [ - -31, - -6 - ], - [ - -21, - 7 - ], - [ - -40, - -50 - ], - [ - -45, - -23 - ], - [ - -58, - -6 - ], - [ - -18, - -7 - ], - [ - -15, - -32 - ], - [ - -19, - -9 - ], - [ - -10, - -30 - ], - [ - -35, - 2 - ], - [ - -22, - -16 - ], - [ - -48, - 6 - ], - [ - -19, - 70 - ], - [ - 2, - 66 - ], - [ - -11, - 35 - ], - [ - -14, - 89 - ], - [ - -20, - 49 - ], - [ - 14, - 6 - ], - [ - -7, - 55 - ], - [ - 9, - 23 - ], - [ - -3, - 52 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - 29, - -1 - ], - [ - 33, - -21 - ], - [ - 24, - 15 - ], - [ - 37, - -12 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -17, - -87 - ], - [ - -9, - -100 - ], - [ - -18, - -54 - ], - [ - -47, - -61 - ], - [ - -14, - -17 - ], - [ - -29, - -61 - ], - [ - -20, - -62 - ], - [ - -39, - -86 - ], - [ - -79, - -123 - ], - [ - -49, - -72 - ], - [ - -53, - -55 - ], - [ - -73, - -47 - ], - [ - -35, - -6 - ], - [ - -9, - -33 - ], - [ - -43, - 18 - ], - [ - -34, - -23 - ], - [ - -76, - 23 - ], - [ - -42, - -15 - ], - [ - -29, - 7 - ], - [ - -72, - -48 - ], - [ - -59, - -19 - ], - [ - -43, - -45 - ], - [ - -32, - -3 - ], - [ - -30, - 43 - ], - [ - -23, - 2 - ], - [ - -30, - 54 - ], - [ - -3, - -17 - ], - [ - -10, - 32 - ], - [ - 1, - 70 - ], - [ - -23, - 81 - ], - [ - 23, - 22 - ], - [ - -2, - 92 - ], - [ - -46, - 112 - ], - [ - -35, - 102 - ], - [ - 0, - 0 - ], - [ - -50, - 156 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -53, - -17 - ], - [ - -40, - -47 - ], - [ - -8, - -42 - ], - [ - -25, - -10 - ], - [ - -61, - -98 - ], - [ - -38, - -78 - ], - [ - -24, - -3 - ], - [ - -22, - 14 - ], - [ - -78, - 13 - ] - ] - ], - "transform": { - "scale": [ - 0.01434548714883443, - 0.008335499711981569 - ], - "translate": [ - -180, - -90 - ] - }, - "objects": { - "ne_110m_admin_0_countries": { - "type": "GeometryCollection", - "geometries": [ - { - "arcs": [ - [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Afghanistan", - "NAME_LONG": "Afghanistan", - "ABBREV": "Afg.", - "FORMAL_EN": "Islamic State of Afghanistan", - "POP_EST": 34124811, - "POP_RANK": 15, - "GDP_MD_EST": 64080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AF", - "ISO_A3": "AFG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 6, - 7, - 8, - 9 - ] - ], - [ - [ - 10, - 11, - 12 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Angola", - "NAME_LONG": "Angola", - "ABBREV": "Ang.", - "FORMAL_EN": "People's Republic of Angola", - "POP_EST": 29310273, - "POP_RANK": 15, - "GDP_MD_EST": 189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AO", - "ISO_A3": "AGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - 13, - 14, - 15, - 16, - 17 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Albania", - "NAME_LONG": "Albania", - "ABBREV": "Alb.", - "FORMAL_EN": "Republic of Albania", - "POP_EST": 3047987, - "POP_RANK": 12, - "GDP_MD_EST": 33900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AL", - "ISO_A3": "ALB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 18, - 19, - 20, - 21, - 22 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "United Arab Emirates", - "NAME_LONG": "United Arab Emirates", - "ABBREV": "U.A.E.", - "FORMAL_EN": "United Arab Emirates", - "POP_EST": 6072475, - "POP_RANK": 13, - "GDP_MD_EST": 667200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AE", - "ISO_A3": "ARE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 23, - 24 - ] - ], - [ - [ - 25, - 26, - 27, - 28, - 29, - 30 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Argentina", - "NAME_LONG": "Argentina", - "ABBREV": "Arg.", - "FORMAL_EN": "Argentine Republic", - "POP_EST": 44293293, - "POP_RANK": 15, - "GDP_MD_EST": 879400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AR", - "ISO_A3": "ARG", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 31, - 32, - 33, - 34, - 35 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Armenia", - "NAME_LONG": "Armenia", - "ABBREV": "Arm.", - "FORMAL_EN": "Republic of Armenia", - "POP_EST": 3045191, - "POP_RANK": 12, - "GDP_MD_EST": 26300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AM", - "ISO_A3": "ARM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 36 - ] - ], - [ - [ - 37 - ] - ], - [ - [ - 38 - ] - ], - [ - [ - 39 - ] - ], - [ - [ - 40 - ] - ], - [ - [ - 41 - ] - ], - [ - [ - 42 - ] - ], - [ - [ - 43 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Antarctica", - "NAME_LONG": "Antarctica", - "ABBREV": "Ant.", - "FORMAL_EN": "", - "POP_EST": 4050, - "POP_RANK": 4, - "GDP_MD_EST": 810, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "AQ", - "ISO_A3": "ATA", - "CONTINENT": "Antarctica", - "REGION_UN": "Antarctica", - "SUBREGION": "Antarctica" - } - }, - { - "arcs": [ - [ - 44 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Fr. S. Antarctic Lands", - "NAME_LONG": "French Southern and Antarctic Lands", - "ABBREV": "Fr. S.A.L.", - "FORMAL_EN": "Territory of the French Southern and Antarctic Lands", - "POP_EST": 140, - "POP_RANK": 1, - "GDP_MD_EST": 16, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TF", - "ISO_A3": "ATF", - "CONTINENT": "Seven seas (open ocean)", - "REGION_UN": "Seven seas (open ocean)", - "SUBREGION": "Seven seas (open ocean)" - } - }, - { - "arcs": [ - [ - [ - 45 - ] - ], - [ - [ - 46 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Australia", - "NAME_LONG": "Australia", - "ABBREV": "Auz.", - "FORMAL_EN": "Commonwealth of Australia", - "POP_EST": 23232413, - "POP_RANK": 15, - "GDP_MD_EST": 1189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AU", - "ISO_A3": "AUS", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - 47, - 48, - 49, - 50, - 51, - 52, - 53 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Austria", - "NAME_LONG": "Austria", - "ABBREV": "Aust.", - "FORMAL_EN": "Republic of Austria", - "POP_EST": 8754413, - "POP_RANK": 13, - "GDP_MD_EST": 416600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AT", - "ISO_A3": "AUT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -33, - 54, - 55, - 56, - 57 - ] - ], - [ - [ - -35, - 58 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Azerbaijan", - "NAME_LONG": "Azerbaijan", - "ABBREV": "Aze.", - "FORMAL_EN": "Republic of Azerbaijan", - "POP_EST": 9961396, - "POP_RANK": 13, - "GDP_MD_EST": 167900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AZ", - "ISO_A3": "AZE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 59, - 60, - 61 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burundi", - "NAME_LONG": "Burundi", - "ABBREV": "Bur.", - "FORMAL_EN": "Republic of Burundi", - "POP_EST": 11466756, - "POP_RANK": 14, - "GDP_MD_EST": 7892, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BI", - "ISO_A3": "BDI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 62, - 63, - 64, - 65, - 66 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belgium", - "NAME_LONG": "Belgium", - "ABBREV": "Belg.", - "FORMAL_EN": "Kingdom of Belgium", - "POP_EST": 11491346, - "POP_RANK": 14, - "GDP_MD_EST": 508600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BE", - "ISO_A3": "BEL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 67, - 68, - 69, - 70, - 71 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Benin", - "NAME_LONG": "Benin", - "ABBREV": "Benin", - "FORMAL_EN": "Republic of Benin", - "POP_EST": 11038805, - "POP_RANK": 14, - "GDP_MD_EST": 24310, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BJ", - "ISO_A3": "BEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -70, - 72, - 73, - 74, - 75, - 76 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burkina Faso", - "NAME_LONG": "Burkina Faso", - "ABBREV": "B.F.", - "FORMAL_EN": "Burkina Faso", - "POP_EST": 20107509, - "POP_RANK": 15, - "GDP_MD_EST": 32990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BF", - "ISO_A3": "BFA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 77, - 78, - 79 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bangladesh", - "NAME_LONG": "Bangladesh", - "ABBREV": "Bang.", - "FORMAL_EN": "People's Republic of Bangladesh", - "POP_EST": 157826578, - "POP_RANK": 17, - "GDP_MD_EST": 628400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BD", - "ISO_A3": "BGD", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 80, - 81, - 82, - 83, - 84, - 85 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bulgaria", - "NAME_LONG": "Bulgaria", - "ABBREV": "Bulg.", - "FORMAL_EN": "Republic of Bulgaria", - "POP_EST": 7101510, - "POP_RANK": 13, - "GDP_MD_EST": 143100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BG", - "ISO_A3": "BGR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 86 - ] - ], - [ - [ - 87 - ] - ], - [ - [ - 88 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Bahamas", - "NAME_LONG": "Bahamas", - "ABBREV": "Bhs.", - "FORMAL_EN": "Commonwealth of the Bahamas", - "POP_EST": 329988, - "POP_RANK": 10, - "GDP_MD_EST": 9066, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BS", - "ISO_A3": "BHS", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 89, - 90, - 91 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bosnia and Herz.", - "NAME_LONG": "Bosnia and Herzegovina", - "ABBREV": "B.H.", - "FORMAL_EN": "Bosnia and Herzegovina", - "POP_EST": 3856181, - "POP_RANK": 12, - "GDP_MD_EST": 42530, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BA", - "ISO_A3": "BIH", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 92, - 93, - 94, - 95, - 96 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belarus", - "NAME_LONG": "Belarus", - "ABBREV": "Bela.", - "FORMAL_EN": "Republic of Belarus", - "POP_EST": 9549747, - "POP_RANK": 13, - "GDP_MD_EST": 165400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BY", - "ISO_A3": "BLR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 97, - 98, - 99 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belize", - "NAME_LONG": "Belize", - "ABBREV": "Belize", - "FORMAL_EN": "Belize", - "POP_EST": 360346, - "POP_RANK": 10, - "GDP_MD_EST": 3088, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BZ", - "ISO_A3": "BLZ", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -27, - 100, - 101, - 102, - 103 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bolivia", - "NAME_LONG": "Bolivia", - "ABBREV": "Bolivia", - "FORMAL_EN": "Plurinational State of Bolivia", - "POP_EST": 11138234, - "POP_RANK": 14, - "GDP_MD_EST": 78350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BO", - "ISO_A3": "BOL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -29, - 104, - -103, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brazil", - "NAME_LONG": "Brazil", - "ABBREV": "Brazil", - "FORMAL_EN": "Federative Republic of Brazil", - "POP_EST": 207353391, - "POP_RANK": 17, - "GDP_MD_EST": 3081000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BR", - "ISO_A3": "BRA", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 113, - 114 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brunei", - "NAME_LONG": "Brunei Darussalam", - "ABBREV": "Brunei", - "FORMAL_EN": "Negara Brunei Darussalam", - "POP_EST": 443593, - "POP_RANK": 10, - "GDP_MD_EST": 33730, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BN", - "ISO_A3": "BRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 115, - 116 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bhutan", - "NAME_LONG": "Bhutan", - "ABBREV": "Bhutan", - "FORMAL_EN": "Kingdom of Bhutan", - "POP_EST": 758288, - "POP_RANK": 11, - "GDP_MD_EST": 6432, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BT", - "ISO_A3": "BTN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 117, - 118, - 119, - 120 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Botswana", - "NAME_LONG": "Botswana", - "ABBREV": "Bwa.", - "FORMAL_EN": "Republic of Botswana", - "POP_EST": 2214858, - "POP_RANK": 12, - "GDP_MD_EST": 35900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BW", - "ISO_A3": "BWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 121, - 122, - 123, - 124, - 125, - 126 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Central African Rep.", - "NAME_LONG": "Central African Republic", - "ABBREV": "C.A.R.", - "FORMAL_EN": "Central African Republic", - "POP_EST": 5625118, - "POP_RANK": 13, - "GDP_MD_EST": 3206, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CF", - "ISO_A3": "CAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 127 - ] - ], - [ - [ - 128 - ] - ], - [ - [ - 129 - ] - ], - [ - [ - 130 - ] - ], - [ - [ - 131 - ] - ], - [ - [ - 132 - ] - ], - [ - [ - 133 - ] - ], - [ - [ - 134 - ] - ], - [ - [ - 135 - ] - ], - [ - [ - 136 - ] - ], - [ - [ - 137, - 138, - 139, - 140 - ] - ], - [ - [ - 141 - ] - ], - [ - [ - 142 - ] - ], - [ - [ - 143 - ] - ], - [ - [ - 144 - ] - ], - [ - [ - 145 - ] - ], - [ - [ - 146 - ] - ], - [ - [ - 147 - ] - ], - [ - [ - 148 - ] - ], - [ - [ - 149 - ] - ], - [ - [ - 150 - ] - ], - [ - [ - 151 - ] - ], - [ - [ - 152 - ] - ], - [ - [ - 153 - ] - ], - [ - [ - 154 - ] - ], - [ - [ - 155 - ] - ], - [ - [ - 156 - ] - ], - [ - [ - 157 - ] - ], - [ - [ - 158 - ] - ], - [ - [ - 159 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Canada", - "NAME_LONG": "Canada", - "ABBREV": "Can.", - "FORMAL_EN": "Canada", - "POP_EST": 35623680, - "POP_RANK": 15, - "GDP_MD_EST": 1674000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CA", - "ISO_A3": "CAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -51, - 160, - 161, - 162 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Switzerland", - "NAME_LONG": "Switzerland", - "ABBREV": "Switz.", - "FORMAL_EN": "Swiss Confederation", - "POP_EST": 8236303, - "POP_RANK": 13, - "GDP_MD_EST": 496300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CH", - "ISO_A3": "CHE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -24, - 163 - ] - ], - [ - [ - -26, - 164, - 165, - -101 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Chile", - "NAME_LONG": "Chile", - "ABBREV": "Chile", - "FORMAL_EN": "Republic of Chile", - "POP_EST": 17789267, - "POP_RANK": 14, - "GDP_MD_EST": 436100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CL", - "ISO_A3": "CHL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -4, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - -117, - 178, - 179, - 180, - 181 - ] - ], - [ - [ - 182 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "China", - "NAME_LONG": "China", - "ABBREV": "China", - "FORMAL_EN": "People's Republic of China", - "POP_EST": 1379302771, - "POP_RANK": 18, - "GDP_MD_EST": 21140000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CN", - "ISO_A3": "CHN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -75, - 183, - 184, - 185, - 186, - 187 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Côte d'Ivoire", - "NAME_LONG": "Côte d'Ivoire", - "ABBREV": "I.C.", - "FORMAL_EN": "Republic of Ivory Coast", - "POP_EST": 24184810, - "POP_RANK": 15, - "GDP_MD_EST": 87120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CI", - "ISO_A3": "CIV", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -127, - 188, - 189, - 190, - 191, - 192, - 193, - 194 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cameroon", - "NAME_LONG": "Cameroon", - "ABBREV": "Cam.", - "FORMAL_EN": "Republic of Cameroon", - "POP_EST": 24994885, - "POP_RANK": 15, - "GDP_MD_EST": 77240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CM", - "ISO_A3": "CMR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -9, - 195, - -13, - 196, - -125, - 197, - 198, - 199, - -60, - 200, - 201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dem. Rep. Congo", - "NAME_LONG": "Democratic Republic of the Congo", - "ABBREV": "D.R.C.", - "FORMAL_EN": "Democratic Republic of the Congo", - "POP_EST": 83301151, - "POP_RANK": 16, - "GDP_MD_EST": 66010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CD", - "ISO_A3": "COD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -12, - 202, - 203, - -189, - -126, - -197 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Congo", - "NAME_LONG": "Republic of the Congo", - "ABBREV": "Rep. Congo", - "FORMAL_EN": "Republic of the Congo", - "POP_EST": 4954674, - "POP_RANK": 12, - "GDP_MD_EST": 30270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CG", - "ISO_A3": "COG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -107, - 204, - 205, - 206, - 207, - 208, - 209 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Colombia", - "NAME_LONG": "Colombia", - "ABBREV": "Col.", - "FORMAL_EN": "Republic of Colombia", - "POP_EST": 47698524, - "POP_RANK": 15, - "GDP_MD_EST": 688000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CO", - "ISO_A3": "COL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 210, - 211, - 212, - 213 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Costa Rica", - "NAME_LONG": "Costa Rica", - "ABBREV": "C.R.", - "FORMAL_EN": "Republic of Costa Rica", - "POP_EST": 4930258, - "POP_RANK": 12, - "GDP_MD_EST": 79260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CR", - "ISO_A3": "CRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - 214 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cuba", - "NAME_LONG": "Cuba", - "ABBREV": "Cuba", - "FORMAL_EN": "Republic of Cuba", - "POP_EST": 11147407, - "POP_RANK": 14, - "GDP_MD_EST": 132900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CU", - "ISO_A3": "CUB", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 215, - 216 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "N. Cyprus", - "NAME_LONG": "Northern Cyprus", - "ABBREV": "N. Cy.", - "FORMAL_EN": "Turkish Republic of Northern Cyprus", - "POP_EST": 265100, - "POP_RANK": 10, - "GDP_MD_EST": 3600, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -217, - 217 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cyprus", - "NAME_LONG": "Cyprus", - "ABBREV": "Cyp.", - "FORMAL_EN": "Republic of Cyprus", - "POP_EST": 1221549, - "POP_RANK": 12, - "GDP_MD_EST": 29260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CY", - "ISO_A3": "CYP", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -53, - 218, - 219, - 220 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Czechia", - "NAME_LONG": "Czech Republic", - "ABBREV": "Cz.", - "FORMAL_EN": "Czech Republic", - "POP_EST": 10674723, - "POP_RANK": 14, - "GDP_MD_EST": 350900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CZ", - "ISO_A3": "CZE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -52, - -163, - 221, - 222, - -63, - 223, - 224, - 225, - 226, - 227, - -219 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Germany", - "NAME_LONG": "Germany", - "ABBREV": "Ger.", - "FORMAL_EN": "Federal Republic of Germany", - "POP_EST": 80594017, - "POP_RANK": 16, - "GDP_MD_EST": 3979000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DE", - "ISO_A3": "DEU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 228, - 229, - 230, - 231 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Djibouti", - "NAME_LONG": "Djibouti", - "ABBREV": "Dji.", - "FORMAL_EN": "Republic of Djibouti", - "POP_EST": 865267, - "POP_RANK": 11, - "GDP_MD_EST": 3345, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DJ", - "ISO_A3": "DJI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -226, - 232 - ] - ], - [ - [ - 233 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Denmark", - "NAME_LONG": "Denmark", - "ABBREV": "Den.", - "FORMAL_EN": "Kingdom of Denmark", - "POP_EST": 5605948, - "POP_RANK": 13, - "GDP_MD_EST": 264800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DK", - "ISO_A3": "DNK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 234, - 235 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dominican Rep.", - "NAME_LONG": "Dominican Republic", - "ABBREV": "Dom. Rep.", - "FORMAL_EN": "Dominican Republic", - "POP_EST": 10734247, - "POP_RANK": 14, - "GDP_MD_EST": 161900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DO", - "ISO_A3": "DOM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 236, - 237, - 238, - 239, - 240, - 241, - 242, - 243 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Algeria", - "NAME_LONG": "Algeria", - "ABBREV": "Alg.", - "FORMAL_EN": "People's Democratic Republic of Algeria", - "POP_EST": 40969443, - "POP_RANK": 15, - "GDP_MD_EST": 609400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DZ", - "ISO_A3": "DZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -206, - 244, - 245 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ecuador", - "NAME_LONG": "Ecuador", - "ABBREV": "Ecu.", - "FORMAL_EN": "Republic of Ecuador", - "POP_EST": 16290913, - "POP_RANK": 14, - "GDP_MD_EST": 182400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EC", - "ISO_A3": "ECU", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 246, - 247, - 248, - 249, - 250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Egypt", - "NAME_LONG": "Egypt", - "ABBREV": "Egypt", - "FORMAL_EN": "Arab Republic of Egypt", - "POP_EST": 97041072, - "POP_RANK": 16, - "GDP_MD_EST": 1105000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EG", - "ISO_A3": "EGY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -232, - 251, - 252, - 253 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eritrea", - "NAME_LONG": "Eritrea", - "ABBREV": "Erit.", - "FORMAL_EN": "State of Eritrea", - "POP_EST": 5918919, - "POP_RANK": 13, - "GDP_MD_EST": 9169, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ER", - "ISO_A3": "ERI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 254, - 255, - 256, - 257 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Spain", - "NAME_LONG": "Spain", - "ABBREV": "Sp.", - "FORMAL_EN": "Kingdom of Spain", - "POP_EST": 48958159, - "POP_RANK": 15, - "GDP_MD_EST": 1690000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ES", - "ISO_A3": "ESP", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 258, - 259, - 260 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Estonia", - "NAME_LONG": "Estonia", - "ABBREV": "Est.", - "FORMAL_EN": "Republic of Estonia", - "POP_EST": 1251581, - "POP_RANK": 12, - "GDP_MD_EST": 38700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EE", - "ISO_A3": "EST", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -231, - 261, - 262, - 263, - 264, - 265, - -252 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ethiopia", - "NAME_LONG": "Ethiopia", - "ABBREV": "Eth.", - "FORMAL_EN": "Federal Democratic Republic of Ethiopia", - "POP_EST": 105350020, - "POP_RANK": 17, - "GDP_MD_EST": 174700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ET", - "ISO_A3": "ETH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 266, - 267, - 268, - 269 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Finland", - "NAME_LONG": "Finland", - "ABBREV": "Fin.", - "FORMAL_EN": "Republic of Finland", - "POP_EST": 5491218, - "POP_RANK": 13, - "GDP_MD_EST": 224137, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FI", - "ISO_A3": "FIN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - [ - 270 - ] - ], - [ - [ - 271 - ] - ], - [ - [ - 272 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Fiji", - "NAME_LONG": "Fiji", - "ABBREV": "Fiji", - "FORMAL_EN": "Republic of Fiji", - "POP_EST": 920938, - "POP_RANK": 11, - "GDP_MD_EST": 8374, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FJ", - "ISO_A3": "FJI", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - 273 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Falkland Is.", - "NAME_LONG": "Falkland Islands", - "ABBREV": "Flk. Is.", - "FORMAL_EN": "Falkland Islands", - "POP_EST": 2931, - "POP_RANK": 4, - "GDP_MD_EST": 281.8, - "POP_YEAR": 2014, - "GDP_YEAR": 2012, - "ISO_A2": "FK", - "ISO_A3": "FLK", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -65, - 274, - -222, - -162, - 275, - 276, - -256, - 277 - ] - ], - [ - [ - -111, - 278, - 279 - ] - ], - [ - [ - 280 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "France", - "NAME_LONG": "France", - "ABBREV": "Fr.", - "FORMAL_EN": "French Republic", - "POP_EST": 67106161, - "POP_RANK": 16, - "GDP_MD_EST": 2699000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FR", - "ISO_A3": "FRA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -190, - -204, - 281, - 282 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gabon", - "NAME_LONG": "Gabon", - "ABBREV": "Gabon", - "FORMAL_EN": "Gabonese Republic", - "POP_EST": 1772255, - "POP_RANK": 12, - "GDP_MD_EST": 35980, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GA", - "ISO_A3": "GAB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 283, - 284 - ] - ], - [ - [ - 285 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United Kingdom", - "NAME_LONG": "United Kingdom", - "ABBREV": "U.K.", - "FORMAL_EN": "United Kingdom of Great Britain and Northern Ireland", - "POP_EST": 64769452, - "POP_RANK": 16, - "GDP_MD_EST": 2788000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GB", - "ISO_A3": "GBR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -32, - 286, - 287, - 288, - -55 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Georgia", - "NAME_LONG": "Georgia", - "ABBREV": "Geo.", - "FORMAL_EN": "Georgia", - "POP_EST": 4926330, - "POP_RANK": 12, - "GDP_MD_EST": 37270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GE", - "ISO_A3": "GEO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -74, - 289, - 290, - -184 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ghana", - "NAME_LONG": "Ghana", - "ABBREV": "Ghana", - "FORMAL_EN": "Republic of Ghana", - "POP_EST": 27499924, - "POP_RANK": 15, - "GDP_MD_EST": 120800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GH", - "ISO_A3": "GHA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -187, - 291, - 292, - 293, - 294, - 295, - 296 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea", - "NAME_LONG": "Guinea", - "ABBREV": "Gin.", - "FORMAL_EN": "Republic of Guinea", - "POP_EST": 12413867, - "POP_RANK": 14, - "GDP_MD_EST": 16080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GN", - "ISO_A3": "GIN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 297, - 298 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gambia", - "NAME_LONG": "The Gambia", - "ABBREV": "Gambia", - "FORMAL_EN": "Republic of the Gambia", - "POP_EST": 2051363, - "POP_RANK": 12, - "GDP_MD_EST": 3387, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GM", - "ISO_A3": "GMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -295, - 299, - 300 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea-Bissau", - "NAME_LONG": "Guinea-Bissau", - "ABBREV": "GnB.", - "FORMAL_EN": "Republic of Guinea-Bissau", - "POP_EST": 1792338, - "POP_RANK": 12, - "GDP_MD_EST": 2851, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GW", - "ISO_A3": "GNB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -191, - -283, - 301 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eq. Guinea", - "NAME_LONG": "Equatorial Guinea", - "ABBREV": "Eq. G.", - "FORMAL_EN": "Republic of Equatorial Guinea", - "POP_EST": 778358, - "POP_RANK": 11, - "GDP_MD_EST": 31770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GQ", - "ISO_A3": "GNQ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - -14, - 302, - -84, - 303, - 304 - ] - ], - [ - [ - 305 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Greece", - "NAME_LONG": "Greece", - "ABBREV": "Greece", - "FORMAL_EN": "Hellenic Republic", - "POP_EST": 10768477, - "POP_RANK": 14, - "GDP_MD_EST": 290500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GR", - "ISO_A3": "GRC", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 306 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Greenland", - "NAME_LONG": "Greenland", - "ABBREV": "Grlnd.", - "FORMAL_EN": "Greenland", - "POP_EST": 57713, - "POP_RANK": 8, - "GDP_MD_EST": 2173, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "GL", - "ISO_A3": "GRL", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -100, - 307, - 308, - 309, - 310, - 311 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guatemala", - "NAME_LONG": "Guatemala", - "ABBREV": "Guat.", - "FORMAL_EN": "Republic of Guatemala", - "POP_EST": 15460732, - "POP_RANK": 14, - "GDP_MD_EST": 131800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GT", - "ISO_A3": "GTM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -109, - 312, - 313, - 314 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guyana", - "NAME_LONG": "Guyana", - "ABBREV": "Guy.", - "FORMAL_EN": "Co-operative Republic of Guyana", - "POP_EST": 737718, - "POP_RANK": 11, - "GDP_MD_EST": 6093, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GY", - "ISO_A3": "GUY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -309, - 315, - 316, - 317, - 318 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Honduras", - "NAME_LONG": "Honduras", - "ABBREV": "Hond.", - "FORMAL_EN": "Republic of Honduras", - "POP_EST": 9038741, - "POP_RANK": 13, - "GDP_MD_EST": 43190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HN", - "ISO_A3": "HND", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -91, - 319, - 320, - 321, - 322, - 323 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Croatia", - "NAME_LONG": "Croatia", - "ABBREV": "Cro.", - "FORMAL_EN": "Republic of Croatia", - "POP_EST": 4292095, - "POP_RANK": 12, - "GDP_MD_EST": 94240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HR", - "ISO_A3": "HRV", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -236, - 324 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Haiti", - "NAME_LONG": "Haiti", - "ABBREV": "Haiti", - "FORMAL_EN": "Republic of Haiti", - "POP_EST": 10646714, - "POP_RANK": 14, - "GDP_MD_EST": 19340, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HT", - "ISO_A3": "HTI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -48, - 325, - 326, - 327, - 328, - -323, - 329 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Hungary", - "NAME_LONG": "Hungary", - "ABBREV": "Hun.", - "FORMAL_EN": "Republic of Hungary", - "POP_EST": 9850845, - "POP_RANK": 13, - "GDP_MD_EST": 267600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HU", - "ISO_A3": "HUN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 330 - ] - ], - [ - [ - 331, - 332 - ] - ], - [ - [ - 333 - ] - ], - [ - [ - 334 - ] - ], - [ - [ - 335 - ] - ], - [ - [ - 336 - ] - ], - [ - [ - 337 - ] - ], - [ - [ - 338 - ] - ], - [ - [ - 339, - 340 - ] - ], - [ - [ - 341 - ] - ], - [ - [ - 342 - ] - ], - [ - [ - 343, - 344 - ] - ], - [ - [ - 345 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Indonesia", - "NAME_LONG": "Indonesia", - "ABBREV": "Indo.", - "FORMAL_EN": "Republic of Indonesia", - "POP_EST": 260580739, - "POP_RANK": 17, - "GDP_MD_EST": 3028000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ID", - "ISO_A3": "IDN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -80, - 346, - 347, - -181, - 348, - -179, - -116, - -178, - 349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "India", - "NAME_LONG": "India", - "ABBREV": "India", - "FORMAL_EN": "Republic of India", - "POP_EST": 1281935911, - "POP_RANK": 18, - "GDP_MD_EST": 8721000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IN", - "ISO_A3": "IND", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -284, - 350 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ireland", - "NAME_LONG": "Ireland", - "ABBREV": "Ire.", - "FORMAL_EN": "Ireland", - "POP_EST": 5011102, - "POP_RANK": 13, - "GDP_MD_EST": 322000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IE", - "ISO_A3": "IRL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -6, - 351, - 352, - 353, - 354, - -59, - -34, - -58, - 355, - 356 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iran", - "NAME_LONG": "Iran", - "ABBREV": "Iran", - "FORMAL_EN": "Islamic Republic of Iran", - "POP_EST": 82021564, - "POP_RANK": 16, - "GDP_MD_EST": 1459000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IR", - "ISO_A3": "IRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -354, - 357, - 358, - 359, - 360, - 361, - 362 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iraq", - "NAME_LONG": "Iraq", - "ABBREV": "Iraq", - "FORMAL_EN": "Republic of Iraq", - "POP_EST": 39192111, - "POP_RANK": 15, - "GDP_MD_EST": 596700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IQ", - "ISO_A3": "IRQ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 363 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iceland", - "NAME_LONG": "Iceland", - "ABBREV": "Iceland", - "FORMAL_EN": "Republic of Iceland", - "POP_EST": 339747, - "POP_RANK": 10, - "GDP_MD_EST": 16150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IS", - "ISO_A3": "ISL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 364, - 365, - 366, - 367, - 368, - 369, - -250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Israel", - "NAME_LONG": "Israel", - "ABBREV": "Isr.", - "FORMAL_EN": "State of Israel", - "POP_EST": 8299706, - "POP_RANK": 13, - "GDP_MD_EST": 297000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IL", - "ISO_A3": "ISR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - -50, - 370, - 371, - -276, - -161 - ] - ], - [ - [ - 372 - ] - ], - [ - [ - 373 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Italy", - "NAME_LONG": "Italy", - "ABBREV": "Italy", - "FORMAL_EN": "Italian Republic", - "POP_EST": 62137802, - "POP_RANK": 16, - "GDP_MD_EST": 2221000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IT", - "ISO_A3": "ITA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 374 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jamaica", - "NAME_LONG": "Jamaica", - "ABBREV": "Jam.", - "FORMAL_EN": "Jamaica", - "POP_EST": 2990561, - "POP_RANK": 12, - "GDP_MD_EST": 25390, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JM", - "ISO_A3": "JAM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -361, - 375, - 376, - -370, - 377, - -368, - 378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jordan", - "NAME_LONG": "Jordan", - "ABBREV": "Jord.", - "FORMAL_EN": "Hashemite Kingdom of Jordan", - "POP_EST": 10248069, - "POP_RANK": 14, - "GDP_MD_EST": 86190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JO", - "ISO_A3": "JOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 379 - ] - ], - [ - [ - 380 - ] - ], - [ - [ - 381 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Japan", - "NAME_LONG": "Japan", - "ABBREV": "Japan", - "FORMAL_EN": "Japan", - "POP_EST": 126451398, - "POP_RANK": 17, - "GDP_MD_EST": 4932000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JP", - "ISO_A3": "JPN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -169, - 382, - 383, - 384, - 385, - 386 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kazakhstan", - "NAME_LONG": "Kazakhstan", - "ABBREV": "Kaz.", - "FORMAL_EN": "Republic of Kazakhstan", - "POP_EST": 18556698, - "POP_RANK": 14, - "GDP_MD_EST": 460700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KZ", - "ISO_A3": "KAZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -264, - 387, - 388, - 389, - 390, - 391 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kenya", - "NAME_LONG": "Kenya", - "ABBREV": "Ken.", - "FORMAL_EN": "Republic of Kenya", - "POP_EST": 47615739, - "POP_RANK": 15, - "GDP_MD_EST": 152700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KE", - "ISO_A3": "KEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -168, - 392, - 393, - -383 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kyrgyzstan", - "NAME_LONG": "Kyrgyzstan", - "ABBREV": "Kgz.", - "FORMAL_EN": "Kyrgyz Republic", - "POP_EST": 5789122, - "POP_RANK": 13, - "GDP_MD_EST": 21010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KG", - "ISO_A3": "KGZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - 394, - 395, - 396, - 397 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cambodia", - "NAME_LONG": "Cambodia", - "ABBREV": "Camb.", - "FORMAL_EN": "Kingdom of Cambodia", - "POP_EST": 16204486, - "POP_RANK": 14, - "GDP_MD_EST": 58940, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KH", - "ISO_A3": "KHM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 398, - 399 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Korea", - "NAME_LONG": "Republic of Korea", - "ABBREV": "S.K.", - "FORMAL_EN": "Republic of Korea", - "POP_EST": 51181299, - "POP_RANK": 16, - "GDP_MD_EST": 1929000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KR", - "ISO_A3": "KOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -17, - 400, - 401, - 402 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kosovo", - "NAME_LONG": "Kosovo", - "ABBREV": "Kos.", - "FORMAL_EN": "Republic of Kosovo", - "POP_EST": 1895250, - "POP_RANK": 12, - "GDP_MD_EST": 18490, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "XK", - "ISO_A3": "-99", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -359, - 403, - 404 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kuwait", - "NAME_LONG": "Kuwait", - "ABBREV": "Kwt.", - "FORMAL_EN": "State of Kuwait", - "POP_EST": 2875422, - "POP_RANK": 12, - "GDP_MD_EST": 301100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KW", - "ISO_A3": "KWT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -176, - 405, - -396, - 406, - 407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Laos", - "NAME_LONG": "Lao PDR", - "ABBREV": "Laos", - "FORMAL_EN": "Lao People's Democratic Republic", - "POP_EST": 7126706, - "POP_RANK": 13, - "GDP_MD_EST": 40960, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LA", - "ISO_A3": "LAO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -366, - 408, - 409 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lebanon", - "NAME_LONG": "Lebanon", - "ABBREV": "Leb.", - "FORMAL_EN": "Lebanese Republic", - "POP_EST": 6229794, - "POP_RANK": 13, - "GDP_MD_EST": 85160, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LB", - "ISO_A3": "LBN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -186, - 410, - 411, - -292 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Liberia", - "NAME_LONG": "Liberia", - "ABBREV": "Liberia", - "FORMAL_EN": "Republic of Liberia", - "POP_EST": 4689021, - "POP_RANK": 12, - "GDP_MD_EST": 3881, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LR", - "ISO_A3": "LBR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -243, - 412, - 413, - -248, - 414, - 415, - 416 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Libya", - "NAME_LONG": "Libya", - "ABBREV": "Libya", - "FORMAL_EN": "Libya", - "POP_EST": 6653210, - "POP_RANK": 13, - "GDP_MD_EST": 90890, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LY", - "ISO_A3": "LBY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 417 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sri Lanka", - "NAME_LONG": "Sri Lanka", - "ABBREV": "Sri L.", - "FORMAL_EN": "Democratic Socialist Republic of Sri Lanka", - "POP_EST": 22409381, - "POP_RANK": 15, - "GDP_MD_EST": 236700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LK", - "ISO_A3": "LKA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 418 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lesotho", - "NAME_LONG": "Lesotho", - "ABBREV": "Les.", - "FORMAL_EN": "Kingdom of Lesotho", - "POP_EST": 1958042, - "POP_RANK": 12, - "GDP_MD_EST": 6019, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LS", - "ISO_A3": "LSO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -93, - 419, - 420, - 421, - 422 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lithuania", - "NAME_LONG": "Lithuania", - "ABBREV": "Lith.", - "FORMAL_EN": "Republic of Lithuania", - "POP_EST": 2823859, - "POP_RANK": 12, - "GDP_MD_EST": 85620, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LT", - "ISO_A3": "LTU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -64, - -223, - -275 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Luxembourg", - "NAME_LONG": "Luxembourg", - "ABBREV": "Lux.", - "FORMAL_EN": "Grand Duchy of Luxembourg", - "POP_EST": 594130, - "POP_RANK": 11, - "GDP_MD_EST": 58740, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LU", - "ISO_A3": "LUX", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -94, - -423, - 423, - -261, - 424 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Latvia", - "NAME_LONG": "Latvia", - "ABBREV": "Lat.", - "FORMAL_EN": "Republic of Latvia", - "POP_EST": 1944643, - "POP_RANK": 12, - "GDP_MD_EST": 50650, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LV", - "ISO_A3": "LVA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -240, - 425, - 426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Morocco", - "NAME_LONG": "Morocco", - "ABBREV": "Mor.", - "FORMAL_EN": "Kingdom of Morocco", - "POP_EST": 33986655, - "POP_RANK": 15, - "GDP_MD_EST": 282800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MA", - "ISO_A3": "MAR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 427, - 428 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Moldova", - "NAME_LONG": "Moldova", - "ABBREV": "Mda.", - "FORMAL_EN": "Republic of Moldova", - "POP_EST": 3474121, - "POP_RANK": 12, - "GDP_MD_EST": 18540, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MD", - "ISO_A3": "MDA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 429 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Madagascar", - "NAME_LONG": "Madagascar", - "ABBREV": "Mad.", - "FORMAL_EN": "Republic of Madagascar", - "POP_EST": 25054161, - "POP_RANK": 15, - "GDP_MD_EST": 36860, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MG", - "ISO_A3": "MDG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -98, - -312, - 430, - 431, - 432 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mexico", - "NAME_LONG": "Mexico", - "ABBREV": "Mex.", - "FORMAL_EN": "United Mexican States", - "POP_EST": 124574795, - "POP_RANK": 17, - "GDP_MD_EST": 2307000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MX", - "ISO_A3": "MEX", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -18, - -403, - 433, - -85, - -303 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Macedonia", - "NAME_LONG": "Macedonia", - "ABBREV": "Mkd.", - "FORMAL_EN": "Former Yugoslav Republic of Macedonia", - "POP_EST": 2103721, - "POP_RANK": 12, - "GDP_MD_EST": 29520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MK", - "ISO_A3": "MKD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -76, - -188, - -297, - 434, - 435, - -237, - 436 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mali", - "NAME_LONG": "Mali", - "ABBREV": "Mali", - "FORMAL_EN": "Republic of Mali", - "POP_EST": 17885245, - "POP_RANK": 14, - "GDP_MD_EST": 38090, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ML", - "ISO_A3": "MLI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -78, - -350, - -177, - -408, - 437, - 438 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Myanmar", - "NAME_LONG": "Myanmar", - "ABBREV": "Myan.", - "FORMAL_EN": "Republic of the Union of Myanmar", - "POP_EST": 55123814, - "POP_RANK": 16, - "GDP_MD_EST": 311100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MM", - "ISO_A3": "MMR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -16, - 439, - -320, - -90, - 440, - -401 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Montenegro", - "NAME_LONG": "Montenegro", - "ABBREV": "Mont.", - "FORMAL_EN": "Montenegro", - "POP_EST": 642550, - "POP_RANK": 11, - "GDP_MD_EST": 10610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ME", - "ISO_A3": "MNE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -171, - 441 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mongolia", - "NAME_LONG": "Mongolia", - "ABBREV": "Mong.", - "FORMAL_EN": "Mongolia", - "POP_EST": 3068243, - "POP_RANK": 12, - "GDP_MD_EST": 37000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MN", - "ISO_A3": "MNG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - 442, - 443, - 444, - 445, - 446, - 447, - 448, - 449 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mozambique", - "NAME_LONG": "Mozambique", - "ABBREV": "Moz.", - "FORMAL_EN": "Republic of Mozambique", - "POP_EST": 26573706, - "POP_RANK": 15, - "GDP_MD_EST": 35010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MZ", - "ISO_A3": "MOZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -238, - -436, - 450, - 451, - 452 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mauritania", - "NAME_LONG": "Mauritania", - "ABBREV": "Mrt.", - "FORMAL_EN": "Islamic Republic of Mauritania", - "POP_EST": 3758571, - "POP_RANK": 12, - "GDP_MD_EST": 16710, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MR", - "ISO_A3": "MRT", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -450, - 453, - 454 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Malawi", - "NAME_LONG": "Malawi", - "ABBREV": "Mal.", - "FORMAL_EN": "Republic of Malawi", - "POP_EST": 19196246, - "POP_RANK": 14, - "GDP_MD_EST": 21200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MW", - "ISO_A3": "MWI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -115, - 455, - -344, - 456 - ] - ], - [ - [ - 457, - 458 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Malaysia", - "NAME_LONG": "Malaysia", - "ABBREV": "Malay.", - "FORMAL_EN": "Malaysia", - "POP_EST": 31381992, - "POP_RANK": 15, - "GDP_MD_EST": 863000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MY", - "ISO_A3": "MYS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -7, - 459, - -119, - 460, - 461 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Namibia", - "NAME_LONG": "Namibia", - "ABBREV": "Nam.", - "FORMAL_EN": "Republic of Namibia", - "POP_EST": 2484780, - "POP_RANK": 12, - "GDP_MD_EST": 25990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NA", - "ISO_A3": "NAM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 462 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "New Caledonia", - "NAME_LONG": "New Caledonia", - "ABBREV": "New C.", - "FORMAL_EN": "New Caledonia", - "POP_EST": 279070, - "POP_RANK": 10, - "GDP_MD_EST": 10770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NC", - "ISO_A3": "NCL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -71, - -77, - -437, - -244, - -417, - 463, - -194, - 464 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Niger", - "NAME_LONG": "Niger", - "ABBREV": "Niger", - "FORMAL_EN": "Republic of Niger", - "POP_EST": 19245344, - "POP_RANK": 14, - "GDP_MD_EST": 20150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NE", - "ISO_A3": "NER", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -72, - -465, - -193, - 465 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nigeria", - "NAME_LONG": "Nigeria", - "ABBREV": "Nigeria", - "FORMAL_EN": "Federal Republic of Nigeria", - "POP_EST": 190632261, - "POP_RANK": 17, - "GDP_MD_EST": 1089000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NG", - "ISO_A3": "NGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -212, - 466, - -317, - 467 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nicaragua", - "NAME_LONG": "Nicaragua", - "ABBREV": "Nic.", - "FORMAL_EN": "Republic of Nicaragua", - "POP_EST": 6025951, - "POP_RANK": 13, - "GDP_MD_EST": 33550, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NI", - "ISO_A3": "NIC", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -67, - 468, - -224 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Netherlands", - "NAME_LONG": "Netherlands", - "ABBREV": "Neth.", - "FORMAL_EN": "Kingdom of the Netherlands", - "POP_EST": 17084719, - "POP_RANK": 14, - "GDP_MD_EST": 870800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NL", - "ISO_A3": "NLD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -268, - 469, - 470, - 471 - ] - ], - [ - [ - 472 - ] - ], - [ - [ - 473 - ] - ], - [ - [ - 474 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Norway", - "NAME_LONG": "Norway", - "ABBREV": "Nor.", - "FORMAL_EN": "Kingdom of Norway", - "POP_EST": 5320045, - "POP_RANK": 13, - "GDP_MD_EST": 364700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NO", - "ISO_A3": "NOR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -180, - -349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nepal", - "NAME_LONG": "Nepal", - "ABBREV": "Nepal", - "FORMAL_EN": "Nepal", - "POP_EST": 29384297, - "POP_RANK": 15, - "GDP_MD_EST": 71520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NP", - "ISO_A3": "NPL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 475 - ] - ], - [ - [ - 476 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "New Zealand", - "NAME_LONG": "New Zealand", - "ABBREV": "N.Z.", - "FORMAL_EN": "New Zealand", - "POP_EST": 4510327, - "POP_RANK": 12, - "GDP_MD_EST": 174800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NZ", - "ISO_A3": "NZL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - [ - -20, - 477 - ] - ], - [ - [ - -22, - 478, - 479, - 480 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Oman", - "NAME_LONG": "Oman", - "ABBREV": "Oman", - "FORMAL_EN": "Sultanate of Oman", - "POP_EST": 3424386, - "POP_RANK": 12, - "GDP_MD_EST": 173100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "OM", - "ISO_A3": "OMN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -5, - -182, - -348, - 481, - -352 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Pakistan", - "NAME_LONG": "Pakistan", - "ABBREV": "Pak.", - "FORMAL_EN": "Islamic Republic of Pakistan", - "POP_EST": 204924861, - "POP_RANK": 17, - "GDP_MD_EST": 988200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PK", - "ISO_A3": "PAK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -208, - 482, - -214, - 483 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Panama", - "NAME_LONG": "Panama", - "ABBREV": "Pan.", - "FORMAL_EN": "Republic of Panama", - "POP_EST": 3753142, - "POP_RANK": 12, - "GDP_MD_EST": 93120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PA", - "ISO_A3": "PAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -102, - -166, - 484, - -245, - -205, - -106 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Peru", - "NAME_LONG": "Peru", - "ABBREV": "Peru", - "FORMAL_EN": "Republic of Peru", - "POP_EST": 31036656, - "POP_RANK": 15, - "GDP_MD_EST": 410400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PE", - "ISO_A3": "PER", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - 485 - ] - ], - [ - [ - 486 - ] - ], - [ - [ - 487 - ] - ], - [ - [ - 488 - ] - ], - [ - [ - 489 - ] - ], - [ - [ - 490 - ] - ], - [ - [ - 491 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Philippines", - "NAME_LONG": "Philippines", - "ABBREV": "Phil.", - "FORMAL_EN": "Republic of the Philippines", - "POP_EST": 104256076, - "POP_RANK": 17, - "GDP_MD_EST": 801900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PH", - "ISO_A3": "PHL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - -340, - 492 - ] - ], - [ - [ - 493 - ] - ], - [ - [ - 494 - ] - ], - [ - [ - 495 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Papua New Guinea", - "NAME_LONG": "Papua New Guinea", - "ABBREV": "P.N.G.", - "FORMAL_EN": "Independent State of Papua New Guinea", - "POP_EST": 6909701, - "POP_RANK": 13, - "GDP_MD_EST": 28020, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PG", - "ISO_A3": "PNG", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -97, - 496, - 497, - -220, - -228, - 498, - 499, - -420 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Poland", - "NAME_LONG": "Poland", - "ABBREV": "Pol.", - "FORMAL_EN": "Republic of Poland", - "POP_EST": 38476269, - "POP_RANK": 15, - "GDP_MD_EST": 1052000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PL", - "ISO_A3": "POL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 500 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Puerto Rico", - "NAME_LONG": "Puerto Rico", - "ABBREV": "P.R.", - "FORMAL_EN": "Commonwealth of Puerto Rico", - "POP_EST": 3351827, - "POP_RANK": 12, - "GDP_MD_EST": 131000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PR", - "ISO_A3": "PRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -173, - 501, - 502, - -400, - 503 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "North Korea", - "NAME_LONG": "Dem. Rep. Korea", - "ABBREV": "N.K.", - "FORMAL_EN": "Democratic People's Republic of Korea", - "POP_EST": 25248140, - "POP_RANK": 15, - "GDP_MD_EST": 40000, - "POP_YEAR": 2013, - "GDP_YEAR": 2016, - "ISO_A2": "KP", - "ISO_A3": "PRK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -258, - 504 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Portugal", - "NAME_LONG": "Portugal", - "ABBREV": "Port.", - "FORMAL_EN": "Portuguese Republic", - "POP_EST": 10839514, - "POP_RANK": 14, - "GDP_MD_EST": 297100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PT", - "ISO_A3": "PRT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -28, - -104, - -105 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Paraguay", - "NAME_LONG": "Paraguay", - "ABBREV": "Para.", - "FORMAL_EN": "Republic of Paraguay", - "POP_EST": 6943739, - "POP_RANK": 13, - "GDP_MD_EST": 64670, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PY", - "ISO_A3": "PRY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -369, - -378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Palestine", - "NAME_LONG": "Palestine", - "ABBREV": "Pal.", - "FORMAL_EN": "West Bank and Gaza", - "POP_EST": 4543126, - "POP_RANK": 12, - "GDP_MD_EST": 21220.77, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PS", - "ISO_A3": "PSE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 505, - 506 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Qatar", - "NAME_LONG": "Qatar", - "ABBREV": "Qatar", - "FORMAL_EN": "State of Qatar", - "POP_EST": 2314307, - "POP_RANK": 12, - "GDP_MD_EST": 334500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "QA", - "ISO_A3": "QAT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -81, - 507, - -328, - 508, - -429, - 509, - 510 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Romania", - "NAME_LONG": "Romania", - "ABBREV": "Rom.", - "FORMAL_EN": "Romania", - "POP_EST": 21529967, - "POP_RANK": 15, - "GDP_MD_EST": 441000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RO", - "ISO_A3": "ROU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - -56, - -289, - 511, - 512, - -95, - -425, - -260, - 513, - -269, - -472, - 514, - -502, - -172, - -442, - -170, - -387, - 515 - ] - ], - [ - [ - -421, - -500, - 516 - ] - ], - [ - [ - 519 - ] - ], - [ - [ - 520 - ] - ], - [ - [ - 521 - ] - ], - [ - [ - 522 - ] - ], - [ - [ - 523 - ] - ], - [ - [ - 524 - ] - ], - [ - [ - 525 - ] - ], - [ - [ - 526 - ] - ], - [ - [ - 527 - ] - ], - [ - [ - 528 - ] - ], - [ - [ - 529 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Russia", - "NAME_LONG": "Russian Federation", - "ABBREV": "Rus.", - "FORMAL_EN": "Russian Federation", - "POP_EST": 142257519, - "POP_RANK": 17, - "GDP_MD_EST": 3745000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RU", - "ISO_A3": "RUS", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -61, - -200, - 530, - 531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Rwanda", - "NAME_LONG": "Rwanda", - "ABBREV": "Rwa.", - "FORMAL_EN": "Republic of Rwanda", - "POP_EST": 11901484, - "POP_RANK": 14, - "GDP_MD_EST": 21970, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RW", - "ISO_A3": "RWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -239, - -453, - 532, - -426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "W. Sahara", - "NAME_LONG": "Western Sahara", - "ABBREV": "W. Sah.", - "FORMAL_EN": "Sahrawi Arab Democratic Republic", - "POP_EST": 603253, - "POP_RANK": 11, - "GDP_MD_EST": 906.5, - "POP_YEAR": 2017, - "GDP_YEAR": 2007, - "ISO_A2": "EH", - "ISO_A3": "ESH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -23, - -481, - 533, - 534, - -376, - -360, - -405, - 535, - -507, - 536 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Saudi Arabia", - "NAME_LONG": "Saudi Arabia", - "ABBREV": "Saud.", - "FORMAL_EN": "Kingdom of Saudi Arabia", - "POP_EST": 28571770, - "POP_RANK": 15, - "GDP_MD_EST": 1731000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SA", - "ISO_A3": "SAU", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -123, - 537, - -415, - -247, - 538, - -253, - -266, - 539 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sudan", - "NAME_LONG": "Sudan", - "ABBREV": "Sudan", - "FORMAL_EN": "Republic of the Sudan", - "POP_EST": 37345935, - "POP_RANK": 15, - "GDP_MD_EST": 176300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SD", - "ISO_A3": "SDN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -124, - -540, - -265, - -392, - 540, - -198 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "S. Sudan", - "NAME_LONG": "South Sudan", - "ABBREV": "S. Sud.", - "FORMAL_EN": "Republic of South Sudan", - "POP_EST": 13026129, - "POP_RANK": 14, - "GDP_MD_EST": 20880, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SS", - "ISO_A3": "SSD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -296, - -301, - 541, - -299, - 542, - -451, - -435 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Senegal", - "NAME_LONG": "Senegal", - "ABBREV": "Sen.", - "FORMAL_EN": "Republic of Senegal", - "POP_EST": 14668522, - "POP_RANK": 14, - "GDP_MD_EST": 39720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SN", - "ISO_A3": "SEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - [ - 543 - ] - ], - [ - [ - 544 - ] - ], - [ - [ - 545 - ] - ], - [ - [ - 546 - ] - ], - [ - [ - 547 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Solomon Is.", - "NAME_LONG": "Solomon Islands", - "ABBREV": "S. Is.", - "FORMAL_EN": "", - "POP_EST": 647581, - "POP_RANK": 11, - "GDP_MD_EST": 1198, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SB", - "ISO_A3": "SLB", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -293, - -412, - 548 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sierra Leone", - "NAME_LONG": "Sierra Leone", - "ABBREV": "S.L.", - "FORMAL_EN": "Republic of Sierra Leone", - "POP_EST": 6163195, - "POP_RANK": 13, - "GDP_MD_EST": 10640, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SL", - "ISO_A3": "SLE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -310, - -319, - 549 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "El Salvador", - "NAME_LONG": "El Salvador", - "ABBREV": "El. S.", - "FORMAL_EN": "Republic of El Salvador", - "POP_EST": 6172011, - "POP_RANK": 13, - "GDP_MD_EST": 54790, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SV", - "ISO_A3": "SLV", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -230, - 550, - 551, - -262 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somaliland", - "NAME_LONG": "Somaliland", - "ABBREV": "Solnd.", - "FORMAL_EN": "Republic of Somaliland", - "POP_EST": 3500000, - "POP_RANK": 12, - "GDP_MD_EST": 12250, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -263, - -552, - 552, - -388 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somalia", - "NAME_LONG": "Somalia", - "ABBREV": "Som.", - "FORMAL_EN": "Federal Republic of Somalia", - "POP_EST": 7531386, - "POP_RANK": 13, - "GDP_MD_EST": 4719, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SO", - "ISO_A3": "SOM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -86, - -434, - -402, - -441, - -92, - -324, - -329, - -508 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Serbia", - "NAME_LONG": "Serbia", - "ABBREV": "Serb.", - "FORMAL_EN": "Republic of Serbia", - "POP_EST": 7111024, - "POP_RANK": 13, - "GDP_MD_EST": 101800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RS", - "ISO_A3": "SRB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -110, - -315, - 553, - -279 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Suriname", - "NAME_LONG": "Suriname", - "ABBREV": "Sur.", - "FORMAL_EN": "Republic of Suriname", - "POP_EST": 591919, - "POP_RANK": 11, - "GDP_MD_EST": 8547, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SR", - "ISO_A3": "SUR", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -54, - -221, - -498, - 554, - -326 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovakia", - "NAME_LONG": "Slovakia", - "ABBREV": "Svk.", - "FORMAL_EN": "Slovak Republic", - "POP_EST": 5445829, - "POP_RANK": 13, - "GDP_MD_EST": 168800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SK", - "ISO_A3": "SVK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -49, - -330, - -322, - 555, - -371 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovenia", - "NAME_LONG": "Slovenia", - "ABBREV": "Slo.", - "FORMAL_EN": "Republic of Slovenia", - "POP_EST": 1972126, - "POP_RANK": 12, - "GDP_MD_EST": 68350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SI", - "ISO_A3": "SVN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -267, - 556, - -470 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sweden", - "NAME_LONG": "Sweden", - "ABBREV": "Swe.", - "FORMAL_EN": "Kingdom of Sweden", - "POP_EST": 9960487, - "POP_RANK": 13, - "GDP_MD_EST": 498100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SE", - "ISO_A3": "SWE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -446, - 557 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Swaziland", - "NAME_LONG": "Swaziland", - "ABBREV": "Swz.", - "FORMAL_EN": "Kingdom of Swaziland", - "POP_EST": 1467152, - "POP_RANK": 12, - "GDP_MD_EST": 11060, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SZ", - "ISO_A3": "SWZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -362, - -379, - -367, - -410, - 558, - 559 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Syria", - "NAME_LONG": "Syria", - "ABBREV": "Syria", - "FORMAL_EN": "Syrian Arab Republic", - "POP_EST": 18028549, - "POP_RANK": 14, - "GDP_MD_EST": 50280, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "SY", - "ISO_A3": "SYR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -122, - -195, - -464, - -416, - -538 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Chad", - "NAME_LONG": "Chad", - "ABBREV": "Chad", - "FORMAL_EN": "Republic of Chad", - "POP_EST": 12075985, - "POP_RANK": 14, - "GDP_MD_EST": 30590, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TD", - "ISO_A3": "TCD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -69, - 560, - -290, - -73 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Togo", - "NAME_LONG": "Togo", - "ABBREV": "Togo", - "FORMAL_EN": "Togolese Republic", - "POP_EST": 7965055, - "POP_RANK": 13, - "GDP_MD_EST": 11610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TG", - "ISO_A3": "TGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -395, - 561, - -459, - 562, - -438, - -407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Thailand", - "NAME_LONG": "Thailand", - "ABBREV": "Thai.", - "FORMAL_EN": "Kingdom of Thailand", - "POP_EST": 68414135, - "POP_RANK": 16, - "GDP_MD_EST": 1161000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TH", - "ISO_A3": "THA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -3, - 563, - -393, - -167 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tajikistan", - "NAME_LONG": "Tajikistan", - "ABBREV": "Tjk.", - "FORMAL_EN": "Republic of Tajikistan", - "POP_EST": 8468555, - "POP_RANK": 13, - "GDP_MD_EST": 25810, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TJ", - "ISO_A3": "TJK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -1, - -357, - 564, - -385, - 565 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Turkmenistan", - "NAME_LONG": "Turkmenistan", - "ABBREV": "Turkm.", - "FORMAL_EN": "Turkmenistan", - "POP_EST": 5351277, - "POP_RANK": 13, - "GDP_MD_EST": 94720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TM", - "ISO_A3": "TKM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -332, - 566 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Timor-Leste", - "NAME_LONG": "Timor-Leste", - "ABBREV": "T.L.", - "FORMAL_EN": "Democratic Republic of Timor-Leste", - "POP_EST": 1291358, - "POP_RANK": 12, - "GDP_MD_EST": 4975, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TL", - "ISO_A3": "TLS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 567 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Trinidad and Tobago", - "NAME_LONG": "Trinidad and Tobago", - "ABBREV": "Tr.T.", - "FORMAL_EN": "Republic of Trinidad and Tobago", - "POP_EST": 1218208, - "POP_RANK": 12, - "GDP_MD_EST": 43570, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TT", - "ISO_A3": "TTO", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -242, - 568, - -413 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tunisia", - "NAME_LONG": "Tunisia", - "ABBREV": "Tun.", - "FORMAL_EN": "Republic of Tunisia", - "POP_EST": 11403800, - "POP_RANK": 14, - "GDP_MD_EST": 130800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TN", - "ISO_A3": "TUN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - [ - -36, - -355, - -363, - -560, - 569, - -287 - ] - ], - [ - [ - -83, - 570, - -304 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Turkey", - "NAME_LONG": "Turkey", - "ABBREV": "Tur.", - "FORMAL_EN": "Republic of Turkey", - "POP_EST": 80845215, - "POP_RANK": 16, - "GDP_MD_EST": 1670000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TR", - "ISO_A3": "TUR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 571 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Taiwan", - "NAME_LONG": "Taiwan", - "ABBREV": "Taiwan", - "FORMAL_EN": "", - "POP_EST": 23508428, - "POP_RANK": 15, - "GDP_MD_EST": 1127000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TW", - "ISO_A3": "TWN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -62, - -532, - 572, - -390, - 573, - -443, - -455, - 574, - -201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tanzania", - "NAME_LONG": "Tanzania", - "ABBREV": "Tanz.", - "FORMAL_EN": "United Republic of Tanzania", - "POP_EST": 53950935, - "POP_RANK": 16, - "GDP_MD_EST": 150600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TZ", - "ISO_A3": "TZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -199, - -541, - -391, - -573, - -531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uganda", - "NAME_LONG": "Uganda", - "ABBREV": "Uga.", - "FORMAL_EN": "Republic of Uganda", - "POP_EST": 39570125, - "POP_RANK": 15, - "GDP_MD_EST": 84930, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UG", - "ISO_A3": "UGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -96, - -513, - 575, - -518, - 576, - -510, - -428, - -509, - -327, - -555, - -497 - ], - [ - 517, - 518 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ukraine", - "NAME_LONG": "Ukraine", - "ABBREV": "Ukr.", - "FORMAL_EN": "Ukraine", - "POP_EST": 44033874, - "POP_RANK": 15, - "GDP_MD_EST": 352600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UA", - "ISO_A3": "UKR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -30, - -113, - 577 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uruguay", - "NAME_LONG": "Uruguay", - "ABBREV": "Ury.", - "FORMAL_EN": "Oriental Republic of Uruguay", - "POP_EST": 3360148, - "POP_RANK": 12, - "GDP_MD_EST": 73250, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UY", - "ISO_A3": "URY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -141, - 578, - -432, - 579 - ] - ], - [ - [ - -139, - 580 - ] - ], - [ - [ - 581 - ] - ], - [ - [ - 582 - ] - ], - [ - [ - 583 - ] - ], - [ - [ - 584 - ] - ], - [ - [ - 585 - ] - ], - [ - [ - 586 - ] - ], - [ - [ - 587 - ] - ], - [ - [ - 588 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United States of America", - "NAME_LONG": "United States", - "ABBREV": "U.S.A.", - "FORMAL_EN": "United States of America", - "POP_EST": 326625791, - "POP_RANK": 17, - "GDP_MD_EST": 18560000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "US", - "ISO_A3": "USA", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -2, - -566, - -384, - -394, - -564 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uzbekistan", - "NAME_LONG": "Uzbekistan", - "ABBREV": "Uzb.", - "FORMAL_EN": "Republic of Uzbekistan", - "POP_EST": 29748859, - "POP_RANK": 15, - "GDP_MD_EST": 202300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UZ", - "ISO_A3": "UZB", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -108, - -210, - 589, - -313 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Venezuela", - "NAME_LONG": "Venezuela", - "ABBREV": "Ven.", - "FORMAL_EN": "Bolivarian Republic of Venezuela", - "POP_EST": 31304016, - "POP_RANK": 15, - "GDP_MD_EST": 468600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VE", - "ISO_A3": "VEN", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -175, - 590, - -397, - -406 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Vietnam", - "NAME_LONG": "Vietnam", - "ABBREV": "Viet.", - "FORMAL_EN": "Socialist Republic of Vietnam", - "POP_EST": 96160163, - "POP_RANK": 16, - "GDP_MD_EST": 594900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VN", - "ISO_A3": "VNM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - 591 - ] - ], - [ - [ - 592 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Vanuatu", - "NAME_LONG": "Vanuatu", - "ABBREV": "Van.", - "FORMAL_EN": "Republic of Vanuatu", - "POP_EST": 282814, - "POP_RANK": 10, - "GDP_MD_EST": 723, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VU", - "ISO_A3": "VUT", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -480, - 593, - -534 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Yemen", - "NAME_LONG": "Yemen", - "ABBREV": "Yem.", - "FORMAL_EN": "Republic of Yemen", - "POP_EST": 28036829, - "POP_RANK": 15, - "GDP_MD_EST": 73450, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "YE", - "ISO_A3": "YEM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -118, - 594, - -447, - -558, - -445, - 595, - -461 - ], - [ - -419 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Africa", - "NAME_LONG": "South Africa", - "ABBREV": "S.Af.", - "FORMAL_EN": "Republic of South Africa", - "POP_EST": 54841552, - "POP_RANK": 16, - "GDP_MD_EST": 739100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZA", - "ISO_A3": "ZAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -10, - -202, - -575, - -454, - -449, - 596, - -120, - -460 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zambia", - "NAME_LONG": "Zambia", - "ABBREV": "Zambia", - "FORMAL_EN": "Republic of Zambia", - "POP_EST": 15972000, - "POP_RANK": 14, - "GDP_MD_EST": 65170, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZM", - "ISO_A3": "ZMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -121, - -597, - -448, - -595 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zimbabwe", - "NAME_LONG": "Zimbabwe", - "ABBREV": "Zimb.", - "FORMAL_EN": "Republic of Zimbabwe", - "POP_EST": 13805084, - "POP_RANK": 14, - "GDP_MD_EST": 28330, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZW", - "ISO_A3": "ZWE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - } - ] - } - } -} diff --git a/explorer-nextjs/app/components/ComponentError.tsx b/explorer-nextjs/app/components/ComponentError.tsx deleted file mode 100644 index 00b448fb9e..0000000000 --- a/explorer-nextjs/app/components/ComponentError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; - -export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }} - variant="body1" - data-testid="delegation-total-amount" - > - {text} - </Typography> -); diff --git a/explorer-nextjs/app/components/ContentCard.tsx b/explorer-nextjs/app/components/ContentCard.tsx deleted file mode 100644 index c83049e5c7..0000000000 --- a/explorer-nextjs/app/components/ContentCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Card, CardHeader, CardContent, Typography } from '@mui/material' -import React, { ReactEventHandler } from 'react' - -type ContentCardProps = { - title?: React.ReactNode - subtitle?: string - Icon?: React.ReactNode - Action?: React.ReactNode - errorMsg?: string - onClick?: ReactEventHandler -} - -export const ContentCard: FCWithChildren<ContentCardProps> = ({ - title, - Icon, - Action, - subtitle, - errorMsg, - children, - onClick, -}) => ( - <Card onClick={onClick} sx={{ height: '100%' }}> - {title && ( - <CardHeader - title={title || ''} - avatar={Icon} - action={Action} - subheader={subtitle} - /> - )} - {children && <CardContent>{children}</CardContent>} - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {errorMsg} - </Typography> - )} - </Card> -) diff --git a/explorer-nextjs/app/components/CustomColumnHeading.tsx b/explorer-nextjs/app/components/CustomColumnHeading.tsx deleted file mode 100644 index e767db34c1..0000000000 --- a/explorer-nextjs/app/components/CustomColumnHeading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { Box, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' - -export const CustomColumnHeading: FCWithChildren<{ - headingTitle: string - tooltipInfo?: string -}> = ({ headingTitle, tooltipInfo }) => { - const theme = useTheme() - - return ( - <Box alignItems="center" display="flex"> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={headingTitle} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - <Typography variant="body2" fontWeight={600} data-testid={headingTitle}> - {headingTitle} - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx b/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx deleted file mode 100644 index 8e859f5eba..0000000000 --- a/explorer-nextjs/app/components/Delegations/ConfirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Button, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - onConfirm: () => void; - onClose?: () => void; - children?: React.ReactNode; - title: React.ReactNode | string; - subTitle?: React.ReactNode | string; - confirmButton: React.ReactNode | string; - disabled?: boolean; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; -} - -export const ConfirmationModal = ({ - open, - onConfirm, - onClose, - children, - title, - subTitle, - confirmButton, - disabled, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> - {title} - {subTitle && - (typeof subTitle === 'string' ? ( - <Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey"> - {subTitle} - </Typography> - ) : ( - subTitle - ))} - </DialogTitle> - ); - const ConfirmButton = - typeof confirmButton === 'string' ? ( - <Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}> - <Typography variant="button" fontSize="large"> - {confirmButton} - </Typography> - </Button> - ) : ( - confirmButton - ); - return ( - <Dialog - open={open} - onClose={onClose} - aria-labelledby="responsive-dialog-title" - maxWidth={maxWidth || 'sm'} - sx={{ textAlign: 'center', ...sx }} - fullWidth={fullWidth} - BackdropProps={backdropProps} - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} - > - {Title} - <DialogContent>{children}</DialogContent> - <DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions> - </Dialog> - ); -}; diff --git a/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx b/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx deleted file mode 100644 index 9ea2f54b6b..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegateIconButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import { Button, IconButton } from '@mui/material' -import { SxProps } from '@mui/system' -import { useIsMobile } from '@/app/hooks' -import { DelegateIcon } from '@/app/icons/DelevateSVG' - -export const DelegateIconButton: FCWithChildren<{ - size?: 'small' | 'medium' - disabled?: boolean - tooltip?: React.ReactNode - sx?: SxProps - onDelegate: () => void -}> = ({ onDelegate, sx, disabled, size = 'medium' }) => { - const isMobile = useIsMobile() - - const handleOnDelegate = () => { - onDelegate() - } - - if (isMobile) { - return ( - <IconButton size="small" disabled={disabled} onClick={handleOnDelegate}> - <DelegateIcon fontSize="small" /> - </IconButton> - ) - } - - return ( - <Button - variant="outlined" - size={size} - disabled={disabled} - onClick={handleOnDelegate} - sx={sx} - > - Delegate - </Button> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/DelegateModal.tsx b/explorer-nextjs/app/components/Delegations/DelegateModal.tsx deleted file mode 100644 index 5b453e8ad5..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegateModal.tsx +++ /dev/null @@ -1,191 +0,0 @@ -'use client' - -import React, { useState } from 'react' -import { Box, SxProps } from '@mui/material' -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField' -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField' -import { CurrencyDenom, DecCoin } from '@nymproject/types' -import { useWalletContext } from '@/app/context/wallet' -import { urls } from '@/app/utils' -import { useDelegationsContext } from '@/app/context/delegations' -import { validateAmount } from '@/app/utils/currency' -import { SimpleModal } from './SimpleModal' -import { ModalListItem } from './ModalListItem' -import { DelegationModalProps } from './DelegationModal' - -const MIN_AMOUNT_TO_DELEGATE = 10 - -type Props = { - mixId: number - identityKey: string - header?: string - buttonText?: string - rewardInterval?: string - estimatedReward?: number - profitMarginPercentage?: string | null - nodeUptimePercentage?: number | null - denom: CurrencyDenom - sx?: SxProps - backdropProps?: object - onClose: () => void - onOk?: (delegationModalProps: DelegationModalProps) => void -} - -export const DelegateModal = ({ - mixId, - identityKey, - onClose, - onOk, - denom, - sx, -}: Props) => { - const [amount, setAmount] = useState<DecCoin | undefined>({ - amount: '10', - denom: 'nym', - }) - const [isValidated, setValidated] = useState<boolean>(false) - const [errorAmount, setErrorAmount] = useState<string | undefined>() - - const { address, balance } = useWalletContext() - const { handleDelegate } = useDelegationsContext() - - const validate = async () => { - let newValidatedValue = true - let errorAmountMessage - - if (amount && !(await validateAmount(amount.amount, '0'))) { - newValidatedValue = false - errorAmountMessage = 'Please enter a valid amount' - } - - if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) { - errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}` - newValidatedValue = false - } - - if (!amount?.amount.length) { - newValidatedValue = false - } - - if (amount && balance.data && +balance.data - +amount.amount <= 0) { - errorAmountMessage = 'Not enough funds' - newValidatedValue = false - } - - setErrorAmount(errorAmountMessage) - setValidated(newValidatedValue) - } - - const delegateToMixnode = async ({ - delegationMixId, - delegationAmount, - }: { - delegationMixId: number - delegationAmount: string - }) => { - try { - const tx = await handleDelegate(delegationMixId, delegationAmount) - return tx - } catch (e) { - console.error('Failed to delegate to mixnode', e) - throw e - } - } - - const handleConfirm = async () => { - if (mixId && amount && onOk) { - onOk({ - status: 'loading', - }) - try { - if (!address) { - throw new Error('Please connect your wallet') - } - - const tx = await delegateToMixnode({ - delegationMixId: mixId, - delegationAmount: amount.amount, - }) - - if (!tx) { - throw new Error('Failed to delegate') - } - - onOk({ - status: 'success', - message: 'Delegation can take up to one hour to process', - transactions: [ - { - url: `${urls('MAINNET').blockExplorer}/transaction/${ - tx.transactionHash - }`, - hash: tx.transactionHash, - }, - ], - }) - } catch (e) { - console.error('Failed to delegate', e) - onOk({ - status: 'error', - message: (e as Error).message, - }) - } - } - } - - const handleAmountChanged = (newAmount: DecCoin) => { - setAmount(newAmount) - } - - React.useEffect(() => { - validate() - }, [amount, identityKey, mixId]) - - return ( - <SimpleModal - open - onClose={onClose} - onOk={handleConfirm} - header="Delegate" - okLabel="Delegate" - okDisabled={!isValidated} - sx={sx} - > - <Box sx={{ mt: 3 }} gap={2}> - <IdentityKeyFormField - required - fullWidth - label="Node identity key" - onChanged={() => undefined} - initialValue={identityKey} - readOnly - showTickOnValid={false} - /> - </Box> - - <Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}> - <CurrencyFormField - showCoinMark={false} - required - fullWidth - autoFocus - label="Amount" - initialValue={amount?.amount || '10'} - onChanged={handleAmountChanged} - denom={denom} - validationError={errorAmount} - /> - </Box> - <Box sx={{ mt: 3 }}> - <ModalListItem - label="Account balance" - value={`${balance.data} NYM`} - divider - fontWeight={600} - /> - </Box> - - <ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" /> - </SimpleModal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/DelegationModal.tsx b/explorer-nextjs/app/components/Delegations/DelegationModal.tsx deleted file mode 100644 index 76c37a9e96..0000000000 --- a/explorer-nextjs/app/components/Delegations/DelegationModal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import { Typography, SxProps, Stack } from '@mui/material' -import { Link } from '@nymproject/react/link/Link' -import { LoadingModal } from './LoadingModal' -import { ConfirmationModal } from './ConfirmationModal' -import { ErrorModal } from './ErrorModal' - -export type DelegationModalProps = { - status: 'loading' | 'success' | 'error' | 'info' - message?: string - transactions?: { - url: string - hash: string - }[] -} - -export const DelegationModal: FCWithChildren< - DelegationModalProps & { - open: boolean - onClose: () => void - sx?: SxProps - backdropProps?: object - children?: React.ReactNode - } -> = ({ - status, - message, - transactions, - open, - onClose, - children, - sx, - backdropProps, -}) => { - if (status === 'loading') - return <LoadingModal sx={sx} backdropProps={backdropProps} /> - - if (status === 'error') { - return ( - <ErrorModal message={message} sx={sx} open={open} onClose={onClose}> - {children} - </ErrorModal> - ) - } - - if (status === 'info') { - return ( - <ConfirmationModal - open={open} - title="Connect wallet" - confirmButton="OK" - onConfirm={onClose} - > - <Typography>{message}</Typography> - </ConfirmationModal> - ) - } - - return ( - <ConfirmationModal - open={open} - onConfirm={onClose || (() => {})} - title="Transaction successful" - confirmButton="Done" - > - <Stack alignItems="center" spacing={2} mb={0}> - {message && <Typography>{message}</Typography>} - {transactions?.length === 1 && ( - <Link - href={transactions[0].url} - target="_blank" - sx={{ ml: 1 }} - text="View on blockchain" - noIcon - /> - )} - {transactions && transactions.length > 1 && ( - <Stack alignItems="center" spacing={1}> - <Typography>View the transactions on blockchain:</Typography> - {transactions.map(({ url, hash }) => ( - <Link - href={url} - target="_blank" - sx={{ ml: 1 }} - text={hash.slice(0, 6)} - key={hash} - noIcon - /> - ))} - </Stack> - )} - </Stack> - </ConfirmationModal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/ErrorModal.tsx b/explorer-nextjs/app/components/Delegations/ErrorModal.tsx deleted file mode 100644 index b9218d1e1e..0000000000 --- a/explorer-nextjs/app/components/Delegations/ErrorModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const ErrorModal: FCWithChildren<{ - open: boolean; - title?: string; - message?: string; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - children?: React.ReactNode; -}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( - <Modal open={open} onClose={onClose} BackdropProps={backdropProps}> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Typography color={(theme) => theme.palette.error.main} mb={1}> - {title || 'Oh no! Something went wrong...'} - </Typography> - <Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}> - {message} - </Typography> - {children} - <Button variant="contained" onClick={onClose}> - Close - </Button> - </Box> - </Modal> -); diff --git a/explorer-nextjs/app/components/Delegations/LoadingModal.tsx b/explorer-nextjs/app/components/Delegations/LoadingModal.tsx deleted file mode 100644 index eda13938d4..0000000000 --- a/explorer-nextjs/app/components/Delegations/LoadingModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const LoadingModal: FCWithChildren<{ - text?: string; - sx?: SxProps; - backdropProps?: object; -}> = ({ sx, text = 'Please wait...' }) => ( - <Modal open> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Stack spacing={4} direction="row" alignItems="center"> - <CircularProgress /> - <Typography sx={{ color: 'text.primary' }}>{text}</Typography> - </Stack> - </Box> - </Modal> -); diff --git a/explorer-nextjs/app/components/Delegations/ModalDivider.tsx b/explorer-nextjs/app/components/Delegations/ModalDivider.tsx deleted file mode 100644 index 6258e0bfac..0000000000 --- a/explorer-nextjs/app/components/Delegations/ModalDivider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Box, SxProps } from '@mui/material'; - -export const ModalDivider: FCWithChildren<{ - sx?: SxProps; -}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />; diff --git a/explorer-nextjs/app/components/Delegations/ModalListItem.tsx b/explorer-nextjs/app/components/Delegations/ModalListItem.tsx deleted file mode 100644 index 830c25705e..0000000000 --- a/explorer-nextjs/app/components/Delegations/ModalListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material'; -import { ModalDivider } from './ModalDivider'; - -export const ModalListItem: FCWithChildren<{ - label: string; - divider?: boolean; - hidden?: boolean; - fontWeight?: TypographyProps['fontWeight']; - fontSize?: TypographyProps['fontSize']; - light?: boolean; - value?: React.ReactNode; - sxValue?: SxProps; -}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => ( - <Box sx={{ display: hidden ? 'none' : 'block' }}> - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}> - {label} - </Typography> - {value && ( - <Typography - fontSize="smaller" - fontWeight={fontWeight} - sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }} - > - {value} - </Typography> - )} - </Stack> - {divider && <ModalDivider />} - </Box> -); diff --git a/explorer-nextjs/app/components/Delegations/SimpleModal.tsx b/explorer-nextjs/app/components/Delegations/SimpleModal.tsx deleted file mode 100644 index b1909c8c5e..0000000000 --- a/explorer-nextjs/app/components/Delegations/SimpleModal.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React from 'react' -import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import ErrorOutline from '@mui/icons-material/ErrorOutline' -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export const modalStyle = (width: number | string = 600) => ({ - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - width, - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}) - -export const StyledBackButton = ({ - onBack, - label, - fullWidth, - sx, -}: { - onBack: () => void - label?: string - fullWidth?: boolean - sx?: SxProps -}) => ( - <Button - disableFocusRipple - size="large" - fullWidth={fullWidth} - variant="outlined" - onClick={onBack} - sx={sx} - > - {label || <ArrowBackIosNewIcon fontSize="small" />} - </Button> -) - -export const SimpleModal: FCWithChildren<{ - open: boolean - hideCloseIcon?: boolean - displayErrorIcon?: boolean - displayInfoIcon?: boolean - headerStyles?: SxProps - subHeaderStyles?: SxProps - buttonFullWidth?: boolean - onClose?: () => void - onOk?: () => Promise<void> - onBack?: () => void - header: string | React.ReactNode - subHeader?: string - okLabel: string - backLabel?: string - backButtonFullWidth?: boolean - okDisabled?: boolean - sx?: SxProps - children?: React.ReactNode -}> = ({ - open, - hideCloseIcon, - displayErrorIcon, - displayInfoIcon, - headerStyles, - buttonFullWidth, - onClose, - okDisabled, - onOk, - onBack, - header, - subHeader, - okLabel, - backLabel, - backButtonFullWidth, - sx, - children, -}) => { - const isMobile = useIsMobile() - - return ( - <Modal open={open} onClose={onClose}> - <Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}> - {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} - {displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />} - <Stack - direction="row" - justifyContent="space-between" - alignItems="center" - > - {typeof header === 'string' ? ( - <Typography - fontSize={20} - fontWeight={600} - sx={{ color: 'text.primary', ...headerStyles }} - > - {header} - </Typography> - ) : ( - header - )} - {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} - </Stack> - - <Typography - mt={subHeader ? 0.5 : 0} - mb={3} - fontSize={12} - color={(theme) => theme.palette.text.secondary} - > - {subHeader} - </Typography> - - {children} - - {(onOk || onBack) && ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: 2, - mt: 2, - width: buttonFullWidth ? '100%' : null, - }} - > - {onBack && ( - <StyledBackButton - onBack={onBack} - label={backLabel} - fullWidth={backButtonFullWidth} - /> - )} - {onOk && ( - <Button - variant="contained" - fullWidth - size="large" - onClick={onOk} - disabled={okDisabled} - > - {okLabel} - </Button> - )} - </Box> - )} - </Box> - </Modal> - ) -} diff --git a/explorer-nextjs/app/components/Delegations/index.ts b/explorer-nextjs/app/components/Delegations/index.ts deleted file mode 100644 index da51de9bd9..0000000000 --- a/explorer-nextjs/app/components/Delegations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './ConfirmationModal'; -export * from './DelegateIconButton'; -export * from './DelegationModal'; -export * from './DelegateModal'; -export * from './ErrorModal'; -export * from './LoadingModal'; -export * from './ModalDivider'; -export * from './ModalListItem'; -export * from './SimpleModal'; -export * from './styles'; diff --git a/explorer-nextjs/app/components/Delegations/styles.ts b/explorer-nextjs/app/components/Delegations/styles.ts deleted file mode 100644 index 9b26551767..0000000000 --- a/explorer-nextjs/app/components/Delegations/styles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material/styles'; - -export const backDropStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { - style: { - left: mode === 'light' ? '0' : '50%', - width: '50%', - }, - }; -}; - -export const modalStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '25%' : '75%' }; -}; - -export const dialogStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '-50%' : '50%' }; -}; diff --git a/explorer-nextjs/app/components/DetailTable.tsx b/explorer-nextjs/app/components/DetailTable.tsx deleted file mode 100644 index b068a5c852..0000000000 --- a/explorer-nextjs/app/components/DetailTable.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import * as React from 'react' -import { - Link, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TableCellProps, -} from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { Box } from '@mui/system' -import { unymToNym } from '@/app/utils/currency' -import { GatewayEnrichedRowType } from './Gateways/Gateways' -import { MixnodeRowType } from './MixNodes' -import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar' -import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; - -export type ColumnsType = { - field: string - title: string - headerAlign?: TableCellProps['align'] - width?: string | number - tooltipInfo?: string -} - -export interface UniversalTableProps<T = any> { - tableName: string - columnsData: ColumnsType[] - rows: T[] -} - -function formatCellValues(val: string | number, field: string) { - if (field === 'identity_key' && typeof val === 'string') { - return ( - <Box display="flex" justifyContent="flex-end"> - <CopyToClipboard - sx={{ mr: 1, mt: 0.5, fontSize: '18px' }} - value={val} - tooltip={`Copy identity key ${val} to clipboard`} - /> - <span>{val}</span> - </Box> - ) - } - - if (field === 'bond') { - return unymToNym(val, 6) - } - - if (field === 'owner') { - return ( - <Link - underline="none" - color="inherit" - target="_blank" - href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`} - > - {val} - </Link> - ) - } - - if (field === 'stake_saturation') { - return <StakeSaturationProgressBar value={Number(val)} threshold={100} /> - } - - return val -} - -export const DetailTable: FCWithChildren<{ - tableName: string - columnsData: ColumnsType[] - rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[] -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { - const theme = useTheme() - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 1080 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, width, tooltipInfo }) => ( - <TableCell - key={field} - sx={{ fontSize: 14, fontWeight: 600, width }} - > - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Box sx={{ display: 'flex', alignItems: 'center' }}> - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={ - theme.palette.nym.networkExplorer.tooltip.color - } - bgColor={ - theme.palette.nym.networkExplorer.tooltip.background - } - maxWidth={230} - arrow - /> - </Box> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((eachRow) => ( - <TableRow - key={eachRow.id} - sx={{ '&:last-child td, &:last-child th': { border: 0 } }} - > - {columnsData?.map((data, index) => ( - <TableCell - key={data.title} - component="th" - scope="row" - variant="body" - sx={{ - padding: 2, - width: 200, - fontSize: 14, - }} - data-testid={`${data.title.replace(/ /g, '-')}-value`} - > - {formatCellValues( - eachRow[columnsData[index].field], - columnsData[index].field - )} - </TableCell> - ))} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/Filters/Filters.tsx b/explorer-nextjs/app/components/Filters/Filters.tsx deleted file mode 100644 index 3a871a5fb4..0000000000 --- a/explorer-nextjs/app/components/Filters/Filters.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client' - -import React, { useState, useEffect, useRef, useCallback } from 'react' -import { - Button, - Dialog, - DialogContent, - DialogActions, - DialogTitle, - Slider, - Typography, - Box, - Snackbar, - Slide, - Alert, -} from '@mui/material' -import { useParams } from 'next/navigation' -import { useMainContext } from '@/app/context/main' -import { - MixnodeStatusWithAll, - toMixnodeStatus, -} from '@/app/typeDefs/explorer-api' -import { EnumFilterKey, TFilterItem, TFilters } from '@/app/typeDefs/filters' -import { Api } from '@/app/api' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { formatOnSave, generateFilterSchema } from './filterSchema' -import FiltersButton from './FiltersButton' - -const FilterItem = ({ - label, - id, - tooltipInfo, - value, - isSmooth, - marks, - scale, - min, - max, - onChange, -}: TFilterItem & { - onChange: (id: EnumFilterKey, newValue: number[]) => void -}) => ( - <Box sx={{ p: 2 }}> - <Typography gutterBottom>{label}</Typography> - <Typography fontSize={12}>{tooltipInfo}</Typography> - <Slider - value={value} - onChange={(e: Event, newValue: number | number[]) => - onChange(id, newValue as number[]) - } - valueLabelDisplay={isSmooth ? 'auto' : 'off'} - marks={marks} - step={isSmooth ? 1 : null} - scale={scale} - min={min} - max={max} - valueLabelFormat={(val: number) => - val === 100 && id === 'stakeSaturation' ? '>100' : val - } - /> - </Box> -) - -export const Filters = () => { - const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext() - const { status } = useParams<{ - status: 'active' | 'standby' | 'inactive' | 'all' - }>() - const isMobile = useIsMobile() - - const [showFilters, setShowFilters] = useState(false) - const [isFiltered, setIsFiltered] = useState(false) - const [filters, setFilters] = React.useState<TFilters>() - const [upperSaturationValue, setUpperSaturationValue] = - React.useState<number>(100) - - const baseFilters = useRef<TFilters>() - const prevFilters = useRef<TFilters>() - - const handleToggleShowFilters = () => setShowFilters(!showFilters) - - const initialiseFilters = useCallback(async () => { - const allMixnodes = await Api.fetchMixnodes() - if (allMixnodes) { - setUpperSaturationValue( - Math.round( - Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1 - ) - ) - const initFilters = generateFilterSchema() - baseFilters.current = initFilters - prevFilters.current = initFilters - setFilters(initFilters) - } - }, []) - - const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { - if (id === 'stakeSaturation' && newValue[1] === 100) { - newValue.splice(1, 1, upperSaturationValue) - } - setFilters((ftrs) => { - if (ftrs) - return { - ...ftrs, - [id]: { - ...ftrs[id], - value: newValue, - }, - } - return undefined - }) - } - - const handleOnSave = async () => { - setShowFilters(false) - await filterMixnodes(formatOnSave(filters!), status) - setIsFiltered(true) - prevFilters.current = filters - } - - const handleOnCancel = () => { - setShowFilters(false) - setFilters(prevFilters.current) - } - - const resetFilters = () => { - setFilters(baseFilters.current) - setIsFiltered(false) - prevFilters.current = baseFilters.current - } - - const onClearFilters = async () => { - await fetchMixnodes(toMixnodeStatus(MixnodeStatusWithAll[status])) - resetFilters() - } - - useEffect(() => { - initialiseFilters() - }, [initialiseFilters]) - - useEffect(() => { - resetFilters() - }, [status]) - - if (!filters) return null - - return ( - <> - <Snackbar - open={isFiltered} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - message="Filters applied" - TransitionComponent={Slide} - transitionDuration={250} - > - <Alert - severity="info" - variant={isMobile ? 'standard' : 'outlined'} - sx={{ color: (t) => t.palette.info.light }} - action={ - <Button size="small" onClick={onClearFilters}> - CLEAR FILTERS - </Button> - } - > - {mixnodes?.data?.length} mixnodes matched your criteria - </Alert> - </Snackbar> - <FiltersButton onClick={handleToggleShowFilters} fullWidth /> - <Dialog - open={showFilters} - onClose={handleToggleShowFilters} - maxWidth="md" - fullWidth - > - <DialogTitle>Mixnode filters</DialogTitle> - <DialogContent dividers> - {Object.values(filters).map((v) => ( - <FilterItem {...v} key={v.id} onChange={handleOnChange} /> - ))} - </DialogContent> - <DialogActions> - <Button size="large" onClick={handleOnCancel}> - Cancel - </Button> - <Button variant="contained" size="large" onClick={handleOnSave}> - Save - </Button> - </DialogActions> - </Dialog> - </> - ) -} diff --git a/explorer-nextjs/app/components/Filters/FiltersButton.tsx b/explorer-nextjs/app/components/Filters/FiltersButton.tsx deleted file mode 100644 index 1e6d3e2940..0000000000 --- a/explorer-nextjs/app/components/Filters/FiltersButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { Tune } from '@mui/icons-material'; - -type FiltersButtonProps = { - iconOnly?: boolean; - fullWidth?: boolean; - onClick: () => void; -}; - -const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => { - if (iconOnly) { - return ( - <IconButton onClick={onClick} color="primary"> - <Tune /> - </IconButton> - ); - } - - return ( - <Button - fullWidth={fullWidth} - size="large" - variant="contained" - endIcon={<Tune />} - onClick={onClick} - sx={{ textTransform: 'none' }} - > - Filters - </Button> - ); -}; - -export default FiltersButton; diff --git a/explorer-nextjs/app/components/Filters/filterSchema.ts b/explorer-nextjs/app/components/Filters/filterSchema.ts deleted file mode 100644 index c5011114b1..0000000000 --- a/explorer-nextjs/app/components/Filters/filterSchema.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; - -export const generateFilterSchema = () => ({ - profitMargin: { - label: 'Profit margin (%)', - id: EnumFilterKey.profitMargin, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: - 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', - }, - stakeSaturation: { - label: 'Stake saturation (%)', - id: EnumFilterKey.stakeSaturation, - value: [0, 100], - isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ - value: value < 100 ? value : 100, - label: value < 100 ? value : '>100', - })), - tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", - }, - routingScore: { - label: 'Routing score (%)', - id: EnumFilterKey.routingScore, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', - }, -}); - -const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { - const lowerValue = value_1 / 100; - const upperValue = value_2 / 100; - - return [lowerValue, upperValue]; -}; - -export const formatOnSave = (filters: TFilters) => ({ - routingScore: filters.routingScore.value, - profitMargin: filters.profitMargin.value, - stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), -}); diff --git a/explorer-nextjs/app/components/Footer.tsx b/explorer-nextjs/app/components/Footer.tsx deleted file mode 100644 index 3e69bd3c42..0000000000 --- a/explorer-nextjs/app/components/Footer.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import Box from '@mui/material/Box' -import MuiLink from '@mui/material/Link' -import Typography from '@mui/material/Typography' -import { useIsMobile } from '../hooks/useIsMobile' -import { NymVpnIcon } from '../icons/NymVpn' -import { Socials } from './Socials' -import Link from 'next/link' - -export const Footer: FCWithChildren = () => { - const isMobile = useIsMobile() - - return ( - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - width: '100%', - height: 'auto', - mt: 3, - pt: 3, - pb: 3, - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - justifyContent: 'center', - alignItems: 'center', - mb: 2, - }} - > - <Box marginRight={1}> - <Link href="http://nymvpn.com" target="_blank"> - <NymVpnIcon /> - </Link> - </Box> - - <Socials isFooter /> - </Box> - - <Typography - sx={{ - fontSize: 12, - textAlign: isMobile ? 'center' : 'end', - color: 'nym.muted.onDarkBg', - }} - > - © {new Date().getFullYear()} Nym Technologies SA, all rights reserved - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Gateways/Gateways.ts b/explorer-nextjs/app/components/Gateways/Gateways.ts deleted file mode 100644 index f4dbf2743c..0000000000 --- a/explorer-nextjs/app/components/Gateways/Gateways.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api'; -import { toPercentInteger } from '@/app/utils'; - -export type GatewayRowType = { - id: string; - owner: string; - identity_key: string; - bond: number; - host: string; - location: string; - version: string; -// node_performance: number; -}; - -export type GatewayEnrichedRowType = GatewayRowType & { - routingScore: string; - avgUptime: string; - clientsPort: number; - mixPort: number; -}; - -export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { - return !arrayOfGateways - ? [] - : arrayOfGateways.map((gw) => ({ - id: gw.owner, - owner: gw.owner, - identity_key: gw.gateway.identity_key || '', - location: gw.location?.country_name.toUpperCase() || '', - bond: gw.pledge_amount.amount || 0, - host: gw.gateway.host || '', - version: gw.gateway.version || '', -// node_performance: toPercentInteger(gw.node_performance.last_24h), - })); -} - -export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType { - return { - id: gateway.owner, - owner: gateway.owner, - identity_key: gateway.gateway.identity_key || '', - location: gateway.location?.country_name.toUpperCase() || '', - bond: gateway.pledge_amount.amount || 0, - host: gateway.gateway.host || '', - version: gateway.gateway.version || '', - clientsPort: gateway.gateway.clients_port || 0, - mixPort: gateway.gateway.mix_port || 0, - routingScore: `${report.most_recent}%`, - avgUptime: `${report.last_day || report.last_hour}%`, -// node_performance: toPercentInteger(gateway.node_performance.most_recent), - }; -} diff --git a/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx b/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx deleted file mode 100644 index 10f230e26a..0000000000 --- a/explorer-nextjs/app/components/Gateways/VersionDisplaySelector.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { FormControl, MenuItem, Select } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export enum VersionSelectOptions { - latestVersion = 'Latest versions', - olderVersions = 'Older versions', - all = 'All', -} -export const VersionDisplaySelector = ({ - selected, - handleChange, -}: { - selected: VersionSelectOptions - handleChange: (option: VersionSelectOptions) => void -}) => { - const isMobile = useIsMobile() - - return ( - <FormControl size="small"> - <Select - value={selected} - onChange={(e) => handleChange(e.target.value as VersionSelectOptions)} - labelId="simple-select-label" - id="simple-select" - sx={{ - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem - value={VersionSelectOptions.latestVersion} - data-testid="show-gateway-latest-version" - > - {VersionSelectOptions.latestVersion} - </MenuItem> - <MenuItem - value={VersionSelectOptions.olderVersions} - data-testid="show-gateway-old-versions" - > - {VersionSelectOptions.olderVersions} - </MenuItem> - <MenuItem - value={VersionSelectOptions.all} - data-testid="show-gateway-all-versions" - > - {VersionSelectOptions.all} - </MenuItem> - </Select> - </FormControl> - ) -} diff --git a/explorer-nextjs/app/components/Icons.ts b/explorer-nextjs/app/components/Icons.ts deleted file mode 100644 index 88761b9d21..0000000000 --- a/explorer-nextjs/app/components/Icons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; -import { MixnodeStatus } from '../typeDefs/explorer-api'; - -export const Icons = { - Mixnodes: { - Status: { - Active: CheckCircleOutlineIcon, - Standby: PauseCircleOutlineIcon, - Inactive: CircleOutlinedIcon, - }, - }, -}; - -export const getMixNodeIcon = (value: any) => { - if (value && typeof value === 'string') { - switch (value) { - case MixnodeStatus.active: - return Icons.Mixnodes.Status.Active; - case MixnodeStatus.standby: - return Icons.Mixnodes.Status.Standby; - default: - return Icons.Mixnodes.Status.Inactive; - } - } - return Icons.Mixnodes.Status.Inactive; -}; diff --git a/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx b/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx deleted file mode 100644 index 58ea4c97b2..0000000000 --- a/explorer-nextjs/app/components/MixNodes/BondBreakdown.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import * as React from 'react' -import { Alert, Box, CircularProgress, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell from '@mui/material/TableCell' -import TableContainer from '@mui/material/TableContainer' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import Paper from '@mui/material/Paper' -import { ExpandMore } from '@mui/icons-material' -import { currencyToString } from '@/app/utils/currency' -import { useMixnodeContext } from '@/app/context/mixnode' -import { useIsMobile } from '@/app/hooks/useIsMobile' - -export const BondBreakdownTable: FCWithChildren = () => { - const { mixNode, delegations, uniqDelegations } = useMixnodeContext() - const [showDelegations, toggleShowDelegations] = - React.useState<boolean>(false) - - const [bonds, setBonds] = React.useState({ - delegations: '0', - pledges: '0', - bondsTotal: '0', - hasLoaded: false, - }) - const theme = useTheme() - const isMobile = useIsMobile() - - React.useEffect(() => { - if (mixNode?.data) { - // delegations - const decimalisedDelegations = currencyToString({ - amount: mixNode.data.total_delegation.amount.toString(), - denom: mixNode.data.total_delegation.denom, - }) - - // pledges - const decimalisedPledges = currencyToString({ - amount: mixNode.data.pledge_amount.amount.toString(), - denom: mixNode.data.pledge_amount.denom, - }) - - // bonds total (del + pledges) - const pledgesSum = Number(mixNode.data.pledge_amount.amount) - const delegationsSum = Number(mixNode.data.total_delegation.amount) - const bondsTotal = currencyToString({ - amount: (pledgesSum + delegationsSum).toString(), - }) - - setBonds({ - delegations: decimalisedDelegations, - pledges: decimalisedPledges, - bondsTotal, - hasLoaded: true, - }) - } - }, [mixNode]) - - const expandDelegations = () => { - if (delegations?.data && delegations.data.length > 0) { - toggleShowDelegations(!showDelegations) - } - } - const calcBondPercentage = (num: number) => { - if (mixNode?.data) { - const rawDelegationAmount = Number(mixNode.data.total_delegation.amount) - const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount) - const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount - return ((num * 100) / rawTotalBondsAmount).toFixed(1) - } - return 0 - } - - if (mixNode?.isLoading || delegations?.isLoading) { - return <CircularProgress /> - } - - if (mixNode?.error) { - return <Alert severity="error">Mixnode not found</Alert> - } - if (delegations?.error) { - return <Alert severity="error">Unable to get delegations for mixnode</Alert> - } - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals"> - <TableBody> - <TableRow sx={isMobile ? { minWidth: '70vw' } : null}> - <TableCell - sx={{ - fontWeight: 400, - width: '150px', - }} - align="left" - > - Stake total - </TableCell> - <TableCell align="left" data-testid="bond-total-amount"> - {bonds.bondsTotal} - </TableCell> - </TableRow> - <TableRow> - <TableCell align="left">Bond</TableCell> - <TableCell align="left" data-testid="pledge-total-amount"> - {bonds.pledges} - </TableCell> - </TableRow> - <TableRow> - <TableCell onClick={expandDelegations} align="left"> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - }} - > - Delegation total {'\u00A0'} - {delegations?.data && delegations?.data?.length > 0 && ( - <ExpandMore /> - )} - </Box> - </TableCell> - <TableCell align="left" data-testid="delegation-total-amount"> - {bonds.delegations} - </TableCell> - </TableRow> - </TableBody> - </Table> - - {showDelegations && ( - <Box - sx={{ - maxHeight: 400, - overflowY: 'scroll', - p: 2, - background: theme.palette.background.paper, - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'baseline', - width: '100%', - p: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - data-testid="delegations-total-amount" - > - <Typography - sx={{ - fontSize: 16, - fontWeight: 600, - }} - > - Delegations   - </Typography> - </Box> - <Table stickyHeader> - <TableHead> - <TableRow> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Delegators - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Amount - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - width: '200px', - }} - align="left" - > - Share of stake - </TableCell> - </TableRow> - </TableHead> - - <TableBody> - {uniqDelegations?.data?.map(({ owner, amount: { amount } }) => ( - <TableRow key={owner}> - <TableCell sx={isMobile ? { width: 190 } : null} align="left"> - {owner} - </TableCell> - <TableCell align="left"> - {currencyToString({ amount: amount.toString() })} - </TableCell> - <TableCell align="left"> - {calcBondPercentage(amount)}% - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Box> - )} - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/DetailSection.tsx b/explorer-nextjs/app/components/MixNodes/DetailSection.tsx deleted file mode 100644 index 94a24f338d..0000000000 --- a/explorer-nextjs/app/components/MixNodes/DetailSection.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react' -import { Box, Button, Grid, Typography, useTheme } from '@mui/material' -import Identicon from 'react-identicons' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MixNodeDescriptionResponse } from '@/app/typeDefs/explorer-api' -import { getMixNodeStatusText, MixNodeStatus } from './Status' -import { MixnodeRowType } from '.' - -interface MixNodeDetailProps { - mixNodeRow: MixnodeRowType - mixnodeDescription: MixNodeDescriptionResponse -} - -export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ - mixNodeRow, - mixnodeDescription, -}) => { - const theme = useTheme() - const palette = [theme.palette.text.primary] - const isMobile = useIsMobile() - const statusText = React.useMemo( - () => getMixNodeStatusText(mixNodeRow.status), - [mixNodeRow.status] - ) - - return ( - <Grid container> - <Grid item xs={12} md={6}> - <Box - display="flex" - flexDirection={isMobile ? 'column' : 'row'} - width="100%" - > - <Box - width={72} - height={72} - sx={{ - minWidth: 72, - minHeight: 72, - borderWidth: 1, - borderColor: theme.palette.text.primary, - borderStyle: 'solid', - borderRadius: '50%', - display: 'grid', - placeItems: 'center', - }} - > - <Identicon - size={43} - string={mixNodeRow.identity_key} - palette={palette} - /> - </Box> - <Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}> - <Typography fontSize={21}>{mixnodeDescription.name}</Typography> - <Typography> - {(mixnodeDescription.description || '').slice(0, 1000)} - </Typography> - <Button - component="a" - variant="text" - sx={{ - mt: isMobile ? 2 : 4, - borderRadius: '30px', - fontWeight: 600, - padding: 0, - }} - href={mixnodeDescription.link} - target="_blank" - > - <Typography - component="span" - textOverflow="ellipsis" - whiteSpace="nowrap" - overflow="hidden" - maxWidth="250px" - > - {mixnodeDescription.link} - </Typography> - </Button> - </Box> - </Box> - </Grid> - <Grid - item - xs={12} - md={6} - display="flex" - justifyContent={isMobile ? 'start' : 'end'} - mt={isMobile ? 3 : undefined} - > - <Box display="flex" flexDirection="column"> - <Typography - fontWeight="600" - alignSelf={isMobile ? 'start' : 'self-end'} - > - Node status: - </Typography> - <Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}> - <MixNodeStatus status={mixNodeRow.status} /> - </Box> - <Typography - mt={1} - alignSelf={isMobile ? 'start' : 'self-end'} - color={theme.palette.text.secondary} - fontSize="smaller" - > - This node is {statusText} in this epoch - </Typography> - </Box> - </Grid> - </Grid> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts b/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts deleted file mode 100644 index b2115c0749..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Columns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ColumnsType } from '../../DetailTable'; - -export const EconomicsInfoColumns: ColumnsType[] = [ - { - field: 'estimatedTotalReward', - title: 'Estimated Total Reward', - width: '15%', - tooltipInfo: - 'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.', - }, - { - field: 'estimatedOperatorReward', - title: 'Estimated Operator Reward', - width: '15%', - tooltipInfo: - "Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.", - }, - { - field: 'selectionChance', - title: 'Active Set Probability', - width: '12.5%', - tooltipInfo: - 'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.', - }, - { - field: 'profitMargin', - title: 'Profit Margin', - width: '12.5%', - tooltipInfo: - 'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.', - }, - { - field: 'operatingCost', - title: 'Operating Cost', - width: '10%', - tooltipInfo: - 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', - }, - { - field: 'nodePerformance', - title: 'Routing Score', - width: '10%', - tooltipInfo: - "Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - tooltipInfo: "Mixnode's average routing score in the last 24 hour", - }, -]; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx b/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx deleted file mode 100644 index aef36113df..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { EconomicsProgress } from './EconomicsProgress'; - -export default { - title: 'Mix Node Detail/Economics/ProgressBar', - component: EconomicsProgress, -} as ComponentMeta<typeof EconomicsProgress>; - -const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />; - -export const Empty = Template.bind({}); -Empty.args = {}; - -export const OverThreshold = Template.bind({}); -OverThreshold.args = { - threshold: 100, - value: 120, -}; - -export const UnderThreshold = Template.bind({}); -UnderThreshold.args = { - threshold: 100, - value: 80, -}; - -export const OnThreshold = Template.bind({}); -OnThreshold.args = { - threshold: 100, - value: 100, -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx deleted file mode 100644 index 24db62d167..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/EconomicsProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/system'; - -const parseToNumber = (value: number | undefined | string) => - typeof value === 'string' ? parseInt(value || '', 10) : value || 0; - -export const EconomicsProgress: FCWithChildren< - LinearProgressProps & { - threshold?: number; - color: string; - } -> = ({ threshold, color, ...props }) => { - const theme = useTheme(); - const { value } = props; - - const valueNumber: number = parseToNumber(value); - const thresholdNumber: number = parseToNumber(threshold); - const percentageToDisplay = Math.min(valueNumber, thresholdNumber); - - return ( - <Box - sx={{ - width: 6 / 10, - color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee, - }} - > - <LinearProgress - {...props} - variant="determinate" - color={color} - value={percentageToDisplay} - sx={{ width: '100%', borderRadius: '5px' }} - /> - </Box> - ); -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx deleted file mode 100644 index c9c82c0438..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { DelegatorsInfoTable } from './Table'; -import { EconomicsInfoColumns } from './Columns'; -import { EconomicsInfoRowWithIndex } from './types'; - -export default { - title: 'Mix Node Detail/Economics', - component: DelegatorsInfoTable, -} as ComponentMeta<typeof DelegatorsInfoTable>; - -const row: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: 'High', - }, - - estimatedOperatorReward: { - value: '80000.123456 NYM', - }, - estimatedTotalReward: { - value: '80000.123456 NYM', - }, - profitMargin: { - value: '10 %', - }, - operatingCost: { - value: '11121 NYM', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Good', - }, -}; - -const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Low', - }, -}; - -const emptyRow: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: '-', - progressBarValue: 0, - }, - - estimatedOperatorReward: { - value: '-', - }, - estimatedTotalReward: { - value: '-', - }, - profitMargin: { - value: '-', - }, - operatingCost: { - value: '-', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />; - -export const Empty = Template.bind({}); -Empty.args = { - rows: [emptyRow], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceHigh = Template.bind({}); -selectionChanceHigh.args = { - rows: [row], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceGood = Template.bind({}); -selectionChanceGood.args = { - rows: [rowGoodProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceLow = Template.bind({}); -selectionChanceLow.args = { - rows: [rowLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts b/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts deleted file mode 100644 index 62b27cf229..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Rows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { currencyToString, unymToNym } from '@/app/utils/currency'; -import { useMixnodeContext } from '@/app/context/mixnode'; -import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '@/app/typeDefs/explorer-api'; -import { toPercentIntegerString } from '@/app/utils'; -import { EconomicsInfoRowWithIndex } from './types'; - -const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => - economicDynamicsStats?.data?.active_set_inclusion_probability || '-'; - -export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { - const { economicDynamicsStats, mixNode } = useMixnodeContext(); - - const estimatedNodeRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '', - }) || '-'; - const estimatedOperatorRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '', - }) || '-'; - const profitMargin = mixNode?.data?.profit_margin_percent - ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) - : '-'; - const avgUptime = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.last_24h) - : '-'; - const nodePerformance = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.most_recent) - : '-'; - - const opCost = mixNode?.data?.operating_cost; - - return { - id: 1, - estimatedTotalReward: { - value: estimatedNodeRewards, - }, - estimatedOperatorReward: { - value: estimatedOperatorRewards, - }, - selectionChance: { - value: selectionChance(economicDynamicsStats), - }, - profitMargin: { - value: profitMargin ? `${profitMargin} %` : '-', - }, - operatingCost: { - value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', - }, - avgUptime: { - value: avgUptime ? `${avgUptime} %` : '-', - }, - nodePerformance: { - value: nodePerformance, - }, - }; -}; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx deleted file mode 100644 index e497a72edd..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/StakeSaturationProgressBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { EconomicsProgress } from './EconomicsProgress' - -export const StakeSaturationProgressBar = ({ - value, - threshold, -}: { - value: number - threshold: number -}) => { - const isTablet = useIsMobile('lg') - const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit' - const textColor = - percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee' - - return ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - flexDirection: isTablet ? 'column' : 'row', - }} - id="field" - color={percentageColor} - > - <Typography - sx={{ - mr: isTablet ? 0 : 1, - mb: isTablet ? 1 : 0, - fontWeight: '600', - fontSize: '12px', - color: textColor, - }} - id="stake-saturation-progress-bar" - > - {value}% - </Typography> - <EconomicsProgress - value={value} - threshold={threshold} - color={percentageColor} - /> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx b/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx deleted file mode 100644 index a70a013a24..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/Table.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react' -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, -} from '@mui/material' -import { Box } from '@mui/system' -import { useTheme } from '@mui/material/styles' -import { Tooltip } from '@nymproject/react/tooltip/Tooltip' -import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types' -import { UniversalTableProps } from '@/app/components/DetailTable' -import { textColour } from '@/app/utils' - -const formatCellValues = (value: EconomicsRowsType, field: string) => ( - <Box sx={{ display: 'flex', alignItems: 'center' }} id="field"> - <Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}> - {value.value} - </Typography> - </Box> -) - -export const DelegatorsInfoTable: FCWithChildren< - UniversalTableProps<EconomicsInfoRowWithIndex> -> = ({ tableName, columnsData, rows }) => { - const theme = useTheme() - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, tooltipInfo, width }) => ( - <TableCell - key={field} - sx={{ fontSize: 14, fontWeight: 600, width }} - > - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={ - theme.palette.nym.networkExplorer.tooltip.color - } - bgColor={ - theme.palette.nym.networkExplorer.tooltip.background - } - maxWidth={230} - arrow - /> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows?.map((eachRow) => ( - <TableRow - key={eachRow.id} - sx={{ '&:last-child td, &:last-child th': { border: 0 } }} - > - {columnsData?.map((_, index: number) => { - const { field } = columnsData[index] - const value: EconomicsRowsType = (eachRow as any)[field] - return ( - <TableCell - key={_.title} - sx={{ - color: textColour(value, field, theme), - }} - data-testid={`${_.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(value, columnsData[index].field)} - </TableCell> - ) - })} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/Economics/index.ts b/explorer-nextjs/app/components/MixNodes/Economics/index.ts deleted file mode 100644 index 6dec752246..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DelegatorsInfoTable } from './Table'; -export { EconomicsInfoColumns } from './Columns'; -export { EconomicsInfoRows } from './Rows'; diff --git a/explorer-nextjs/app/components/MixNodes/Economics/types.ts b/explorer-nextjs/app/components/MixNodes/Economics/types.ts deleted file mode 100644 index 0bc550253f..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Economics/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type EconomicsRowsType = { - progressBarValue?: number; - value: string; -}; - -type TEconomicsInfoProperties = - | 'estimatedTotalReward' - | 'estimatedOperatorReward' - | 'estimatedOperatorReward' - | 'selectionChance' - | 'profitMargin' - | 'avgUptime' - | 'nodePerformance' - | 'operatingCost'; - -export type EconomicsInfoRow = { - [k in TEconomicsInfoProperties]: EconomicsRowsType; -}; - -export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer-nextjs/app/components/MixNodes/Status.tsx b/explorer-nextjs/app/components/MixNodes/Status.tsx deleted file mode 100644 index bc8f1fe660..0000000000 --- a/explorer-nextjs/app/components/MixNodes/Status.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react' -import { Typography } from '@mui/material' -import { getMixNodeIcon } from '@/app/components/Icons' -import { MixnodeStatus } from '@/app/typeDefs/explorer-api' -import { useGetMixNodeStatusColor } from '@/app/hooks/useGetMixnodeStatusColor' - -interface MixNodeStatusProps { - status: MixnodeStatus -} -// TODO: should be done with i18n -export const getMixNodeStatusText = (status: MixnodeStatus) => { - switch (status) { - case MixnodeStatus.active: - return 'active' - case MixnodeStatus.standby: - return 'on standby' - default: - return 'inactive' - } -} - -export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({ - status, -}) => { - const Icon = React.useMemo(() => getMixNodeIcon(status), [status]) - const color = useGetMixNodeStatusColor(status) - - return ( - <Typography color={color} display="flex" alignItems="center"> - <Icon /> - <Typography ml={1} component="span" color="inherit"> - {`${status[0].toUpperCase()}${status.slice(1)}`} - </Typography> - </Typography> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx b/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx deleted file mode 100644 index dc505869ed..0000000000 --- a/explorer-nextjs/app/components/MixNodes/StatusDropdown.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from 'react' -import { MenuItem } from '@mui/material' -import Select from '@mui/material/Select' -import { SelectChangeEvent } from '@mui/material/Select/SelectInput' -import { SxProps } from '@mui/system' -import { - MixnodeStatus, - MixnodeStatusWithAll, -} from '@/app/typeDefs/explorer-api' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MixNodeStatus } from './Status' - -// TODO: replace with i18n -const ALL_NODES = 'All nodes' - -interface MixNodeStatusDropdownProps { - status?: MixnodeStatusWithAll - sx?: SxProps - onSelectionChanged?: (status?: MixnodeStatusWithAll) => void -} - -export const MixNodeStatusDropdown: FCWithChildren< - MixNodeStatusDropdownProps -> = ({ status, onSelectionChanged, sx }) => { - const isMobile = useIsMobile() - const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>( - status || MixnodeStatusWithAll.all - ) - const onChange = React.useCallback( - (event: SelectChangeEvent) => { - setStatusValue(event.target.value as MixnodeStatusWithAll) - if (onSelectionChanged) { - onSelectionChanged(event.target.value as MixnodeStatusWithAll) - } - }, - [onSelectionChanged] - ) - - return ( - <Select - labelId="mixnodeStatusSelect_label" - id="mixnodeStatusSelect" - value={statusValue} - onChange={onChange} - renderValue={(value) => { - switch (value) { - case 'active': - case 'standby': - case 'inactive': - return <MixNodeStatus status={value as unknown as MixnodeStatus} /> - default: - return ALL_NODES - } - }} - sx={{ - width: isMobile ? '50%' : 200, - ...sx, - }} - > - <MenuItem - value={MixnodeStatus.active} - data-testid="mixnodeStatusSelectOption_active" - > - <MixNodeStatus status={MixnodeStatus.active} /> - </MenuItem> - <MenuItem - value={MixnodeStatus.standby} - data-testid="mixnodeStatusSelectOption_standby" - > - <MixNodeStatus status={MixnodeStatus.standby} /> - </MenuItem> - <MenuItem - value={MixnodeStatus.inactive} - data-testid="mixnodeStatusSelectOption_inactive" - > - <MixNodeStatus status={MixnodeStatus.inactive} /> - </MenuItem> - <MenuItem value={'all'} data-testid="mixnodeStatusSelectOption_allNodes"> - {ALL_NODES} - </MenuItem> - </Select> - ) -} diff --git a/explorer-nextjs/app/components/MixNodes/index.ts b/explorer-nextjs/app/components/MixNodes/index.ts deleted file mode 100644 index ccb6c5a8b3..0000000000 --- a/explorer-nextjs/app/components/MixNodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Status'; -export * from './StatusDropdown'; -export * from './mappings'; diff --git a/explorer-nextjs/app/components/MixNodes/mappings.ts b/explorer-nextjs/app/components/MixNodes/mappings.ts deleted file mode 100644 index df8558d39b..0000000000 --- a/explorer-nextjs/app/components/MixNodes/mappings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable camelcase */ -import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; -import { toPercentInteger, toPercentIntegerString } from '@/app/utils'; -import { unymToNym } from '@/app/utils/currency'; - -export type MixnodeRowType = { - mix_id: number; - id: string; - status: MixnodeStatus; - owner: string; - location: string; - identity_key: string; - bond: number; - self_percentage: string; - pledge_amount: number; - host: string; - layer: string; - profit_percentage: number; - avg_uptime: string; - stake_saturation: React.ReactNode; - operating_cost: number; - node_performance: number; - blacklisted: boolean; -}; - -export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { - return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType); -} - -export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType { - const pledge = Number(item.pledge_amount.amount) || 0; - const delegations = Number(item.total_delegation.amount) || 0; - const totalBond = pledge + delegations; - const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0; - const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0; - - return { - mix_id: item.mix_id, - id: item.owner, - status: item.status, - owner: item.owner, - identity_key: item.mix_node.identity_key || '', - bond: totalBond || 0, - location: item?.location?.country_name || '', - self_percentage: selfPercentage, - pledge_amount: pledge, - host: item?.mix_node?.host || '', - layer: item?.layer || '', - profit_percentage: profitPercentage, - avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`, - stake_saturation: Number(uncappedSaturation.toFixed(2)), - operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0, - node_performance: toPercentInteger(item.node_performance.most_recent), - blacklisted: item.blacklisted, - }; -} diff --git a/explorer-nextjs/app/components/Nav/DesktopNav.tsx b/explorer-nextjs/app/components/Nav/DesktopNav.tsx deleted file mode 100644 index e1faab46f1..0000000000 --- a/explorer-nextjs/app/components/Nav/DesktopNav.tsx +++ /dev/null @@ -1,379 +0,0 @@ -'use client' - -import * as React from 'react' -import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material' -import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles' -import { Link as MuiLink } from '@mui/material' -import Button from '@mui/material/Button' -import Box from '@mui/material/Box' -import ListItem from '@mui/material/ListItem' -import MuiDrawer from '@mui/material/Drawer' -import AppBar from '@mui/material/AppBar' -import Toolbar from '@mui/material/Toolbar' -import Typography from '@mui/material/Typography' -import List from '@mui/material/List' -import IconButton from '@mui/material/IconButton' -import ListItemButton from '@mui/material/ListItemButton' -import ListItemIcon from '@mui/material/ListItemIcon' -import ListItemText from '@mui/material/ListItemText' -import { NYM_WEBSITE } from '@/app/api/constants' -import { useMainContext } from '@/app/context/main' -import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose' -import { NavOptionType, originalNavOptions } from '@/app/context/nav' -import { ReleaseAlert } from '@/app/components/ReleaseAlert' -import { DarkLightSwitchDesktop } from '@/app/components/Switch' -import { Footer } from '@/app/components/Footer' -import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet' -import { usePathname, useRouter } from 'next/navigation' -import {SearchToolbar} from "@/app/components/Nav/Search"; - -const drawerWidth = 255 -const bannerHeight = 80 - -const openedMixin = (theme: Theme): CSSObject => ({ - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - overflowX: 'hidden', -}) - -const closedMixin = (theme: Theme): CSSObject => ({ - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: 'hidden', - width: `calc(${theme.spacing(7)} + 1px)`, -}) - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), - height: 64, -})) - -const Drawer = styled(MuiDrawer, { - shouldForwardProp: (prop) => prop !== 'open', -})(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})) - -type ExpandableButtonType = { - title: string - url: string - isActive?: boolean - Icon?: React.ReactNode - nested?: NavOptionType[] - isChild?: boolean - isMobile: boolean - drawIsTempOpen: boolean - drawIsFixed: boolean - isExternalLink?: boolean - openDrawer: () => void - closeDrawer?: () => void - fixDrawerClose?: () => void -} - -export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({ - title, - url, - drawIsTempOpen, - drawIsFixed, - Icon, - nested, - isMobile, - isChild, - isExternalLink, - openDrawer, - closeDrawer, - fixDrawerClose, -}) => { - const { palette } = useTheme() - const pathname = usePathname() - const router = useRouter() - - const handleClick = () => { - if (title === 'Network Components') { - return undefined - } - - if (isExternalLink) { - window.open(url, '_blank') - - return undefined - } - - if (!isExternalLink) { - router.push(url, {}) - } - - if (closeDrawer) { - closeDrawer() - } - } - const selectedStyle = { - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - } - - return ( - <> - <ListItem - disablePadding - disableGutters - sx={{ - borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)', - ...(pathname === url - ? selectedStyle - : { - background: palette.nym.networkExplorer.nav.background, - borderRight: 'none', - }), - }} - > - <ListItemButton - onClick={() => handleClick()} - sx={{ - pt: 2, - pb: 2, - background: isChild - ? palette.nym.networkExplorer.nav.selected.nested - : 'none', - }} - > - <ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon> - <ListItemText - primary={title} - sx={{ - color: palette.nym.networkExplorer.nav.text, - }} - /> - </ListItemButton> - </ListItem> - {nested?.map((each) => ( - <ExpandableButton - url={each.url} - key={each.title} - title={each.title} - openDrawer={openDrawer} - drawIsTempOpen={drawIsTempOpen} - closeDrawer={closeDrawer} - drawIsFixed={drawIsFixed} - fixDrawerClose={fixDrawerClose} - isMobile={isMobile} - isChild - isExternalLink={each.isExternal} - /> - ))} - </> - ) -} - -export const Nav: FCWithChildren = ({ children }) => { - const { environment } = useMainContext() - const [drawerIsOpen, setDrawerToOpen] = React.useState(false) - const [fixedOpen, setFixedOpen] = React.useState(false) - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false) - const theme = useTheme() - - const explorerName = environment - ? `${environment} Explorer` - : 'Mainnet Explorer' - - const switchNetworkText = - environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet' - const switchNetworkLink = - environment === 'mainnet' - ? 'https://sandbox-explorer.nymtech.net' - : 'https://explorer.nymtech.net' - - const fixDrawerOpen = () => { - setFixedOpen(true) - setDrawerToOpen(true) - } - - const fixDrawerClose = () => { - setFixedOpen(false) - setDrawerToOpen(false) - } - - const tempDrawerOpen = () => { - if (!fixedOpen) { - setDrawerToOpen(true) - } - } - - const tempDrawerClose = () => { - if (!fixedOpen) { - setDrawerToOpen(false) - } - } - - return ( - <Box sx={{ display: 'flex' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <Toolbar - disableGutters - sx={{ - display: 'flex', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - ml: 0.5, - }} - > - <IconButton component="a" href={NYM_WEBSITE} target="_blank"> - {/* <NymLogo /> */} - </IconButton> - <Typography - variant="h6" - noWrap - sx={{ - color: theme.palette.nym.networkExplorer.nav.text, - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink - href="/" - underline="none" - color="inherit" - textTransform="capitalize" - > - {explorerName} - </MuiLink> - <Button - size="small" - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ - borderRadius: 2, - textTransform: 'none', - width: 150, - ml: 4, - fontSize: 14, - fontWeight: 600, - }} - > - {switchNetworkText} - </Button> - </Typography> - </Box> - <Box - sx={{ - mr: 2, - alignItems: 'center', - display: 'flex', - }} - > - <Box> - <SearchToolbar/> - </Box> - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - pr: 0, - pl: 2, - justifyContent: 'flex-end', - alignItems: 'center', - }} - > - <Box sx={{ mr: 1 }}> - <ConnectKeplrWallet /> - </Box> - <DarkLightSwitchDesktop defaultChecked /> - </Box> - </Box> - </Toolbar> - </AppBar> - <Drawer - variant="permanent" - open={true} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - borderRadius: 0, - top: openMaintenance ? bannerHeight : 0, - }, - }} - > - <DrawerHeader - sx={{ - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - justifyContent: 'flex-start', - paddingLeft: 0, - display: 'none', - }} - > - <IconButton - onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen} - sx={{ - padding: 1, - ml: 1, - color: theme.palette.nym.networkExplorer.nav.text, - }} - > - {drawerIsOpen ? <MobileDrawerClose /> : <Menu />} - </IconButton> - </DrawerHeader> - - <List - sx={{ pb: 0 }} - onMouseEnter={tempDrawerOpen} - onMouseLeave={tempDrawerClose} - > - {originalNavOptions.map((props) => ( - <ExpandableButton - key={props.url} - closeDrawer={tempDrawerClose} - drawIsTempOpen={drawerIsOpen} - drawIsFixed={fixedOpen} - fixDrawerClose={fixDrawerClose} - openDrawer={tempDrawerOpen} - isMobile={false} - {...props} - /> - ))} - </List> - </Drawer> - <Box - style={{ width: `calc(100% - ${drawerWidth}px` }} - sx={{ py: 5, px: 6, mt: 7 }} - > - <ReleaseAlert /> - {children} - <Footer /> - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Nav/MobileNav.tsx b/explorer-nextjs/app/components/Nav/MobileNav.tsx deleted file mode 100644 index 3d1cfdcbf9..0000000000 --- a/explorer-nextjs/app/components/Nav/MobileNav.tsx +++ /dev/null @@ -1,147 +0,0 @@ -'use client' - -import * as React from 'react' -import { useTheme } from '@mui/material/styles' -import { - AppBar, - Box, - Drawer, - IconButton, - List, - ListItem, - ListItemButton, - ListItemIcon, - Toolbar, -} from '@mui/material' -import { Menu } from '@mui/icons-material' -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { MobileDrawerClose } from '@/app/icons/MobileDrawerClose' -import { Footer } from '../Footer' -import { ExpandableButton } from './DesktopNav' -import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet' -import { NetworkTitle } from '../NetworkTitle' -import { originalNavOptions } from '@/app/context/nav' -import { ReleaseAlert } from '@/app/components/ReleaseAlert' -import {SearchToolbar} from "@/app/components/Nav/Search"; - -export const MobileNav: FCWithChildren = ({ children }) => { - const theme = useTheme() - const [drawerOpen, setDrawerOpen] = React.useState(false) - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false) - const isSmallMobile = useIsMobile(400) - - const toggleDrawer = () => { - setDrawerOpen(!drawerOpen) - } - - const openDrawer = () => { - setDrawerOpen(true) - } - - return ( - <Box sx={{ display: 'flex', flexDirection: 'column' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner - open={openMaintenance} - onClick={() => setOpenMaintenance(false)} - /> - <Toolbar - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }} - > - <IconButton onClick={toggleDrawer}> - <Menu sx={{ color: 'primary.contrastText' }} /> - </IconButton> - {!isSmallMobile && <NetworkTitle />} - </Box> - <Box sx={{ - alignItems: 'center', - display: 'flex', - }}> - <Box mr={0.5}> - <SearchToolbar/> - </Box> - <ConnectKeplrWallet /> - </Box> - </Toolbar> - </AppBar> - <Drawer - anchor="left" - open={drawerOpen} - onClose={toggleDrawer} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - }, - }} - > - <Box role="presentation"> - <List sx={{ pt: 0, pb: 0 }}> - <ListItem - disablePadding - disableGutters - sx={{ - height: 64, - background: theme.palette.nym.networkExplorer.nav.background, - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - }} - > - <ListItemButton - onClick={toggleDrawer} - sx={{ - pt: 2, - pb: 2, - background: theme.palette.nym.networkExplorer.nav.background, - display: 'flex', - justifyContent: 'flex-start', - }} - > - <ListItemIcon> - <MobileDrawerClose /> - </ListItemIcon> - </ListItemButton> - </ListItem> - {originalNavOptions.map((props) => ( - <ExpandableButton - key={props.url} - title={props.title} - openDrawer={openDrawer} - url={props.url} - drawIsTempOpen={false} - drawIsFixed={false} - Icon={props.Icon} - nested={props.nested} - closeDrawer={toggleDrawer} - isMobile - /> - ))} - </List> - </Box> - </Drawer> - - <Box sx={{ width: '100%', p: 4, mt: 7 }}> - <ReleaseAlert /> - {children} - <Footer /> - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Nav/Navbar.tsx b/explorer-nextjs/app/components/Nav/Navbar.tsx deleted file mode 100644 index 15bcd47566..0000000000 --- a/explorer-nextjs/app/components/Nav/Navbar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client' - -import React from 'react' -import { useIsMobile } from '@/app/hooks' -import { MobileNav } from './MobileNav' -import { Nav } from './DesktopNav' - -const Navbar = ({ children }: { children: React.ReactNode }) => { - const isMobile = useIsMobile() - - if (isMobile) { - return <MobileNav>{children}</MobileNav> - } - - return <Nav>{children}</Nav> -} - -export { Navbar } diff --git a/explorer-nextjs/app/components/Nav/Search.tsx b/explorer-nextjs/app/components/Nav/Search.tsx deleted file mode 100644 index 888125b84a..0000000000 --- a/explorer-nextjs/app/components/Nav/Search.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import { styled, alpha } from '@mui/material/styles'; -import AppBar from '@mui/material/AppBar'; -import Box from '@mui/material/Box'; -import Toolbar from '@mui/material/Toolbar'; -import IconButton from '@mui/material/IconButton'; -import Typography from '@mui/material/Typography'; -import InputBase from '@mui/material/InputBase'; -import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; -import {useRouter} from "next/navigation"; - -const Search = styled('div')(({ theme }) => ({ - position: 'relative', - borderRadius: theme.shape.borderRadius, - backgroundColor: alpha(theme.palette.common.white, 0.15), - '&:hover': { - backgroundColor: alpha(theme.palette.common.white, 0.25), - }, - marginLeft: 0, - width: '100%', - [theme.breakpoints.up('sm')]: { - marginLeft: theme.spacing(1), - width: 'auto', - }, -})); - -const SearchIconWrapper = styled('div')(({ theme }) => ({ - padding: theme.spacing(0, 2), - height: '100%', - position: 'absolute', - pointerEvents: 'none', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -})); - -const StyledInputBase = styled(InputBase)(({ theme }) => ({ - color: 'inherit', - width: '100%', - '& .MuiInputBase-input': { - padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon - paddingLeft: `calc(1em + ${theme.spacing(4)})`, - [theme.breakpoints.up('sm')]: { - width: '30ch', - }, - }, -})); - -export const SearchToolbar = () => { - const [search, setSearch] = React.useState<string>(); - const router = useRouter(); - const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => { - e.preventDefault(); - if(search?.trim().length) { - router.push(`/account/${search.trim()}`); - } - } - return ( - <Search> - <SearchIconWrapper> - <SearchIcon /> - </SearchIconWrapper> - <form onSubmit={handleSubmit}> - <StyledInputBase - placeholder="Search for account id…" - inputProps={{ 'aria-label': 'search' }} - onChange={(event: React.ChangeEvent<HTMLInputElement>) => { - setSearch(event.target.value); - }} - /> - </form> - </Search> - ); -} \ No newline at end of file diff --git a/explorer-nextjs/app/components/NetworkTitle.tsx b/explorer-nextjs/app/components/NetworkTitle.tsx deleted file mode 100644 index 0c450007da..0000000000 --- a/explorer-nextjs/app/components/NetworkTitle.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react' -import { Button, Typography, Link as MuiLink } from '@mui/material' -import { useMainContext } from '@/app/context/main' - -type NetworkTitleProps = { - showToggleNetwork?: boolean -} - -const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => { - const { environment } = useMainContext() - - const explorerName = - `${ - environment && environment.charAt(0).toUpperCase() + environment.slice(1) - } Explorer` || 'Mainnet Explorer' - - const switchNetworkText = - environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet' - const switchNetworkLink = - environment === 'mainnet' - ? 'https://sandbox-explorer.nymtech.net' - : 'https://explorer.nymtech.net' - return ( - <Typography - variant="h6" - noWrap - sx={{ - color: 'nym.networkExplorer.nav.text', - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink href="/" underline="none" color="inherit" fontWeight={700}> - {explorerName} - </MuiLink> - - {showToggleNetwork && ( - <Button - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ - textTransform: 'none', - width: 114, - fontSize: '12px', - fontWeight: 600, - ml: 1, - }} - > - {switchNetworkText} - </Button> - )} - </Typography> - ) -} - -export { NetworkTitle } diff --git a/explorer-nextjs/app/components/ReleaseAlert.tsx b/explorer-nextjs/app/components/ReleaseAlert.tsx deleted file mode 100644 index 5d974420f9..0000000000 --- a/explorer-nextjs/app/components/ReleaseAlert.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Alert, Box, Typography } from '@mui/material'; - -export const ReleaseAlert = () => ( - <Alert severity="warning" sx={{ mb: 3, fontSize: 'medium', width: '100%' }}> - <Box> - <Typography>You are now viewing the legacy Nym mixnet explorer. Explorer 2.0 is coming soon.</Typography> - </Box> - </Alert> -); diff --git a/explorer-nextjs/app/components/Socials.tsx b/explorer-nextjs/app/components/Socials.tsx deleted file mode 100644 index 4a4856c8f2..0000000000 --- a/explorer-nextjs/app/components/Socials.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' -import { Box, IconButton } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { TelegramIcon } from '../icons/socials/TelegramIcon' -import { GitHubIcon } from '../icons/socials/GitHubIcon' -import { TwitterIcon } from '../icons/socials/TwitterIcon' -import { DiscordIcon } from '../icons/socials/DiscordIcon' - -// socials -export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram'; -export const TWITTER_LINK = 'https://nymtech.net/go/x'; -export const GITHUB_LINK = 'https://nymtech.net/go/github'; -export const DISCORD_LINK = 'https://nymtech.net/go/discord'; - -export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({ - isFooter = false, -}) => { - const theme = useTheme() - const color = isFooter - ? theme.palette.nym.networkExplorer.footer.socialIcons - : theme.palette.nym.networkExplorer.topNav.socialIcons - return ( - <Box> - <IconButton - component="a" - href={TELEGRAM_LINK} - target="_blank" - data-testid="telegram" - > - <TelegramIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={DISCORD_LINK} - target="_blank" - data-testid="discord" - > - <DiscordIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={TWITTER_LINK} - target="_blank" - data-testid="twitter" - > - <TwitterIcon color={color} size={24} /> - </IconButton> - <IconButton - component="a" - href={GITHUB_LINK} - target="_blank" - data-testid="github" - > - <GitHubIcon color={color} size={24} /> - </IconButton> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/StatsCard.tsx b/explorer-nextjs/app/components/StatsCard.tsx deleted file mode 100644 index 2c87998986..0000000000 --- a/explorer-nextjs/app/components/StatsCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react' -import { Box, Card, CardContent, IconButton, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import EastIcon from '@mui/icons-material/East' - -interface StatsCardProps { - icon: React.ReactNode - title: string - count?: string | number - errorMsg?: Error | string - onClick?: () => void - color?: string -} -export const StatsCard: FCWithChildren<StatsCardProps> = ({ - icon, - title, - count, - onClick, - errorMsg, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <Card onClick={onClick} sx={{ height: '100%' }}> - <CardContent - sx={{ - padding: 1.5, - paddingLeft: 3, - '&:last-child': { - paddingBottom: 1.5, - }, - cursor: 'pointer', - fontSize: 14, - fontWeight: 600, - }} - > - <Box display="flex" alignItems="center" color={color}> - <Box display="flex"> - {icon} - <Typography - ml={3} - mr={0.75} - fontSize="inherit" - fontWeight="inherit" - data-testid={`${title}-amount`} - > - {count === undefined || count === null ? '' : count} - </Typography> - <Typography - mr={1} - fontSize="inherit" - fontWeight="inherit" - data-testid={title} - > - {title} - </Typography> - </Box> - <IconButton color="inherit" sx={{ fontSize: '16px' }}> - <EastIcon fontSize="inherit" /> - </IconButton> - </Box> - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {typeof errorMsg === 'string' - ? errorMsg - : errorMsg.message || 'Oh no! An error occurred'} - </Typography> - )} - </CardContent> - </Card> - ) -} diff --git a/explorer-nextjs/app/components/StyledLink.tsx b/explorer-nextjs/app/components/StyledLink.tsx deleted file mode 100644 index 36dd798180..0000000000 --- a/explorer-nextjs/app/components/StyledLink.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { Link as MuiLink, SxProps, Typography } from '@mui/material' -import Link from 'next/link' - -type StyledLinkProps = { - to: string - children: React.ReactNode - target?: React.HTMLAttributeAnchorTarget - dataTestId?: string - color?: string - sx?: SxProps -} - -const StyledLink = ({ - to, - children, - dataTestId, - target, - color, - sx, -}: StyledLinkProps) => ( - <Link - href={to} - target={target} - data-testid={dataTestId} - style={{ textDecoration: 'none' }} - > - <Typography component="a" sx={{ ...sx }} color={color}> - {children} - </Typography> - </Link> -) - -export default StyledLink diff --git a/explorer-nextjs/app/components/Switch.tsx b/explorer-nextjs/app/components/Switch.tsx deleted file mode 100644 index 840530a327..0000000000 --- a/explorer-nextjs/app/components/Switch.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Switch from '@mui/material/Switch'; -import { Button } from '@mui/material'; -import { useMainContext } from '../context/main'; -import { LightSwitchSVG } from '../icons/LightSwitchSVG'; - -export const DarkLightSwitch = styled(Switch)(({ theme }) => ({ - width: 55, - height: 34, - padding: 7, - '& .MuiSwitch-switchBase': { - margin: 1, - padding: 2, - transform: 'translateX(4px)', - '&.Mui-checked': { - color: '#fff', - transform: 'translateX(22px)', - '& .MuiSwitch-thumb:before': { - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')', - }, - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - }, - }, - }, - '& .MuiSwitch-thumb': { - backgroundColor: theme.palette.nym.networkExplorer.nav.text, - width: 25, - height: 25, - marginTop: '2px', - '&:before': { - content: "''", - position: 'absolute', - width: '100%', - height: '100%', - left: 0, - top: 0, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')', - }, - }, - '& .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - borderRadius: 20 / 2, - }, -})); - -export const DarkLightSwitchMobile: FCWithChildren = () => { - const { toggleMode } = useMainContext(); - return ( - <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}> - <LightSwitchSVG /> - </Button> - ); -}; - -export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => { - const { toggleMode } = useMainContext(); - return ( - <Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button"> - <DarkLightSwitch defaultChecked={defaultChecked} /> - </Button> - ); -}; diff --git a/explorer-nextjs/app/components/TableToolbar.tsx b/explorer-nextjs/app/components/TableToolbar.tsx deleted file mode 100644 index f22b84aca9..0000000000 --- a/explorer-nextjs/app/components/TableToolbar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client' - -import React from 'react' -import { Box, SelectChangeEvent } from '@mui/material' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { Filters } from './Filters/Filters' - -const fieldsHeight = '42.25px' - -type TableToolBarProps = { - childrenBefore?: React.ReactNode - childrenAfter?: React.ReactNode -} - -export const TableToolbar: FCWithChildren<TableToolBarProps> = ({ - childrenBefore, - childrenAfter, -}) => { - const isMobile = useIsMobile() - return ( - <Box - sx={{ - width: '100%', - marginBottom: 2, - display: 'flex', - flexDirection: isMobile ? 'column' : 'row', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: isMobile ? 'column-reverse' : 'row', - alignItems: 'middle', - }} - > - <Box - sx={{ - display: 'flex', - justifyContent: 'space-between', - height: fieldsHeight, - }} - > - {childrenBefore} - </Box> - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'end', - gap: 1, - marginTop: isMobile ? 2 : 0, - }} - > - {childrenAfter} - </Box> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Title.tsx b/explorer-nextjs/app/components/Title.tsx deleted file mode 100644 index 032837d3c8..0000000000 --- a/explorer-nextjs/app/components/Title.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { Typography } from '@mui/material'; - -export const Title: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - variant="h5" - sx={{ - fontWeight: 600, - }} - data-testid={text} - > - {text} - </Typography> -); diff --git a/explorer-nextjs/app/components/Tooltip.tsx b/explorer-nextjs/app/components/Tooltip.tsx deleted file mode 100644 index 0febd9f891..0000000000 --- a/explorer-nextjs/app/components/Tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; - -type ValueType<T> = T[keyof T]; - -type Props = { - text: string; - id: string; - placement?: ValueType<Pick<TooltipProps, 'placement'>>; - tooltipSx?: TooltipComponentsPropsOverrides; - children: React.ReactNode; -}; - -export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( - <MUITooltip - title={text} - id={id} - placement={placement || 'top-start'} - componentsProps={{ - tooltip: { - sx: { - maxWidth: 200, - background: (t) => t.palette.nym.networkExplorer.tooltip.background, - color: (t) => t.palette.nym.networkExplorer.tooltip.color, - '& .MuiTooltip-arrow': { - color: (t) => t.palette.nym.networkExplorer.tooltip.background, - }, - }, - ...tooltipSx, - }, - }} - arrow - > - {children as ReactElement<any, any>} - </MUITooltip> -); diff --git a/explorer-nextjs/app/components/TwoColSmallTable.tsx b/explorer-nextjs/app/components/TwoColSmallTable.tsx deleted file mode 100644 index a211c29bdd..0000000000 --- a/explorer-nextjs/app/components/TwoColSmallTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp'; -import ErrorIcon from '@mui/icons-material/Error'; - -interface TableProps { - title?: string; - icons?: boolean[]; - keys: string[]; - values: number[]; - marginBottom?: boolean; - error?: string; - loading: boolean; -} - -export const TwoColSmallTable: FCWithChildren<TableProps> = ({ - loading, - title, - icons, - keys, - values, - marginBottom, - error, -}) => ( - <> - {title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>} - - <TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}> - <Table aria-label="two col small table"> - <TableBody> - {keys.map((each: string, i: number) => ( - <TableRow key={each}> - {icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>} - <TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}> - {each} - </TableCell> - <TableCell - sx={error ? { opacity: 0.4 } : null} - align="right" - data-testid={`${each.replace(/ /g, '-')}-value`} - > - {values[i]} - </TableCell> - {error && ( - <TableCell align="right" sx={{ opacity: 0.4 }}> - {values[i]} - </TableCell> - )} - {!error && loading && ( - <TableCell align="right"> - <CircularProgress /> - </TableCell> - )} - {error && !icons && ( - <TableCell sx={{ opacity: 0.2 }} align="right"> - <ErrorIcon /> - </TableCell> - )} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> -); - -TwoColSmallTable.defaultProps = { - title: undefined, - icons: undefined, - marginBottom: false, - error: undefined, -}; diff --git a/explorer-nextjs/app/components/Universal-DataGrid.tsx b/explorer-nextjs/app/components/Universal-DataGrid.tsx deleted file mode 100644 index 36e98be9ce..0000000000 --- a/explorer-nextjs/app/components/Universal-DataGrid.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client' - -import * as React from 'react' -import { makeStyles } from '@mui/styles' -import { - DataGrid, - GridColDef, - GridEventListener, - useGridApiContext, -} from '@mui/x-data-grid' -import Pagination from '@mui/material/Pagination' -import { LinearProgress } from '@mui/material' -import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity' - -const useStyles = makeStyles({ - root: { - display: 'flex', - }, -}) - -const CustomPagination = () => { - const apiRef = useGridApiContext() - const classes = useStyles() - console.log(apiRef.current.state) - - return ( - <Pagination - className={classes.root} - sx={{ mt: 2 }} - color="primary" - count={apiRef.current.state.pagination.paginationModel.pageSize} - page={apiRef.current.state.pagination.paginationModel.page + 1} - onChange={(_, value) => apiRef.current.setPage(value - 1)} - /> - ) -} - -type DataGridProps = { - columns: GridColDef[] - pagination?: true | undefined - pageSize?: string | undefined - rows: any - loading?: boolean - initialState?: GridInitialStateCommunity - onRowClick?: GridEventListener<'rowClick'> | undefined -} -export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ - rows, - columns, - loading, - pagination, - pageSize, - initialState, - onRowClick, -}) => { - if (loading) return <LinearProgress /> - - return ( - <DataGrid - onRowClick={onRowClick} - pagination={pagination} - rows={rows} - slots={{ - pagination: CustomPagination, - }} - columns={columns} - autoHeight - hideFooter={!pagination} - initialState={initialState} - style={{ - width: '100%', - border: 'none', - }} - sx={{ - '*::-webkit-scrollbar': { - width: '1em', - }, - '*::-webkit-scrollbar-track': { - background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, - outline: (t) => - `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, - boxShadow: 'auto', - borderRadius: 'auto', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, - borderRadius: '20px', - width: '.4em', - border: (t) => - `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, - shadow: 'auto', - }, - }} - /> - ) -} diff --git a/explorer-nextjs/app/components/UptimeChart.tsx b/explorer-nextjs/app/components/UptimeChart.tsx deleted file mode 100644 index 00a981a3af..0000000000 --- a/explorer-nextjs/app/components/UptimeChart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Chart } from 'react-google-charts'; -import { format } from 'date-fns'; -import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; - -interface ChartProps { - title?: string; - xLabel: string; - yLabel?: string; - uptimeStory: ApiState<UptimeStoryResponse>; - loading: boolean; -} - -type FormattedDateRecord = [string, number]; -type FormattedChartHeadings = string[]; -type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord]; - -export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => { - const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>(); - const theme = useTheme(); - const color = theme.palette.text.primary; - React.useEffect(() => { - if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; - uptimeStory.data.history.forEach((eachDate) => { - const formattedDateUptimeRecord: FormattedDateRecord = [ - format(new Date(eachDate.date), 'MMM dd'), - eachDate.uptime, - ]; - allFormattedChartData.push(formattedDateUptimeRecord); - }); - setFormattedChartData(allFormattedChartData); - } else { - const emptyData: any = [ - ['Date', 'Score'], - ['Jul 27', 10], - ]; - setFormattedChartData(emptyData); - } - }, [uptimeStory]); - - return ( - <> - {title && <Typography>{title}</Typography>} - {loading && <CircularProgress />} - - {!loading && uptimeStory && ( - <Chart - style={{ minHeight: 480 }} - chartType="LineChart" - loader={<p>...</p>} - data={ - uptimeStory.data - ? formattedChartData - : [ - ['Date', 'Routing Score'], - [format(new Date(Date.now()), 'MMM dd'), 0], - ] - } - options={{ - backgroundColor: - theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined, - color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)', - colors: ['#FB7A21'], - legend: { - textStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - - intervals: { style: 'sticks' }, - hAxis: { - // horizontal / date - title: xLabel, - titleTextStyle: { - color, - }, - textStyle: { - color, - // fontSize: 11 - }, - gridlines: { - count: -1, - }, - }, - vAxis: { - // vertical / % Routing Score - viewWindow: { - min: 0, - max: 100, - }, - title: yLabel, - titleTextStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - textStyle: { - color, - fontSize: 11, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - }} - /> - )} - </> - ); -}; - -UptimeChart.defaultProps = { - title: undefined, -}; diff --git a/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx b/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx deleted file mode 100644 index 23114f9056..0000000000 --- a/explorer-nextjs/app/components/Wallet/ConnectKeplrWallet.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import { Button, IconButton, Stack, CircularProgress } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { useWalletContext } from '@/app/context/wallet' -import { WalletAddress, WalletBalance } from '@/app/components/Wallet' - -export const ConnectKeplrWallet = () => { - const { - connectWallet, - disconnectWallet, - isWalletConnected, - isWalletConnecting, - } = useWalletContext() - const isMobile = useIsMobile(1200) - - if (!connectWallet || !disconnectWallet) { - return null - } - - if (isWalletConnected) { - return ( - <Stack direction="row" spacing={1}> - <WalletBalance /> - <WalletAddress /> - <IconButton - size="small" - onClick={async () => { - await disconnectWallet() - }} - > - <CloseIcon fontSize="small" sx={{ color: 'white' }} /> - </IconButton> - </Stack> - ) - } - - return ( - <Button - variant="outlined" - onClick={() => connectWallet()} - disabled={isWalletConnecting} - endIcon={ - isWalletConnecting && <CircularProgress size={14} color="inherit" /> - } - > - Connect {isMobile ? '' : ' Wallet'} - </Button> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/WalletAddress.tsx b/explorer-nextjs/app/components/Wallet/WalletAddress.tsx deleted file mode 100644 index 3e251eb2bb..0000000000 --- a/explorer-nextjs/app/components/Wallet/WalletAddress.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { ElipsSVG } from '@/app/icons/ElipsSVG' -import { trimAddress } from '@/app/utils' -import { useWalletContext } from '@/app/context/wallet' - -export const WalletAddress = () => { - const { address } = useWalletContext() - - const displayAddress = trimAddress(address, 7) - - return ( - <Box display="flex" alignItems="center" gap={0.5}> - <ElipsSVG /> - <Typography variant="body1" fontWeight={600}> - {displayAddress} - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/WalletBalance.tsx b/explorer-nextjs/app/components/Wallet/WalletBalance.tsx deleted file mode 100644 index 7d1c5979bc..0000000000 --- a/explorer-nextjs/app/components/Wallet/WalletBalance.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { Box, Typography } from '@mui/material' -import { useWalletContext } from '@/app/context/wallet' -import { useIsMobile } from '@/app/hooks' -import { TokenSVG } from '@/app/icons/TokenSVG' - -export const WalletBalance = () => { - const { balance } = useWalletContext() - const isMobile = useIsMobile(1200) - - const showBalance = !isMobile && balance.status === 'success' - - if (!showBalance) { - return null - } - - return ( - <Box display="flex" alignItems="center" gap={1}> - <TokenSVG /> - <Typography variant="body1" fontWeight={600}> - {balance.data} NYM - </Typography> - </Box> - ) -} diff --git a/explorer-nextjs/app/components/Wallet/index.ts b/explorer-nextjs/app/components/Wallet/index.ts deleted file mode 100644 index 645c8b4ffb..0000000000 --- a/explorer-nextjs/app/components/Wallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './WalletBalance'; -export * from './WalletAddress'; diff --git a/explorer-nextjs/app/components/WorldMap.tsx b/explorer-nextjs/app/components/WorldMap.tsx deleted file mode 100644 index 9b5c329571..0000000000 --- a/explorer-nextjs/app/components/WorldMap.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client' - -import * as React from 'react' -import { scaleLinear } from 'd3-scale' -import { - ComposableMap, - Geographies, - Geography, - Marker, - ZoomableGroup, -} from 'react-simple-maps' -import ReactTooltip from 'react-tooltip' -import { CircularProgress } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api' -import MAP_TOPOJSON from '../assets/world-110m.json' - -type MapProps = { - userLocation?: [number, number] - countryData?: ApiState<CountryDataResponse> - loading: boolean -} - -export const WorldMap: FCWithChildren<MapProps> = ({ - countryData, - userLocation, - loading, -}) => { - const { palette } = useTheme() - - const colorScale = React.useMemo(() => { - if (countryData?.data) { - const heighestNumberOfNodes = Math.max( - ...Object.values(countryData.data).map((country) => country.nodes) - ) - return scaleLinear<string, string>() - .domain([ - 0, - 1, - heighestNumberOfNodes / 4, - heighestNumberOfNodes / 2, - heighestNumberOfNodes, - ]) - .range(palette.nym.networkExplorer.map.fills) - .unknown(palette.nym.networkExplorer.map.fills[0]) - } - return () => palette.nym.networkExplorer.map.fills[0] - }, [countryData, palette]) - - const [tooltipContent, setTooltipContent] = React.useState<string | null>( - null - ) - - if (loading) { - return <CircularProgress /> - } - - return ( - <> - <ComposableMap - data-tip="" - style={{ - backgroundColor: palette.nym.networkExplorer.background.tertiary, - width: '100%', - height: 'auto', - }} - viewBox="0, 50, 800, 350" - projection="geoMercator" - projectionConfig={{ - scale: userLocation ? 200 : 100, - center: userLocation, - }} - > - <ZoomableGroup> - <Geographies geography={MAP_TOPOJSON}> - {({ geographies }) => - geographies.map((geo) => { - const d = (countryData?.data || {})[geo.properties.ISO_A3] - return ( - <Geography - key={geo.rsmKey} - geography={geo} - fill={colorScale(d?.nodes || 0)} - stroke={palette.nym.networkExplorer.map.stroke} - strokeWidth={0.2} - onMouseEnter={() => { - const { NAME_LONG } = geo.properties - if (!userLocation) { - setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`) - } - }} - onMouseLeave={() => { - setTooltipContent('') - }} - style={{ - hover: - !userLocation && countryData - ? { - fill: palette.nym.highlight, - outline: 'white', - } - : undefined, - }} - /> - ) - }) - } - </Geographies> - - {userLocation && ( - <Marker coordinates={userLocation}> - <g - fill="grey" - stroke="#FF5533" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - transform="translate(-12, -10)" - > - <circle cx="12" cy="10" r="5" /> - </g> - </Marker> - )} - </ZoomableGroup> - </ComposableMap> - <ReactTooltip>{tooltipContent}</ReactTooltip> - </> - ) -} diff --git a/explorer-nextjs/app/components/index.ts b/explorer-nextjs/app/components/index.ts deleted file mode 100644 index 92eeebd010..0000000000 --- a/explorer-nextjs/app/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './CustomColumnHeading'; -export * from './Title'; -export * from './Universal-DataGrid'; -export * from './Tooltip'; -export { default as StyledLink } from './StyledLink'; -export * from './Delegations'; -export * from './MixNodes'; -export * from './TableToolbar'; -export * from './Icons'; diff --git a/explorer-nextjs/app/context/cosmos-kit.tsx b/explorer-nextjs/app/context/cosmos-kit.tsx deleted file mode 100644 index 987d30aa65..0000000000 --- a/explorer-nextjs/app/context/cosmos-kit.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import React from 'react' -import { ChainProvider } from '@cosmos-kit/react' -import { wallets as keplr } from '@cosmos-kit/keplr-extension' -import { assets, chains } from 'chain-registry' -import { Chain, AssetList } from '@chain-registry/types' -import { VALIDATOR_BASE_URL } from '@/app/api/constants' - -const nymSandbox: Chain = { - chain_name: 'sandbox', - chain_id: 'sandbox', - bech32_prefix: 'n', - network_type: 'devnet', - pretty_name: 'Nym Sandbox', - status: 'active', - slip44: 118, - apis: { - rpc: [ - { - address: 'https://rpc.sandbox.nymtech.net', - }, - ], - }, -} - -const nymSandboxAssets = { - chain_name: 'sandbox', - assets: [ - { - name: 'Nym', - base: 'unym', - symbol: 'NYM', - display: 'NYM', - denom_units: [], - }, - ], -} - -const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => { - // Only use the nyx chains - const chainsFixedUp = React.useMemo(() => { - const nyx = chains.find((chain) => chain.chain_id === 'nyx') - - return nyx ? [nymSandbox, nyx] : [nymSandbox] - }, [chains]) - - // Only use the nyx assets - const assetsFixedUp = React.useMemo(() => { - const nyx = assets.find((asset) => asset.chain_name === 'nyx') - - return nyx ? [nymSandboxAssets, nyx] : [nymSandboxAssets] - }, [assets]) as AssetList[] - - return ( - <ChainProvider - chains={chainsFixedUp} - assetLists={assetsFixedUp} - wallets={[...keplr]} - endpointOptions={{ - endpoints: { - nyx: { - rpc: [VALIDATOR_BASE_URL], - }, - }, - }} - > - {children} - </ChainProvider> - ) -} - -export default CosmosKitProvider diff --git a/explorer-nextjs/app/context/delegations.tsx b/explorer-nextjs/app/context/delegations.tsx deleted file mode 100644 index 75a099970c..0000000000 --- a/explorer-nextjs/app/context/delegations.tsx +++ /dev/null @@ -1,252 +0,0 @@ -'use client' - -import React, { - createContext, - useCallback, - useContext, - useMemo, - useState, -} from 'react' -import { - Delegation, - PendingEpochEvent, - PendingEpochEventKind, -} from '@nymproject/contract-clients/Mixnet.types' -import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' -import { useWalletContext } from './wallet' -import { useMainContext } from './main' - -const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] } - -export type PendingEvent = ReturnType<typeof getEventsByAddress> - -export type DelegationWithRewards = Delegation & { - rewards: string - identityKey: string - pending: PendingEvent -} - -const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => { - if ('delegate' in kind && kind.delegate.owner === address) { - return { - kind: 'delegate' as const, - mixId: kind.delegate.mix_id, - amount: kind.delegate.amount, - } - } - - if ('undelegate' in kind && kind.undelegate.owner === address) { - return { - kind: 'undelegate' as const, - mixId: kind.undelegate.mix_id, - } - } - - return undefined -} - -interface DelegationsState { - delegations?: DelegationWithRewards[] - handleGetDelegations: () => Promise<void> - handleDelegate: ( - mixId: number, - amount: string - ) => Promise<ExecuteResult | undefined> - handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined> -} - -export const DelegationsContext = createContext<DelegationsState>({ - delegations: undefined, - handleGetDelegations: async () => { - throw new Error('Please connect your wallet') - }, - handleDelegate: async () => { - throw new Error('Please connect your wallet') - }, - handleUndelegate: async () => { - throw new Error('Please connect your wallet') - }, -}) - -export const DelegationsProvider = ({ - children, -}: { - children: React.ReactNode -}) => { - const [delegations, setDelegations] = useState<DelegationWithRewards[]>() - const { address, nymQueryClient, nymClient } = useWalletContext() - const { fetchMixnodes } = useMainContext() - - const handleGetPendingEvents = async () => { - if (!nymQueryClient) { - return undefined - } - - if (!address) { - return undefined - } - - const response = await nymQueryClient.getPendingEpochEvents({}) - const pendingEvents: PendingEvent[] = [] - - response.events.forEach((e: PendingEpochEvent) => { - const event = getEventsByAddress(e.event.kind, address) - if (event) { - pendingEvents.push(event) - } - }) - - return pendingEvents - } - - const handleGetDelegationRewards = async (mixId: number) => { - if (!nymQueryClient) { - return undefined - } - - if (!address) { - return undefined - } - - const response = await nymQueryClient.getPendingDelegatorReward({ - address, - mixId, - }) - - return response - } - - const handleGetDelegations = useCallback(async () => { - if (!nymQueryClient) { - setDelegations(undefined) - return undefined - } - - if (!address) { - setDelegations(undefined) - return undefined - } - - // Get all mixnodes - Required to get the identity key for each delegation - const mixnodes = await fetchMixnodes() - - // Get delegations - const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ - delegator: address, - }) - - // Get rewards for each delegation - const rewardsResponse = await Promise.all( - delegationsResponse.delegations.map((d: Delegation) => - handleGetDelegationRewards(d.mix_id) - ) - ) - - // Get all pending events - const pendingEvents = await handleGetPendingEvents() - - const delegationsWithRewards: DelegationWithRewards[] = [] - - // Merge delegations with rewards and pending events - delegationsResponse.delegations.forEach((d: Delegation, index: number) => { - delegationsWithRewards.push({ - ...d, - pending: pendingEvents?.find((e: PendingEvent) => - e?.mixId === d.mix_id ? e.kind : undefined - ), - identityKey: - mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || - '', - rewards: rewardsResponse[index]?.amount_earned_detailed || '0', - }) - }) - - // Add pending events that are not in the delegations list - pendingEvents?.forEach((e) => { - if ( - e && - !delegationsWithRewards.find( - (d: DelegationWithRewards) => d.mix_id === e.mixId - ) - ) { - delegationsWithRewards.push({ - mix_id: e.mixId, - height: 0, - cumulative_reward_ratio: '0', - owner: address, - amount: { - amount: '0', - denom: 'unym', - }, - rewards: '0', - identityKey: - mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node - .identity_key || '', - pending: e, - }) - } - }) - - setDelegations(delegationsWithRewards) - - return undefined - }, [address, nymQueryClient]) - - const handleDelegate = async (mixId: number, amount: string) => { - if (!address) { - throw new Error('Please connect your wallet') - } - - const amountToDelegate = (Number(amount) * 1000000).toString() - const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }] - try { - const tx = await nymClient?.delegateToMixnode( - { mixId }, - fee, - 'Delegation from Nym Explorer', - uNymFunds - ) - - return tx as unknown as ExecuteResult - } catch (e) { - console.error('Failed to delegate to mixnode', e) - throw e - } - } - - const handleUndelegate = async (mixId: number) => { - const tx = await nymClient?.undelegateFromMixnode( - { mixId }, - fee, - 'Undelegation from Nym Explorer' - ) - - return tx as unknown as ExecuteResult - } - - const contextValue: DelegationsState = useMemo( - () => ({ - delegations, - handleGetDelegations, - handleDelegate, - handleUndelegate, - }), - [delegations, handleGetDelegations] - ) - - return ( - <DelegationsContext.Provider value={contextValue}> - {children} - </DelegationsContext.Provider> - ) -} - -export const useDelegationsContext = () => { - const context = useContext(DelegationsContext) - if (!context) { - throw new Error( - 'useDelegationsContext must be used within a DelegationsProvider' - ) - } - return context -} diff --git a/explorer-nextjs/app/context/gateway.tsx b/explorer-nextjs/app/context/gateway.tsx deleted file mode 100644 index 458a9da907..0000000000 --- a/explorer-nextjs/app/context/gateway.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - GatewayReportResponse, - UptimeStoryResponse, -} from '@/app/typeDefs/explorer-api' -import { Api } from '@/app/api' -import { useApiState } from './hooks' - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface GatewayState { - uptimeReport?: ApiState<GatewayReportResponse> - uptimeStory?: ApiState<UptimeStoryResponse> -} - -export const GatewayContext = React.createContext<GatewayState>({}) - -export const useGatewayContext = (): React.ContextType<typeof GatewayContext> => - React.useContext<GatewayState>(GatewayContext) - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const GatewayContextProvider = ({ - gatewayIdentityKey, - children, -}: { - gatewayIdentityKey: string - children: JSX.Element -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = - useApiState<GatewayReportResponse>( - gatewayIdentityKey, - Api.fetchGatewayReportById, - 'Failed to fetch gateway uptime report by id' - ) - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - gatewayIdentityKey, - Api.fetchGatewayUptimeStoryById, - 'Failed to fetch gateway uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById() - clearUptimeHistory() - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) - }, [gatewayIdentityKey]) - - const state = React.useMemo<GatewayState>( - () => ({ - uptimeReport, - uptimeStory, - }), - [uptimeReport, uptimeStory] - ) - - return ( - <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/hooks.ts b/explorer-nextjs/app/context/hooks.ts deleted file mode 100644 index 55004f89ee..0000000000 --- a/explorer-nextjs/app/context/hooks.ts +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -import * as React from 'react'; -import { ApiState } from '@/app/typeDefs/explorer-api'; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -export const useApiState = <T>( - id: string, - fn: (argId: string) => Promise<T>, - errorMessage: string, -): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => { - // stores the state - const [value, setValue] = React.useState<ApiState<T>>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - setValue({ isLoading: true }); - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState<T> = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState<T> = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn, id, errorMessage]); - return [value, wrappedFetchFn, clearValueFn]; -}; diff --git a/explorer-nextjs/app/context/main.tsx b/explorer-nextjs/app/context/main.tsx deleted file mode 100644 index 0494b87749..0000000000 --- a/explorer-nextjs/app/context/main.tsx +++ /dev/null @@ -1,297 +0,0 @@ -'use client' - -import * as React from 'react' -import { PaletteMode } from '@mui/material' -import { - ApiState, - BlockResponse, - CountryDataResponse, - GatewayResponse, - MixNodeResponse, - MixnodeStatus, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - DirectoryServiceProvider, -} from '@/app/typeDefs/explorer-api' -import { EnumFilterKey } from '@/app/typeDefs/filters' -import { Api, getEnvironment } from '@/app/api' -import { toPercentIntegerString } from '@/app/utils' -import { NavOptionType, originalNavOptions } from './nav' - -interface StateData { - summaryOverview?: ApiState<SummaryOverviewResponse> - block?: ApiState<BlockResponse> - countryData?: ApiState<CountryDataResponse> - gateways?: ApiState<GatewayResponse> - globalError?: string | undefined - mixnodes?: ApiState<MixNodeResponse> - nodes?: ApiState<any> - mode: PaletteMode - validators?: ApiState<ValidatorsResponse> - environment?: Environment - serviceProviders?: ApiState<DirectoryServiceProvider[]> -} - -interface StateApi { - fetchMixnodes: ( - status?: MixnodeStatus - ) => Promise<MixNodeResponse | undefined> - filterMixnodes: (filters: any, status: any) => void - fetchNodes: () => Promise<any> - fetchNodeById: (id: number) => Promise<any> - fetchAccountById: (accountAddr: string) => Promise<any> - toggleMode: () => void -} - -type State = StateData & StateApi - -export const MainContext = React.createContext<State>({ - mode: 'dark', - toggleMode: () => undefined, - filterMixnodes: () => null, - fetchMixnodes: () => Promise.resolve(undefined), - fetchNodes: async () => undefined, - fetchNodeById: async () => undefined, - fetchAccountById: async () => undefined, -}) - -export const useMainContext = (): React.ContextType<typeof MainContext> => - React.useContext<State>(MainContext) - -export const MainContextProvider: FCWithChildren = ({ children }) => { - // network explorer environment - const [environment, setEnvironment] = React.useState<Environment>() - - // light/dark mode - const [mode, setMode] = React.useState<PaletteMode>('dark') - - // global / banner error messaging - const [globalError] = React.useState<string>() - - // various APIs for Overview page - const [summaryOverview, setSummaryOverview] = - React.useState<ApiState<SummaryOverviewResponse>>() - const [nodes, setNodes] = React.useState<ApiState<any>>() - const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>() - const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>() - const [validators, setValidators] = - React.useState<ApiState<ValidatorsResponse>>() - const [block, setBlock] = React.useState<ApiState<BlockResponse>>() - const [countryData, setCountryData] = - React.useState<ApiState<CountryDataResponse>>() - const [serviceProviders, setServiceProviders] = - React.useState<ApiState<DirectoryServiceProvider[]>>() - - const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark')) - - const fetchOverviewSummary = async () => { - try { - const data = await Api.fetchOverviewSummary() - setSummaryOverview({ data, isLoading: false }) - } catch (error) { - setSummaryOverview({ - error: - error instanceof Error - ? error - : new Error('Overview summary api fail'), - isLoading: false, - }) - } - } - - const fetchMixnodes = async () => { - let data - setMixnodes((d) => ({ ...d, isLoading: true })) - try { - data = await Api.fetchMixnodes() - setMixnodes({ data, isLoading: false }) - } catch (error) { - setMixnodes({ - error: error instanceof Error ? error : new Error('Mixnode api fail'), - isLoading: false, - }) - } - return data - } - - const filterMixnodes = async ( - filters: { [key in EnumFilterKey]: number[] }, - status?: MixnodeStatus - ) => { - setMixnodes((d) => ({ ...d, isLoading: true })) - const mxns = await Api.fetchMixnodes() - - const filtered = mxns?.filter( - (m) => - +m.profit_margin_percent >= filters.profitMargin[0] / 100 && - +m.profit_margin_percent <= filters.profitMargin[1] / 100 && - m.stake_saturation >= filters.stakeSaturation[0] && - m.stake_saturation <= filters.stakeSaturation[1] && - m.avg_uptime >= filters.routingScore[0] && - m.avg_uptime <= filters.routingScore[1] - ) - - setMixnodes({ data: filtered, isLoading: false }) - } - - const fetchGateways = async () => { - setGateways((d) => ({ ...d, isLoading: true })) - try { - const data = await Api.fetchGateways() - setGateways({ data, isLoading: false }) - } catch (error) { - setGateways({ - error: error instanceof Error ? error : new Error('Gateways api fail'), - isLoading: false, - }) - } - } - const fetchValidators = async () => { - try { - const data = await Api.fetchValidators() - setValidators({ data, isLoading: false }) - } catch (error) { - setValidators({ - error: - error instanceof Error ? error : new Error('Validators api fail'), - isLoading: false, - }) - } - } - const fetchBlock = async () => { - try { - const data = await Api.fetchBlock() - setBlock({ data, isLoading: false }) - } catch (error) { - setBlock({ - error: error instanceof Error ? error : new Error('Block api fail'), - isLoading: false, - }) - } - } - const fetchCountryData = async () => { - setCountryData({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchCountryData() - setCountryData({ data: res, isLoading: false }) - } catch (error) { - setCountryData({ - error: - error instanceof Error ? error : new Error('Country Data api fail'), - isLoading: false, - }) - } - } - - const fetchServiceProviders = async () => { - setServiceProviders({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchServiceProviders() - const resWithRoutingScorePercentage = res.map((item) => ({ - ...item, - routing_score: item.routing_score - ? `${toPercentIntegerString(item.routing_score.toString())}%` - : item.routing_score, - })) - setServiceProviders({ - data: resWithRoutingScorePercentage, - isLoading: false, - }) - } catch (error) { - setServiceProviders({ - error: - error instanceof Error - ? error - : new Error('Service provider api fail'), - isLoading: false, - }) - } - } - - const fetchNodes = async () => { - setNodes({ data: undefined, isLoading: true }) - try { - const res = await Api.fetchNodes(); - res.forEach((node: any) => node.total_stake = - Math.round(Number.parseFloat(node.rewarding_details?.operator || "0") - + Number.parseFloat(node.rewarding_details?.delegates || "0")) - ); - setNodes({ - data: res.sort((a: any, b: any) => b.total_stake - a.total_stake), - isLoading: false, - }) - } catch (error) { - setNodes({ - error: - error instanceof Error - ? error - : new Error('Service provider api fail'), - isLoading: false, - }) - } }; - - const fetchNodeById = async (id: number) => { - const res = await Api.fetchNodeById(id); - return res; - }; - - const fetchAccountById = async (id: string) => { - const res = await Api.fetchAccountById(id); - return res; - }; - - React.useEffect(() => { - if (environment === 'mainnet') { - fetchServiceProviders() - } - }, [environment]) - - React.useEffect(() => { - setEnvironment(getEnvironment()) - Promise.all([ - fetchOverviewSummary(), - fetchGateways(), - fetchValidators(), - fetchBlock(), - fetchCountryData(), - ]) - }, []) - - const state = React.useMemo<State>( - () => ({ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - nodes, - summaryOverview, - validators, - serviceProviders, - toggleMode, - fetchMixnodes, - filterMixnodes, - fetchNodes, - fetchNodeById, - fetchAccountById, - }), - [ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - nodes, - summaryOverview, - validators, - serviceProviders, - ] - ) - - return <MainContext.Provider value={state}>{children}</MainContext.Provider> -} diff --git a/explorer-nextjs/app/context/mixnode.tsx b/explorer-nextjs/app/context/mixnode.tsx deleted file mode 100644 index 71b48fd045..0000000000 --- a/explorer-nextjs/app/context/mixnode.tsx +++ /dev/null @@ -1,173 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - DelegationsResponse, - UniqDelegationsResponse, - MixNodeDescriptionResponse, - MixNodeEconomicDynamicsStatsResponse, - MixNodeResponseItem, - StatsResponse, - StatusResponse, - UptimeStoryResponse, -} from '../typeDefs/explorer-api' -import { Api } from '../api' -import { useApiState } from './hooks' -import { - mixNodeResponseItemToMixnodeRowType, - MixnodeRowType, -} from '../components/MixNodes' - -/** - * This context provides the state for a single mixnode by identity key. - */ - -interface MixnodeState { - delegations?: ApiState<DelegationsResponse> - uniqDelegations?: ApiState<UniqDelegationsResponse> - description?: ApiState<MixNodeDescriptionResponse> - economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse> - mixNode?: ApiState<MixNodeResponseItem | undefined> - mixNodeRow?: MixnodeRowType - stats?: ApiState<StatsResponse> - status?: ApiState<StatusResponse> - uptimeStory?: ApiState<UptimeStoryResponse> -} - -export const MixnodeContext = React.createContext<MixnodeState>({}) - -export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> => - React.useContext<MixnodeState>(MixnodeContext) - -interface MixnodeContextProviderProps { - mixId: string - children: React.ReactNode -} - -/** - * Provides a state context for a mixnode by identity - * @param mixId The mixID of the mixnode - */ -export const MixnodeContextProvider: FCWithChildren< - MixnodeContextProviderProps -> = ({ mixId, children }) => { - const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState< - MixNodeResponseItem | undefined - >(mixId, Api.fetchMixnodeByID, 'Failed to fetch mixnode by id') - - const [mixNodeRow, setMixnodeRow] = React.useState< - MixnodeRowType | undefined - >() - - const [delegations, fetchDelegations, clearDelegations] = - useApiState<DelegationsResponse>( - mixId, - Api.fetchDelegationsById, - 'Failed to fetch delegations for mixnode' - ) - - const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = - useApiState<UniqDelegationsResponse>( - mixId, - Api.fetchUniqDelegationsById, - 'Failed to fetch delegations for mixnode' - ) - - const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>( - mixId, - Api.fetchStatusById, - 'Failed to fetch mixnode status' - ) - - const [stats, fetchStats, clearStats] = useApiState<StatsResponse>( - mixId, - Api.fetchStatsById, - 'Failed to fetch mixnode stats' - ) - - const [description, fetchDescription, clearDescription] = - useApiState<MixNodeDescriptionResponse>( - mixId, - Api.fetchMixnodeDescriptionById, - 'Failed to fetch mixnode description' - ) - - const [ - economicDynamicsStats, - fetchEconomicDynamicsStats, - clearEconomicDynamicsStats, - ] = useApiState<MixNodeEconomicDynamicsStatsResponse>( - mixId, - Api.fetchMixnodeEconomicDynamicsStatsById, - 'Failed to fetch mixnode dynamics stats by id' - ) - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - mixId, - Api.fetchUptimeStoryById, - 'Failed to fetch mixnode uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearMixnodeById() - clearDelegations() - clearUniqDelegations() - clearStatus() - clearStats() - clearDescription() - clearEconomicDynamicsStats() - clearUptimeHistory() - - // fetch the mixnode, then get all the other stuff - fetchMixnodeById().then((value) => { - if (!value.data || value.error) { - setMixnodeRow(undefined) - return - } - setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data)) - Promise.all([ - fetchDelegations(), - fetchUniqDelegations(), - fetchStatus(), - fetchStats(), - fetchDescription(), - fetchEconomicDynamicsStats(), - fetchUptimeHistory(), - ]) - }) - }, [mixId]) - - const state = React.useMemo<MixnodeState>( - () => ({ - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }), - [ - { - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }, - ] - ) - - return ( - <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/nav.tsx b/explorer-nextjs/app/context/nav.tsx deleted file mode 100644 index e41882edbb..0000000000 --- a/explorer-nextjs/app/context/nav.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client' - -import * as React from 'react' -import { DelegateIcon } from '@/app/icons/DelevateSVG' -import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' -import { OverviewSVG } from '@/app/icons/OverviewSVG' -import { NodemapSVG } from '@/app/icons/NodemapSVG' -import { NetworkComponentsSVG } from '@/app/icons/NetworksSVG' - -export type NavOptionType = { - url: string - title: string - Icon?: React.ReactNode - nested?: NavOptionType[] - isExpandedChild?: boolean - isExternal?: boolean -} - -export const originalNavOptions: NavOptionType[] = [ - { - url: '/', - title: 'Overview', - Icon: <OverviewSVG />, - }, - { - url: '/network-components', - title: 'Network Components', - Icon: <NetworkComponentsSVG />, - nested: [ - { - url: '/network-components/nodes', - title: 'Nodes', - }, - { - url: '/network-components/mixnodes', - title: 'Mixnodes (legacy)', - }, - { - url: '/network-components/gateways', - title: 'Gateways (legacy)', - }, - { - url: `${BLOCK_EXPLORER_BASE_URL}/validators`, - title: 'Validators', - isExternal: true, - }, - ], - }, - { - url: '/nodemap', - title: 'Nodemap', - Icon: <NodemapSVG />, - }, - { - url: '/delegations', - title: 'Delegations', - Icon: <DelegateIcon sx={{ color: 'white' }} />, - }, -] diff --git a/explorer-nextjs/app/context/node.tsx b/explorer-nextjs/app/context/node.tsx deleted file mode 100644 index 2aa885439c..0000000000 --- a/explorer-nextjs/app/context/node.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import * as React from 'react' -import { - ApiState, - NymNodeReportResponse, - UptimeStoryResponse, -} from '@/app/typeDefs/explorer-api' -import { Api } from '@/app/api' -import { useApiState } from './hooks' - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface NymNodeState { - uptimeReport?: ApiState<NymNodeReportResponse> - uptimeHistory?: ApiState<UptimeStoryResponse> -} - -export const NymNodeContext = React.createContext<NymNodeState>({}) - -export const useNymNodeContext = (): React.ContextType<typeof NymNodeContext> => - React.useContext<NymNodeState>(NymNodeContext) - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const NymNodeContextProvider = ({ - nymNodeId, - children, -}: { - nymNodeId: string - children: JSX.Element -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = - useApiState<any>( - nymNodeId, - Api.fetchNymNodePerformanceById, - 'Failed to fetch gateway uptime report by id' - ) - - const [uptimeHistory, fetchUptimeHistory, clearUptimeHistory] = - useApiState<UptimeStoryResponse>( - nymNodeId, - async (arg) => { - const res = await Api.fetchNymNodeUptimeHistoryById(arg); - const uptimeHistory: UptimeStoryResponse = { - history: res.history.data, - identity: '', - owner: '', - } - return uptimeHistory; - }, - 'Failed to fetch gateway uptime history' - ) - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById() - clearUptimeHistory() - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) - }, [nymNodeId]) - - const state = React.useMemo<NymNodeState>( - () => ({ - uptimeReport, - uptimeHistory, - }), - [uptimeReport, uptimeHistory] - ) - - return ( - <NymNodeContext.Provider value={state}>{children}</NymNodeContext.Provider> - ) -} diff --git a/explorer-nextjs/app/context/wallet.tsx b/explorer-nextjs/app/context/wallet.tsx deleted file mode 100644 index c9acb6b850..0000000000 --- a/explorer-nextjs/app/context/wallet.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client' - -import React, { - createContext, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { useChain } from '@cosmos-kit/react' -import { Wallet } from '@cosmos-kit/core' -import { unymToNym } from '@/app/utils/currency' -import { useNymClient } from '@/app/hooks' -import { - MixnetClient, - MixnetQueryClient, -} from '@nymproject/contract-clients/Mixnet.client' -import { COSMOS_KIT_USE_CHAIN } from '@/app/api/constants' - -interface WalletState { - balance: { status: 'loading' | 'success'; data?: string } - address?: string - isWalletConnected: boolean - isWalletConnecting: boolean - wallet?: Wallet - nymClient?: MixnetClient - nymQueryClient?: MixnetQueryClient - connectWallet: () => Promise<void> - disconnectWallet: () => Promise<void> -} - -export const WalletContext = createContext<WalletState>({ - address: undefined, - balance: { status: 'loading', data: undefined }, - isWalletConnected: false, - isWalletConnecting: false, - nymClient: undefined, - nymQueryClient: undefined, - connectWallet: async () => { - throw new Error('Please connect your wallet') - }, - disconnectWallet: async () => { - throw new Error('Please connect your wallet') - }, -}) - -export const WalletProvider = ({ children }: { children: React.ReactNode }) => { - const [balance, setBalance] = useState<WalletState['balance']>({ - status: 'loading', - data: undefined, - }) - - const { - connect, - disconnect, - wallet, - address, - isWalletConnected, - isWalletConnecting, - getCosmWasmClient, - } = useChain(COSMOS_KIT_USE_CHAIN) - - const { nymClient, nymQueryClient } = useNymClient(address) - - const getBalance = async (walletAddress: string) => { - const account = await getCosmWasmClient() - const uNYMBalance = await account.getBalance(walletAddress, 'unym') - const NYMBalance = unymToNym(uNYMBalance.amount) - - return NYMBalance - } - - const init = async (walletAddress: string) => { - const walletBalance = await getBalance(walletAddress) - setBalance({ status: 'success', data: walletBalance }) - } - - useEffect(() => { - if (isWalletConnected && address) { - init(address) - } - }, [address, isWalletConnected]) - - const handleConnectWallet = async () => { - await connect() - } - - const handleDisconnectWallet = async () => { - await disconnect() - setBalance({ status: 'loading', data: undefined }) - } - - const contextValue: WalletState = useMemo( - () => ({ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - connectWallet: handleConnectWallet, - disconnectWallet: handleDisconnectWallet, - }), - [ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - ] - ) - - return ( - <WalletContext.Provider value={contextValue}> - {children} - </WalletContext.Provider> - ) -} - -export const useWalletContext = () => useContext(WalletContext) diff --git a/explorer-nextjs/app/delegations/page.tsx b/explorer-nextjs/app/delegations/page.tsx deleted file mode 100644 index 60b27b3c91..0000000000 --- a/explorer-nextjs/app/delegations/page.tsx +++ /dev/null @@ -1,311 +0,0 @@ -'use client' - -import React, { useCallback, useEffect, useMemo } from 'react' -import { - Alert, - AlertTitle, - Box, - Button, - Card, - Chip, - IconButton, - Tooltip, - Typography, -} from '@mui/material' -import { DelegationModal, DelegationModalProps, Title } from '@/app/components' -import { useWalletContext } from '@/app/context/wallet' -import { unymToNym } from '@/app/utils/currency' -import { - DelegationWithRewards, - DelegationsProvider, - PendingEvent, - useDelegationsContext, -} from '@/app/context/delegations' -import { urls } from '@/app/utils' -import { useClipboard } from 'use-clipboard-copy' -import { Close } from '@mui/icons-material' -import { useRouter } from 'next/navigation' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' - -const mapToDelegationsRow = ( - delegation: DelegationWithRewards, - index: number -) => ({ - identity: delegation.identityKey, - mix_id: delegation.mix_id, - amount: `${unymToNym(delegation.amount.amount)} NYM`, - rewards: `${unymToNym(delegation.rewards)} NYM`, - id: index, - pending: delegation.pending, -}) - -const Banner = ({ onClose }: { onClose: () => void }) => { - const { copy } = useClipboard() - - return ( - <Alert - severity="info" - sx={{ mb: 3, fontSize: 'medium', width: '100%' }} - action={ - <IconButton size="small" onClick={onClose}> - <Close fontSize="small" /> - </IconButton> - } - > - <AlertTitle> Mobile Delegations Beta</AlertTitle> - <Box> - <Typography> - This is a beta release for mobile delegations If you have any feedback - or feature suggestions contact us at support@nymte.ch - <Button - size="small" - onClick={() => copy('support@nymte.ch')} - sx={{ display: 'inline-block' }} - > - Copy - </Button> - </Typography> - </Box> - </Alert> - ) -} - -const DelegationsPage = () => { - const [confirmationModalProps, setConfirmationModalProps] = React.useState< - DelegationModalProps | undefined - >() - const [isLoading, setIsLoading] = React.useState(false) - const [showBanner, setShowBanner] = React.useState(true) - - const { isWalletConnected } = useWalletContext() - const { handleGetDelegations, handleUndelegate, delegations } = - useDelegationsContext() - - const router = useRouter() - - useEffect(() => { - let timeoutId: NodeJS.Timeout - - const fetchDelegations = async () => { - setIsLoading(true) - try { - await handleGetDelegations() - } catch (error) { - setConfirmationModalProps({ - status: 'error', - message: "Couldn't fetch delegations. Please try again later.", - }) - } finally { - setIsLoading(false) - - timeoutId = setTimeout(() => { - fetchDelegations() - }, 60_000) - } - } - - fetchDelegations() - - return () => { - clearTimeout(timeoutId) - } - }, [handleGetDelegations]) - - const getTooltipTitle = (pending: PendingEvent) => { - if (pending?.kind === 'undelegate') { - return 'You have an undelegation pending' - } - - if (pending?.kind === 'delegate') { - return `You have a delegation pending worth ${unymToNym( - pending.amount.amount - )} NYM` - } - - return undefined - } - - const onUndelegate = useCallback( - async (mixId: number) => { - setConfirmationModalProps({ status: 'loading' }) - - try { - const tx = await handleUndelegate(mixId) - - if (tx) { - setConfirmationModalProps({ - status: 'success', - message: 'Undelegation can take up to one hour to process', - transactions: [ - { - url: `${urls('MAINNET').blockExplorer}/transaction/${ - tx.transactionHash - }`, - hash: tx.transactionHash, - }, - ], - }) - } - } catch (error) { - if (error instanceof Error) { - setConfirmationModalProps({ status: 'error', message: error.message }) - } - } - }, - [handleUndelegate] - ) - - const columns = useMemo< - MRT_ColumnDef<ReturnType<typeof mapToDelegationsRow>>[] - >(() => { - return [ - { - id: 'delegations-data', - header: 'Delegations Data', - columns: [ - { - id: 'identity', - accessorKey: 'identity', - header: 'Identity Key', - width: 400, - }, - { - id: 'mix_id', - accessorKey: 'mix_id', - header: 'Mix ID', - size: 150, - }, - { - id: 'amount', - accessorKey: 'amount', - header: 'Amount', - width: 150, - }, - { - id: 'rewards', - accessorKey: 'rewards', - header: 'Rewards', - width: 150, - enableColumnFilters: false, - }, - { - id: 'undelegate', - accessorKey: 'undelegate', - header: '', - enableSorting: false, - enableColumnActions: false, - Filter: () => null, - Cell: ({ row }) => { - return ( - <Box - sx={{ width: '100%', display: 'flex', justifyContent: 'end' }} - > - {row.original.pending ? ( - <Tooltip - placement="left" - title={getTooltipTitle(row.original.pending)} - onClick={(e) => e.stopPropagation()} - PopperProps={{}} - > - <Chip size="small" label="Pending events" /> - </Tooltip> - ) : ( - <Button - size="small" - variant="outlined" - onClick={(e) => { - e.stopPropagation() - onUndelegate(row.original.mix_id) - }} - > - Undelegate - </Button> - )} - </Box> - ) - }, - }, - ], - }, - ] - }, [onUndelegate]) - - const data = useMemo(() => { - return (delegations || []).map(mapToDelegationsRow) - }, [delegations]) - - const table = useMaterialReactTable({ - columns, - data, - enableFullScreenToggle: false, - state: { - isLoading, - }, - initialState: { - columnPinning: { right: ['undelegate'] }, - }, - }) - - return ( - <Box> - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - if (confirmationModalProps.status === 'success') { - await handleGetDelegations() - } - setConfirmationModalProps(undefined) - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - {showBanner && <Banner onClose={() => setShowBanner(false)} />} - <Box display="flex" justifyContent="space-between" alignItems="center"> - <Title text="Your Delegations" /> - <Button - variant="contained" - color="primary" - onClick={() => router.push('/network-components/mixnodes')} - > - Delegate - </Button> - </Box> - {!isWalletConnected ? ( - <Box> - <Typography mb={2} variant="h6"> - Connect your wallet to view your delegations. - </Typography> - </Box> - ) : null} - - <Card - sx={{ - mt: 2, - padding: 2, - height: '100%', - }} - > - <MaterialReactTable table={table} /> - </Card> - </Box> - ) -} - -const Delegations = () => ( - <DelegationsProvider> - <DelegationsPage /> - </DelegationsProvider> -) - -export default Delegations diff --git a/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx b/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx deleted file mode 100644 index 4dbf9eb16e..0000000000 --- a/explorer-nextjs/app/errors/ErrorBoundaryContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react' -import { FallbackProps } from 'react-error-boundary' -import { Alert, AlertTitle, Container } from '@mui/material' -import { NymThemeProvider } from '@nymproject/mui-theme' -import { NymLogo } from '@nymproject/react/logo/NymLogo' - -export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({ - error, -}) => ( - <NymThemeProvider mode="dark"> - <Container sx={{ py: 4 }}> - <NymLogo height="75px" width="75px" /> - <h1>Oh no! Sorry, something went wrong</h1> - <Alert severity="error" data-testid="error-message"> - <AlertTitle>{error.name}</AlertTitle> - {error.message} - </Alert> - {process.env.NODE_ENV === 'development' && ( - <Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace"> - <AlertTitle>Stack trace</AlertTitle> - {error.stack} - </Alert> - )} - </Container> - </NymThemeProvider> -) diff --git a/explorer-nextjs/app/hooks/index.ts b/explorer-nextjs/app/hooks/index.ts deleted file mode 100644 index ba04855f77..0000000000 --- a/explorer-nextjs/app/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useIsMobile'; -export * from './useIsMounted'; -export * from './useGetMixnodeStatusColor'; -export * from './useNymClient'; diff --git a/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts b/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts deleted file mode 100644 index 3e1762209f..0000000000 --- a/explorer-nextjs/app/hooks/useGetMixnodeStatusColor.ts +++ /dev/null @@ -1,19 +0,0 @@ -'use client' - -import { useTheme } from '@mui/material'; -import { MixnodeStatus } from '@/app/typeDefs/explorer-api'; - -export const useGetMixNodeStatusColor = (status: MixnodeStatus) => { - const theme = useTheme(); - - switch (status) { - case MixnodeStatus.active: - return theme.palette.nym.networkExplorer.mixnodes.status.active; - - case MixnodeStatus.standby: - return theme.palette.nym.networkExplorer.mixnodes.status.standby; - - default: - return theme.palette.nym.networkExplorer.mixnodes.status.inactive; - } -}; diff --git a/explorer-nextjs/app/hooks/useIsMobile.ts b/explorer-nextjs/app/hooks/useIsMobile.ts deleted file mode 100644 index 0948d36cbe..0000000000 --- a/explorer-nextjs/app/hooks/useIsMobile.ts +++ /dev/null @@ -1,11 +0,0 @@ -'use client' - -import { Breakpoint, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); - - return isMobile; -}; diff --git a/explorer-nextjs/app/hooks/useIsMounted.ts b/explorer-nextjs/app/hooks/useIsMounted.ts deleted file mode 100644 index 5a78ae4fec..0000000000 --- a/explorer-nextjs/app/hooks/useIsMounted.ts +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import { useRef, useEffect, useCallback } from 'react'; - -export function useIsMounted(): () => boolean { - const ref = useRef(false); - - useEffect(() => { - ref.current = true; - return () => { - ref.current = false; - }; - }, []); - - return useCallback(() => ref.current, [ref]); -} diff --git a/explorer-nextjs/app/hooks/useNymClient.tsx b/explorer-nextjs/app/hooks/useNymClient.tsx deleted file mode 100644 index 2fa400b130..0000000000 --- a/explorer-nextjs/app/hooks/useNymClient.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { useChain } from '@cosmos-kit/react' -import { contracts } from '@nymproject/contract-clients' -import { - MixnetClient, - MixnetQueryClient, -} from '@nymproject/contract-clients/Mixnet.client' -import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@/app/api/constants' - -export const useNymClient = (address?: string) => { - const [nymClient, setNymClient] = useState<MixnetClient>() - const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>() - - const { getCosmWasmClient, getSigningCosmWasmClient } = - useChain(COSMOS_KIT_USE_CHAIN) - - useEffect(() => { - if (address) { - const init = async () => { - const cosmWasmSigningClient = await getSigningCosmWasmClient() - const cosmWasmClient = await getCosmWasmClient() - - const client = new contracts.Mixnet.MixnetClient( - cosmWasmSigningClient as any, - address, - NYM_MIXNET_CONTRACT - ) - const queryClient = new contracts.Mixnet.MixnetQueryClient( - cosmWasmClient as any, - NYM_MIXNET_CONTRACT - ) - - setNymClient(client) - setNymQueryClient(queryClient) - } - - init() - } - }, [address, getCosmWasmClient, getSigningCosmWasmClient]) - - return { nymClient, nymQueryClient } -} diff --git a/explorer-nextjs/app/icons/DelevateSVG.tsx b/explorer-nextjs/app/icons/DelevateSVG.tsx deleted file mode 100644 index 56180d1b8a..0000000000 --- a/explorer-nextjs/app/icons/DelevateSVG.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const DelegateIcon = (props: SvgIconProps) => ( - <SvgIcon {...props}> - <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> - <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> - <rect x="18" y="12" width="2" height="3" /> - <rect x="18" y="17" width="2" height="3" /> - <rect x="4" y="17" width="2" height="3" /> - </SvgIcon> -); diff --git a/explorer-nextjs/app/icons/ElipsSVG.tsx b/explorer-nextjs/app/icons/ElipsSVG.tsx deleted file mode 100644 index 4a430d6cb6..0000000000 --- a/explorer-nextjs/app/icons/ElipsSVG.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -export const ElipsSVG: FCWithChildren = () => ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" /> - <defs> - <radialGradient - id="paint0_angular_2549_7570" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(12 12.5) rotate(90) scale(12)" - > - <stop stopColor="#22D27E" /> - <stop offset="1" stopColor="#9002FF" /> - </radialGradient> - </defs> - </svg> -); diff --git a/explorer-nextjs/app/icons/GatewaysSVG.tsx b/explorer-nextjs/app/icons/GatewaysSVG.tsx deleted file mode 100644 index 00e4a21198..0000000000 --- a/explorer-nextjs/app/icons/GatewaysSVG.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const GatewaysSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path - d="M20.1 9.40015L22.7 12.0001L20.1 14.6001" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/LightSwitchSVG.tsx b/explorer-nextjs/app/icons/LightSwitchSVG.tsx deleted file mode 100644 index 7a32590dfc..0000000000 --- a/explorer-nextjs/app/icons/LightSwitchSVG.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const LightSwitchSVG: FCWithChildren = () => { - const { palette } = useTheme(); - return ( - <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z" - fill={palette.background.default} - /> - <path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/MixnodesSVG.tsx b/explorer-nextjs/app/icons/MixnodesSVG.tsx deleted file mode 100644 index 56902cb0de..0000000000 --- a/explorer-nextjs/app/icons/MixnodesSVG.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const MixnodesSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" /> - <path - d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/MobileDrawerClose.tsx b/explorer-nextjs/app/icons/MobileDrawerClose.tsx deleted file mode 100644 index 6c8ecfacbc..0000000000 --- a/explorer-nextjs/app/icons/MobileDrawerClose.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export const MobileDrawerClose: FCWithChildren = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}> - <path - d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z" - fill="#F2F2F2" - /> - </svg> -); diff --git a/explorer-nextjs/app/icons/NetworksSVG.tsx b/explorer-nextjs/app/icons/NetworksSVG.tsx deleted file mode 100644 index 0db2b9bdfb..0000000000 --- a/explorer-nextjs/app/icons/NetworksSVG.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NetworkComponentsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z" stroke={color} strokeMiterlimit="10" /> - <path d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z" stroke={color} strokeMiterlimit="10" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/NodemapSVG.tsx b/explorer-nextjs/app/icons/NodemapSVG.tsx deleted file mode 100644 index 9486d64dcc..0000000000 --- a/explorer-nextjs/app/icons/NodemapSVG.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NodemapSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M1.5 22.5999H17.1" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/NymVpn.tsx b/explorer-nextjs/app/icons/NymVpn.tsx deleted file mode 100644 index ab9f484d91..0000000000 --- a/explorer-nextjs/app/icons/NymVpn.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' - -interface DiscordIconProps { - size?: { width: number; height: number } -} - -export const NymVpnIcon: FCWithChildren<DiscordIconProps> = ({ size }) => ( - <svg - width={size?.width} - height={size?.height} - viewBox="0 0 170 24" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M19.6118 0.128906H19.5405V0.187854V20.7961L10.7849 0.164277L10.773 0.128906H10.7255H5.75959H0.187819H0.128418V0.187854V23.8142V23.8732H0.187819H5.75959H5.81899V23.8142V3.17063L14.6103 23.8378L14.6222 23.8732H14.6697H19.6118H25.1717H25.2311V23.8142V0.187854V0.128906H25.1717H19.6118Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M19.4121 0H25.3603V24H14.5297L14.4901 23.8819L5.94824 3.80121V24H0V0H10.8663L10.906 0.118132L19.4121 20.1621V0ZM19.5409 20.7951L10.7853 0.163225L10.7734 0.127854H0.128835V23.8721H5.81941V3.16958L14.6107 23.8368L14.6226 23.8721H25.2315V0.127854H19.5409V20.7951Z" - fill="white" - /> - <path - d="M89.8116 0.128906H79.1908H79.1314L79.1195 0.176068L73.6784 20.8904L68.2255 0.176068L68.2136 0.128906H68.1661H57.5215H57.4502V0.187854V23.8142V23.8732H57.5215H63.0814H63.1408V23.8142V3.33568L68.5225 23.826L68.5343 23.8732H68.5937H78.7394H78.7869L78.7988 23.826L84.1804 3.33568V23.8142V23.8732H84.2398H89.8116H89.871V23.8142V0.187854V0.128906H89.8116Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M79.0312 0H90.0003V24H84.052V4.33208L78.9242 23.856L78.9238 23.8572L78.8879 24H68.4342L68.3982 23.8572L68.3979 23.856L63.27 4.33208V24H57.3218V0H68.3146L68.3505 0.142699L68.3509 0.144015L73.6787 20.383L78.9949 0.144015L78.9953 0.142765L79.0312 0ZM73.6788 20.8894L68.2259 0.175015L68.214 0.127854H57.4506V23.8721H63.1412V3.33463L68.5229 23.825L68.5348 23.8721H78.7873L78.7992 23.825L84.1809 3.33463V23.8721H89.8714V0.127854H79.1318L79.1199 0.175015L73.6788 20.8894Z" - fill="white" - /> - <path - d="M48.2909 0.128906H48.2553L48.2434 0.152487L41.4836 11.8124L34.6882 0.152487L34.6763 0.128906H34.6407H28.2135H28.0947L28.1541 0.223225L38.6205 18.2142V23.8142V23.8732H38.6799H44.2517H44.3111V23.8142V18.2142L54.7775 0.223225L54.8369 0.128906H54.7181H48.2909Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M48.1757 0H55.0693L54.8879 0.288036L44.4399 18.2474V24H38.4917V18.2474L28.0437 0.288036L27.8623 0H34.756L34.8017 0.0907854L41.4833 11.5555L48.1299 0.0909153L48.1757 0ZM48.2434 0.151434L41.4836 11.8114L34.6882 0.151434L34.6763 0.127854H28.0948L28.1542 0.222173L38.6205 18.2131V23.8721H44.3111V18.2131L54.7775 0.222173L54.8369 0.127854H48.2553L48.2434 0.151434Z" - fill="white" - /> - <path - d="M169.238 0V24H166.422C166.006 24 165.654 23.9341 165.366 23.8023C165.088 23.6596 164.811 23.418 164.534 23.0776L153.542 8.76321C153.584 9.19149 153.611 9.60878 153.622 10.0151C153.643 10.4104 153.654 10.7838 153.654 11.1352V24H148.886V0H151.734C151.968 0 152.166 0.0109813 152.326 0.032944C152.486 0.0549066 152.63 0.0988326 152.758 0.164722C152.886 0.219629 153.008 0.30199 153.126 0.411805C153.243 0.521619 153.376 0.669869 153.526 0.856553L164.614 15.2697C164.56 14.8085 164.523 14.3638 164.502 13.9355C164.48 13.4962 164.47 13.0844 164.47 12.7001V0H169.238Z" - fill="#A8A6A6" - /> - <path - d="M134.206 11.7776C135.614 11.7776 136.627 11.4317 137.246 10.7399C137.865 10.048 138.174 9.08167 138.174 7.84077C138.174 7.29169 138.094 6.79204 137.934 6.3418C137.774 5.89156 137.529 5.50721 137.198 5.18874C136.878 4.8593 136.467 4.60673 135.966 4.43102C135.475 4.25532 134.889 4.16747 134.206 4.16747H131.39V11.7776H134.206ZM134.206 0C135.849 0 137.257 0.203157 138.43 0.609471C139.614 1.0048 140.585 1.55388 141.342 2.25669C142.11 2.95951 142.675 3.78861 143.038 4.74399C143.401 5.69938 143.582 6.73164 143.582 7.84077C143.582 9.03775 143.395 10.1359 143.022 11.1352C142.649 12.1345 142.078 12.9911 141.31 13.7049C140.542 14.4187 139.566 14.9787 138.382 15.385C137.209 15.7804 135.817 15.978 134.206 15.978H131.39V24H125.982V0H134.206Z" - fill="#A8A6A6" - /> - <path - d="M121.584 0L112.24 24H107.344L98 0H102.352C102.821 0 103.2 0.115305 103.488 0.345915C103.776 0.565545 103.995 0.851064 104.144 1.20247L108.656 14.0508C108.869 14.6108 109.077 15.2258 109.28 15.8957C109.483 16.5546 109.675 17.2464 109.856 17.9712C110.005 17.2464 110.171 16.5546 110.352 15.8957C110.544 15.2258 110.747 14.6108 110.96 14.0508L115.44 1.20247C115.557 0.894989 115.765 0.620452 116.064 0.378861C116.373 0.126287 116.752 0 117.2 0H121.584Z" - fill="#A8A6A6" - /> - </svg> -) diff --git a/explorer-nextjs/app/icons/OverviewSVG.tsx b/explorer-nextjs/app/icons/OverviewSVG.tsx deleted file mode 100644 index 2ced0bb17b..0000000000 --- a/explorer-nextjs/app/icons/OverviewSVG.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const OverviewSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M1.4 21.6H22.6" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M7.4 11.8H3.2V21.6H7.4V11.8Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/TokenSVG.tsx b/explorer-nextjs/app/icons/TokenSVG.tsx deleted file mode 100644 index 94ab1468c9..0000000000 --- a/explorer-nextjs/app/icons/TokenSVG.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export const TokenSVG: FCWithChildren = () => { - const color = 'white'; - - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <g clipPath="url(#clip0_2549_7563)"> - <path - d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z" - fill={color} - /> - <path - d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_2549_7563"> - <rect width="24" height="24" fill="white" transform="translate(0 0.5)" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/ValidatorsSVG.tsx b/explorer-nextjs/app/icons/ValidatorsSVG.tsx deleted file mode 100644 index cf03f1c330..0000000000 --- a/explorer-nextjs/app/icons/ValidatorsSVG.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const ValidatorsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clipPath="url(#clip0)"> - <path - d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M5 12.3L7.9 15.3L14.5 8.69995" - stroke={color} - strokeWidth="2" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - </g> - <defs> - <clipPath id="clip0"> - <rect width="24" height="24" fill="white" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer-nextjs/app/icons/socials/DiscordIcon.tsx b/explorer-nextjs/app/icons/socials/DiscordIcon.tsx deleted file mode 100644 index b81c571c33..0000000000 --- a/explorer-nextjs/app/icons/socials/DiscordIcon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface DiscordIconProps { - size?: number | string - color?: string -} - -export const DiscordIcon: FCWithChildren<DiscordIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2296)"> - <path - d="M12.4 0C5.80002 0 0.400024 5.4 0.400024 12C0.400024 18.6 5.80002 24 12.4 24C19 24 24.4 18.6 24.4 12C24.4 5.4 19 0 12.4 0ZM20.1 15.9C18.8 16.9 17.5 17.5 16.2 17.9C16.2 17.9 16.2 17.9 16.1 17.9C15.8 17.5 15.5 17.1 15.3 16.6V16.5C15.7 16.3 16.1 16.1 16.5 15.9V15.8C16.4 15.7 16.3 15.7 16.3 15.6C16.3 15.6 16.3 15.6 16.2 15.6C13.7 16.8 10.9 16.8 8.40002 15.6C8.40002 15.6 8.40002 15.6 8.30002 15.6C8.20002 15.7 8.10002 15.7 8.10002 15.8V15.9C8.50002 16.1 8.90002 16.3 9.30002 16.5C9.30002 16.5 9.30002 16.5 9.30002 16.6C9.10002 17.1 8.80002 17.5 8.50002 17.9C8.50002 17.9 8.50002 17.9 8.40002 17.9C7.10002 17.5 5.90002 16.9 4.50002 15.9C4.40002 13 5.00002 10.1 7.00002 7.1C8.00002 6.6 9.00002 6.3 10.2 6.1C10.2 6.1 10.2 6.1 10.3 6.1C10.4 6.3 10.6 6.7 10.7 6.9C11.9 6.7 13.1 6.7 14.2 6.9C14.3 6.7 14.5 6.3 14.6 6.1C14.6 6.1 14.6 6.1 14.7 6.1C15.8 6.3 16.9 6.6 17.9 7.1C19.5 9.7 20.4 12.6 20.1 15.9Z" - fill={color} - /> - <path - d="M15 11C14.2 11 13.6 11.7 13.6 12.6C13.6 13.5 14.2 14.2 15 14.2C15.8 14.2 16.4 13.5 16.4 12.6C16.4 11.7 15.8 11 15 11Z" - fill={color} - /> - <path - d="M9.80002 11C9.10002 11 8.40002 11.7 8.40002 12.6C8.40002 13.5 9.00002 14.2 9.80002 14.2C10.6 14.2 11.2 13.5 11.2 12.6C11.2 11.7 10.6 11 9.80002 11Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2296"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/GitHubIcon.tsx b/explorer-nextjs/app/icons/socials/GitHubIcon.tsx deleted file mode 100644 index 11389e6969..0000000000 --- a/explorer-nextjs/app/icons/socials/GitHubIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface GitHubIconProps { - size?: number | string - color?: string -} - -export const GitHubIcon: FCWithChildren<GitHubIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2302)"> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M12.7 0C5.90002 0 0.400024 5.5 0.400024 12.3C0.400024 17.7 3.90002 22.3 8.80002 24C9.40002 24.1 9.60002 23.7 9.60002 23.4C9.60002 23.1 9.60002 22.1 9.60002 21.1C6.50002 21.7 5.70002 20.3 5.50002 19.7C5.40002 19.3 4.80002 18.3 4.20002 18C3.80002 17.8 3.20002 17.2 4.20002 17.2C5.20002 17.2 5.90002 18.1 6.10002 18.5C7.20002 20.4 9.00002 19.8 9.70002 19.5C9.80002 18.7 10.1 18.2 10.5 17.9C7.80002 17.6 4.90002 16.5 4.90002 11.8C4.90002 10.5 5.40002 9.4 6.20002 8.5C6.00002 8 5.60002 6.8 6.30002 5.1C6.30002 5.1 7.30002 4.8 9.70002 6.4C10.7 6.1 11.7 6 12.8 6C13.8 6 14.9 6.1 15.9 6.4C18.3 4.8 19.3 5.1 19.3 5.1C20 6.8 19.5 8.1 19.4 8.4C20.2 9.3 20.7 10.4 20.7 11.7C20.7 16.4 17.8 17.5 15.1 17.8C15.5 18.2 15.9 18.9 15.9 20.1C15.9 21.7 15.9 23.1 15.9 23.5C15.9 23.8 16.1 24.2 16.7 24.1C21.6 22.5 25.1 17.9 25.1 12.4C25 5.5 19.5 0 12.7 0Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2302"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/TelegramIcon.tsx b/explorer-nextjs/app/icons/socials/TelegramIcon.tsx deleted file mode 100644 index a66020a849..0000000000 --- a/explorer-nextjs/app/icons/socials/TelegramIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface TelegramIconProps { - size?: number | string - color?: string -} - -export const TelegramIcon: FCWithChildren<TelegramIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <path - d="M12.4 24C19.029 24 24.4 18.629 24.4 12C24.4 5.371 19.029 0 12.4 0C5.77102 0 0.400024 5.371 0.400024 12C0.400024 18.629 5.77102 24 12.4 24ZM5.89102 11.74L17.461 7.279C17.998 7.085 18.467 7.41 18.293 8.222L18.294 8.221L16.324 17.502C16.178 18.16 15.787 18.32 15.24 18.01L12.24 15.799L10.793 17.193C10.633 17.353 10.498 17.488 10.188 17.488L10.401 14.435L15.961 9.412C16.203 9.199 15.907 9.079 15.588 9.291L8.71702 13.617L5.75502 12.693C5.11202 12.489 5.09802 12.05 5.89102 11.74Z" - fill={color} - /> - </svg> - ) -} diff --git a/explorer-nextjs/app/icons/socials/TwitterIcon.tsx b/explorer-nextjs/app/icons/socials/TwitterIcon.tsx deleted file mode 100644 index 5d94de99a3..0000000000 --- a/explorer-nextjs/app/icons/socials/TwitterIcon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react' -import { useTheme } from '@mui/material/styles' - -interface TwitterIconProps { - size?: number | string - color?: string -} - -export const TwitterIcon: FCWithChildren<TwitterIconProps> = ({ - size, - color: colorProp, -}) => { - const theme = useTheme() - const color = colorProp || theme.palette.text.primary - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2294)"> - <path - d="M12.4 0C5.77362 0 0.400024 5.3736 0.400024 12C0.400024 18.6264 5.77362 24 12.4 24C19.0264 24 24.4 18.6264 24.4 12C24.4 5.3736 19.0264 0 12.4 0ZM17.8791 9.35632C17.8844 9.47443 17.887 9.59308 17.887 9.71228C17.887 13.3519 15.1166 17.5488 10.0502 17.549H10.0504H10.0502C8.49475 17.549 7.0473 17.0931 5.82837 16.3118C6.04388 16.3372 6.26324 16.3499 6.48535 16.3499C7.77588 16.3499 8.9635 15.9097 9.90631 15.1708C8.70056 15.1485 7.68396 14.3522 7.33313 13.2578C7.50104 13.29 7.67371 13.3076 7.85077 13.3076C8.10217 13.3076 8.3457 13.2737 8.57715 13.2105C7.31683 12.9582 6.36743 11.8444 6.36743 10.5106C6.36743 10.4982 6.36743 10.487 6.3678 10.4755C6.73895 10.6818 7.16339 10.806 7.6153 10.8199C6.87573 10.3264 6.38959 9.48285 6.38959 8.52722C6.38959 8.02258 6.526 7.5498 6.76257 7.14276C8.12085 8.80939 10.1508 9.90546 12.4399 10.0206C12.3927 9.81885 12.3683 9.60864 12.3683 9.39258C12.3683 7.87207 13.6019 6.63849 15.123 6.63849C15.9153 6.63849 16.6309 6.97339 17.1335 7.50879C17.761 7.38501 18.3502 7.15576 18.8825 6.84027C18.6765 7.48315 18.24 8.02258 17.6713 8.36371C18.2285 8.29706 18.7595 8.14929 19.2529 7.92993C18.8843 8.48236 18.4169 8.96759 17.8791 9.35632V9.35632Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2294"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ) -} diff --git a/explorer-nextjs/app/layout.tsx b/explorer-nextjs/app/layout.tsx deleted file mode 100644 index a532714d84..0000000000 --- a/explorer-nextjs/app/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { Metadata } from 'next' -import '@interchain-ui/react/styles' -import { App } from './App' - -export const metadata: Metadata = { - title: 'Nym Network Explorer', -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - <html lang="en"> - <body> - <App>{children}</App> - </body> - </html> - ) -} diff --git a/explorer-nextjs/app/loading.tsx b/explorer-nextjs/app/loading.tsx deleted file mode 100644 index bb90466dc5..0000000000 --- a/explorer-nextjs/app/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { LinearProgress, Box } from '@mui/material' - -export default function Loading() { - return ( - <Box sx={{ py: 16 }}> - <LinearProgress /> - </Box> - ) -} diff --git a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx deleted file mode 100644 index 87f779872a..0000000000 --- a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client' - -import * as React from 'react' -import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' -import { useParams } from 'next/navigation' -import {GatewayBond, LocatedGateway} from '@/app/typeDefs/explorer-api' -import { ColumnsType, DetailTable } from '@/app/components/DetailTable' -import { - gatewayEnrichedToGridRow, - GatewayEnrichedRowType, -} from '@/app/components/Gateways/Gateways' -import { ComponentError } from '@/app/components/ComponentError' -import { ContentCard } from '@/app/components/ContentCard' -import { TwoColSmallTable } from '@/app/components/TwoColSmallTable' -import { UptimeChart } from '@/app/components/UptimeChart' -import { - GatewayContextProvider, - useGatewayContext, -} from '@/app/context/gateway' -import { useMainContext } from '@/app/context/main' -import { Title } from '@/app/components/Title' - -const columns: ColumnsType[] = [ - { - field: 'identity_key', - title: 'Identity Key', - headerAlign: 'left', - width: 230, - }, - { - field: 'bond', - title: 'Bond', - headerAlign: 'left', - }, - { - field: 'avgUptime', - title: 'Avg. Score', - headerAlign: 'left', - tooltipInfo: "Gateway's average routing score in the last 24 hours", - }, - { - field: 'host', - title: 'IP', - headerAlign: 'left', - width: 99, - }, - { - field: 'location', - title: 'Location', - headerAlign: 'left', - }, - { - field: 'owner', - title: 'Owner', - headerAlign: 'left', - }, - { - field: 'version', - title: 'Version', - headerAlign: 'left', - }, -] - -/** - * Shows gateway details - */ -const PageGatewayDetailsWithState = ({ - selectedGateway, -}: { - selectedGateway: LocatedGateway | undefined -}) => { - const [enrichGateway, setEnrichGateway] = - React.useState<GatewayEnrichedRowType>() - const [status, setStatus] = React.useState<number[] | undefined>() - const { uptimeReport, uptimeStory } = useGatewayContext() - - React.useEffect(() => { - if (uptimeReport?.data && selectedGateway) { - setEnrichGateway( - gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data) - ) - } - }, [uptimeReport, selectedGateway]) - - React.useEffect(() => { - if (enrichGateway) { - setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]) - } - }, [enrichGateway]) - - return ( - <Box component="main"> - <Title text="Legacy Gateway Detail" /> - - <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> - <AlertTitle> - Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet - </AlertTitle> - </Alert> - - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Gateway detail table" - rows={enrichGateway ? [enrichGateway] : []} - /> - </Grid> - </Grid> - - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Gateway Status"> - <TwoColSmallTable - loading={false} - keys={['Mix port', 'Client WS API Port']} - values={status.map((each) => each)} - icons={status.map((elem) => !!elem)} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ) -} - -/** - * Guard component to handle loadingW and not found states - */ -const PageGatewayDetailGuard = () => { - const [selectedGateway, setSelectedGateway] = React.useState<LocatedGateway>() - const { gateways } = useMainContext() - const { id } = useParams() - - React.useEffect(() => { - if (gateways?.data) { - setSelectedGateway( - gateways.data.find((g) => g.gateway.identity_key === id) - ) - } - }, [gateways, id]) - - if (gateways?.isLoading) { - return <CircularProgress /> - } - - if (gateways?.error) { - // eslint-disable-next-line no-console - console.error(gateways?.error) - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ) - } - - // loaded, but not found - if (gateways && !gateways.isLoading && !gateways.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Gateway not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ) - } - - return <PageGatewayDetailsWithState selectedGateway={selectedGateway} /> -} - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -const PageGatewayDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! No mixnode identity key specified</Alert> - ) - } - - return ( - <GatewayContextProvider gatewayIdentityKey={id}> - <PageGatewayDetailGuard /> - </GatewayContextProvider> - ) -} - -export default PageGatewayDetail diff --git a/explorer-nextjs/app/network-components/gateways/page.tsx b/explorer-nextjs/app/network-components/gateways/page.tsx deleted file mode 100644 index c2660ce387..0000000000 --- a/explorer-nextjs/app/network-components/gateways/page.tsx +++ /dev/null @@ -1,256 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { Box, Card, Grid, Stack } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip' -import { diff, gte, rcompare } from 'semver' -import { useMainContext } from '@/app/context/main' -import { TableToolbar } from '@/app/components/TableToolbar' -import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' -import { Title } from '@/app/components/Title' -import { unymToNym } from '@/app/utils/currency' -import { Tooltip } from '@/app/components/Tooltip' -import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { splice } from '@/app/utils' -import { - VersionDisplaySelector, - VersionSelectOptions, -} from '@/app/components/Gateways/VersionDisplaySelector' -import StyledLink from '@/app/components/StyledLink' -import { - GatewayRowType, - gatewayToGridRow, -} from '@/app/components/Gateways/Gateways' -import {LocatedGateway} from "@/app/typeDefs/explorer-api"; - -const gatewaySanitize = (g?: LocatedGateway): boolean => { - if(!g) { - return false; - } - - if(!g.gateway.version || !g.gateway.version.trim().length) { - return false; - } - - if(g.gateway.version === "null") { - return false; - } - - return true; -} - -const PageGateways = () => { - const { gateways } = useMainContext() - const [versionFilter, setVersionFilter] = - React.useState<VersionSelectOptions>(VersionSelectOptions.all) - - const theme = useTheme() - - const highestVersion = React.useMemo(() => { - if (gateways?.data) { - const versions = gateways.data.filter(gatewaySanitize).reduce( - (a: string[], b) => [...a, b.gateway.version], - [] - ) - const [lastestVersion] = versions.sort(rcompare) - return lastestVersion - } - // fallback value - return '2.0.0' - }, [gateways]) - - const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version) - return versionDiff === 'patch' || versionDiff === null - }) - if (filtered) return filtered - return [] - }, [gateways]) - - const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version) - return versionDiff === 'major' || versionDiff === 'minor' - }) - if (filtered) return filtered - return [] - }, [gateways]) - - const filteredByVersion = React.useMemo(() => { - switch (versionFilter) { - case VersionSelectOptions.latestVersion: - return filterByLatestVersions - case VersionSelectOptions.olderVersions: - return filterByOlderVersions - case VersionSelectOptions.all: - return gateways?.data || [] - default: - return [] - } - }, [versionFilter, gateways]) - - const data = useMemo(() => { - return gatewayToGridRow(filteredByVersion || []) - }, [filteredByVersion]) - - const columns = useMemo<MRT_ColumnDef<GatewayRowType>[]>(() => { - return [ - { - id: 'gateway-data', - header: 'Gateways Data', - columns: [ - { - id: 'identity_key', - header: 'Identity Key', - accessorKey: 'identity_key', - size: 250, - Cell: ({ row }) => { - return ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={row.original.identity_key} - tooltip={`Copy identity key ${row.original.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - dataTestId="identity-link" - color="text.primary" - > - {splice(7, 29, row.original.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'version', - header: 'Version', - accessorKey: 'version', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="version" - color="text.primary" - > - {row.original.version} - </StyledLink> - ) - }, - }, - { - id: 'location', - header: 'Location', - accessorKey: 'location', - size: 150, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip - text={row.original.location} - id="gateway-location-text" - > - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {row.original.location} - </Box> - </Tooltip> - </Box> - ) - }, - }, - { - id: 'host', - header: 'IP:Port', - accessorKey: 'host', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="host" - color="text.primary" - > - {row.original.host} - </StyledLink> - ) - }, - }, - { - id: 'owner', - header: 'Owner', - accessorKey: 'owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} - target="_blank" - data-testid="owner" - color="text.primary" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ) - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data, - }) - - return ( - <> - <Box mb={2}> - <Title text="Legacy Gateways" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <VersionDisplaySelector - handleChange={(option) => setVersionFilter(option)} - selected={versionFilter} - /> - } - /> - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default PageGateways diff --git a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx deleted file mode 100644 index 5e03b5d132..0000000000 --- a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx +++ /dev/null @@ -1,302 +0,0 @@ -'use client' - -import * as React from 'react' -import { - Alert, - AlertTitle, - Box, - CircularProgress, - Grid, - Typography, -} from '@mui/material' -import { ColumnsType, DetailTable } from '@/app/components/DetailTable' -import { BondBreakdownTable } from '@/app/components/MixNodes/BondBreakdown' -import { - DelegatorsInfoTable, - EconomicsInfoColumns, - EconomicsInfoRows, -} from '@/app/components/MixNodes/Economics' -import { ComponentError } from '@/app/components/ComponentError' -import { ContentCard } from '@/app/components/ContentCard' -import { TwoColSmallTable } from '@/app/components/TwoColSmallTable' -import { UptimeChart } from '@/app/components/UptimeChart' -import { WorldMap } from '@/app/components/WorldMap' -import { MixNodeDetailSection } from '@/app/components/MixNodes/DetailSection' -import { - MixnodeContextProvider, - useMixnodeContext, -} from '@/app/context/mixnode' -import { Title } from '@/app/components/Title' -import { useIsMobile } from '@/app/hooks/useIsMobile' -import { useParams } from 'next/navigation' - -const columns: ColumnsType[] = [ - { - field: 'owner', - title: 'Owner', - width: '15%', - }, - { - field: 'identity_key', - title: 'Identity Key', - width: '15%', - }, - - { - field: 'bond', - title: 'Stake', - width: '12.5%', - }, - { - field: 'stake_saturation', - title: 'Stake Saturation', - width: '12.5%', - tooltipInfo: - 'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.', - }, - { - field: 'self_percentage', - width: '10%', - title: 'Bond %', - tooltipInfo: - "Percentage of the operator's bond to the total stake on the node", - }, - - { - field: 'host', - width: '10%', - title: 'Host', - }, - { - field: 'location', - title: 'Location', - }, - - { - field: 'layer', - title: 'Layer', - }, -] - -/** - * Shows mix node details - */ -const PageMixnodeDetailWithState = () => { - const { - mixNode, - mixNodeRow, - description, - stats, - status, - uptimeStory, - uniqDelegations, - } = useMixnodeContext() - const isMobile = useIsMobile() - return ( - <Box component="main"> - <Title text="Legacy Mixnode Detail" /> - <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> - <AlertTitle> - Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet - </AlertTitle> - </Alert> - <Grid container spacing={2} mt={1} mb={6}> - <Grid item xs={12}> - {mixNodeRow && description?.data && ( - <MixNodeDetailSection - mixNodeRow={mixNodeRow} - mixnodeDescription={description.data} - /> - )} - {mixNodeRow?.blacklisted && ( - <Typography - textAlign={isMobile ? 'left' : 'right'} - fontSize="smaller" - sx={{ color: 'error.main' }} - > - This node is having a poor performance - </Typography> - )} - </Grid> - </Grid> - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Mixnode detail table" - rows={mixNodeRow ? [mixNodeRow] : []} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <DelegatorsInfoTable - columnsData={EconomicsInfoColumns} - tableName="Delegators info table" - rows={[EconomicsInfoRows()]} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <ContentCard - title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`} - > - <BondBreakdownTable /> - </ContentCard> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - <ContentCard title="Mixnode Stats"> - {stats && ( - <> - {stats.error && ( - <ComponentError text="There was a problem retrieving this nodes stats." /> - )} - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since startup" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_startup || 0, - stats?.data?.packets_sent_since_startup || 0, - stats?.data?.packets_explicitly_dropped_since_startup || 0, - ]} - /> - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since last update" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_last_update || 0, - stats?.data?.packets_sent_since_last_update || 0, - stats?.data?.packets_explicitly_dropped_since_last_update || - 0, - ]} - marginBottom - /> - </> - )} - {!stats && <Typography>No stats information</Typography>} - </ContentCard> - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Mixnode Status"> - {status.error && ( - <ComponentError text="There was a problem retrieving port information" /> - )} - <TwoColSmallTable - loading={status.isLoading} - error={status?.error?.message} - keys={['Mix port', 'Verloc port', 'HTTP port']} - values={[1789, 1790, 8000].map((each) => each)} - icons={ - (status?.data?.ports && Object.values(status.data.ports)) || [ - false, - false, - false, - ] - } - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {mixNode && ( - <ContentCard title="Location"> - {mixNode?.error && ( - <ComponentError text="There was a problem retrieving this mixnode location" /> - )} - {mixNode?.data?.location?.latitude && - mixNode?.data?.location?.longitude && ( - <WorldMap - loading={mixNode.isLoading} - userLocation={[ - mixNode.data.location.longitude, - mixNode.data.location.latitude, - ]} - /> - )} - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ) -} - -/** - * Guard component to handle loading and not found states - */ -const PageMixnodeDetailGuard = () => { - const { mixNode } = useMixnodeContext() - const { id } = useParams() - - if (mixNode?.isLoading) { - return <CircularProgress /> - } - - if (mixNode?.error) { - // eslint-disable-next-line no-console - console.error(mixNode?.error) - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ) - } - - // loaded, but not found - if (mixNode && !mixNode.isLoading && !mixNode.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Mixnode not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ) - } - - return <PageMixnodeDetailWithState /> -} - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -const PageMixnodeDetail = () => { - const { id } = useParams() - - if (!id || typeof id !== 'string') { - return ( - <Alert severity="error">Oh no! No mixnode identity key specified</Alert> - ) - } - - return ( - <MixnodeContextProvider mixId={id}> - <PageMixnodeDetailGuard /> - </MixnodeContextProvider> - ) -} - -export default PageMixnodeDetail diff --git a/explorer-nextjs/app/network-components/mixnodes/page.tsx b/explorer-nextjs/app/network-components/mixnodes/page.tsx deleted file mode 100644 index 2f2774a7f4..0000000000 --- a/explorer-nextjs/app/network-components/mixnodes/page.tsx +++ /dev/null @@ -1,382 +0,0 @@ -'use client' - -import React, { useCallback, useMemo } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' -import { - MaterialReactTable, - useMaterialReactTable, - type MRT_ColumnDef, -} from 'material-react-table' -import { Grid, Card, Button, Box, Stack } from '@mui/material' -import { - CustomColumnHeading, - DelegateIconButton, - DelegateModal, - DelegationModal, - DelegationModalProps, - MixNodeStatusDropdown, - MixnodeRowType, - StyledLink, - TableToolbar, - Title, - Tooltip, - mixnodeToGridRow, -} from '@/app/components' -import { DelegationsProvider } from '@/app/context/delegations' -import { useWalletContext } from '@/app/context/wallet' -import { useGetMixNodeStatusColor, useIsMobile } from '@/app/hooks' -import { useMainContext } from '@/app/context/main' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { splice } from '@/app/utils' -import { currencyToString } from '@/app/utils/currency' -import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { - MixnodeStatusWithAll, - toMixnodeStatus, -} from '@/app/typeDefs/explorer-api' - -export default function MixnodesPage() { - const isMobile = useIsMobile() - const { isWalletConnected } = useWalletContext() - const { mixnodes, fetchMixnodes } = useMainContext() - const router = useRouter() - - const [itemSelectedForDelegation, setItemSelectedForDelegation] = - React.useState<{ - mixId: number - identityKey: string - }>() - const [confirmationModalProps, setConfirmationModalProps] = React.useState< - DelegationModalProps | undefined - >() - - const search = useSearchParams() - const status = search.get('status') as MixnodeStatusWithAll - - React.useEffect(() => { - // when the status changes, get the mixnodes - fetchMixnodes(toMixnodeStatus(status)) - }, [status]) - - const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => { - router.push( - newStatus && newStatus !== 'all' - ? `/network-components/mixnodes?status=${newStatus}` - : '/network-components/mixnodes' - ) - } - - const handleOnDelegate = useCallback( - ({ identityKey, mixId }: { identityKey: string; mixId: number }) => { - if (!isWalletConnected) { - setConfirmationModalProps({ - status: 'info', - message: 'Please connect your wallet to delegate', - }) - } else { - setItemSelectedForDelegation({ identityKey, mixId }) - } - }, - [isWalletConnected] - ) - - const handleNewDelegation = (delegationModalProps: DelegationModalProps) => { - setItemSelectedForDelegation(undefined) - setConfirmationModalProps(delegationModalProps) - } - - const columns = useMemo<MRT_ColumnDef<MixnodeRowType>[]>(() => { - return [ - { - id: 'mixnode-data', - header: 'Mixnode Data', - columns: [ - { - id: 'delegate', - accessorKey: 'delegate', - size: isMobile ? 50 : 150, - header: '', - grow: false, - Cell: ({ row }) => ( - <DelegateIconButton - size="small" - onDelegate={() => - handleOnDelegate({ - identityKey: row.original.identity_key, - mixId: row.original.mix_id, - }) - } - /> - ), - enableSorting: false, - enableColumnActions: false, - Filter: () => null, - }, - { - id: 'identity_key', - header: 'Identity Key', - accessorKey: 'identity_key', - size: 250, - Cell: ({ row }) => { - return ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={row.original.identity_key} - tooltip={`Copy identity key ${row.original.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - dataTestId="identity-link" - > - {splice(7, 29, row.original.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'bond', - header: 'Stake', - accessorKey: 'bond', - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - > - {currencyToString({ amount: row.original.bond.toString() })} - </StyledLink> - ), - }, - { - id: 'stake_saturation', - header: 'Stake Saturation', - accessorKey: 'stake_saturation', - size: 225, - Header() { - return ( - <CustomColumnHeading - headingTitle="Stake Saturation" - tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set." - /> - ) - }, - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.stake_saturation} %`}</StyledLink> - ), - }, - { - id: 'pledge_amount', - header: 'Bond', - accessorKey: 'pledge_amount', - size: 185, - Header: () => ( - <CustomColumnHeading - headingTitle="Bond" - tooltipInfo="Node operator's share of stake." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - > - {currencyToString({ - amount: row.original.pledge_amount.toString(), - })} - </StyledLink> - ), - }, - { - id: 'profit_percentage', - accessorKey: 'profit_percentage', - header: 'Profit Margin', - size: 145, - Header: () => ( - <CustomColumnHeading - headingTitle="Profit Margin" - tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.profit_percentage}%`}</StyledLink> - ), - }, - { - id: 'operating_cost', - accessorKey: 'operating_cost', - size: 220, - header: 'Operating Cost', - disableColumnMenu: true, - Header: () => ( - <CustomColumnHeading - headingTitle="Operating Cost" - tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.operating_cost} NYM`}</StyledLink> - ), - }, - { - id: 'owner', - accessorKey: 'owner', - size: 150, - header: 'Owner', - Header: () => <CustomColumnHeading headingTitle="Owner" />, - Cell: ({ row }) => ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} - color={useGetMixNodeStatusColor(row.original.status)} - target="_blank" - data-testid="big-dipper-link" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ), - }, - { - id: 'location', - accessorKey: 'location', - header: 'Location', - maxSize: 150, - Header: () => <CustomColumnHeading headingTitle="Location" />, - Cell: ({ row }) => ( - <Tooltip text={row.original.location} id="mixnode-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - cursor: 'pointer', - color: useGetMixNodeStatusColor(row.original.status), - }} - > - {row.original.location} - </Box> - </Tooltip> - ), - }, - { - id: 'host', - accessorKey: 'host', - header: 'Host', - size: 130, - Header: () => <CustomColumnHeading headingTitle="Host" />, - Cell: ({ row }) => ( - <StyledLink - color={useGetMixNodeStatusColor(row.original.status)} - to={`/network-components/mixnodes/${row.original.mix_id}`} - > - {row.original.host} - </StyledLink> - ), - }, - ], - }, - ] - }, [handleOnDelegate, isMobile]) - - const data = useMemo(() => { - return mixnodeToGridRow(mixnodes?.data) - }, [mixnodes?.data]) - - const table = useMaterialReactTable({ - columns, - data, - enableFullScreenToggle: false, - state: { - isLoading: mixnodes?.isLoading, - }, - layoutMode: 'grid-no-grow', - initialState: { - columnPinning: { left: ['delegate'] }, - }, - }) - - return ( - <DelegationsProvider> - <Box mb={2}> - <Title text="Legacy Mixnodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <MixNodeStatusDropdown - sx={{ mr: 2 }} - status={status} - onSelectionChanged={handleMixnodeStatusChanged} - /> - } - childrenAfter={ - isWalletConnected && ( - <Button - fullWidth - size="large" - variant="outlined" - onClick={() => router.push('/delegations')} - > - Delegations - </Button> - ) - } - /> - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - {itemSelectedForDelegation && ( - <DelegateModal - onClose={() => { - setItemSelectedForDelegation(undefined) - }} - header="Delegate" - buttonText="Delegate stake" - denom="nym" - onOk={(delegationModalProps: DelegationModalProps) => - handleNewDelegation(delegationModalProps) - } - identityKey={itemSelectedForDelegation.identityKey} - mixId={itemSelectedForDelegation.mixId} - /> - )} - - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - setConfirmationModalProps(undefined) - if (confirmationModalProps.status === 'success') { - router.push('/delegations') - } - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - </DelegationsProvider> - ) -} diff --git a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx deleted file mode 100644 index fac36689f2..0000000000 --- a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import { Chip } from "@mui/material"; - -export const DeclaredRole = ({ declared_role }: { declared_role?: any }) => ( - <> - {declared_role?.mixnode && <Chip size="small" label="Mixnode" sx={{ mr: 0.5 }} color="info" />} - {declared_role?.entry && <Chip size="small" label="Entry" sx={{ mr: 0.5 }} color="success" />} - {declared_role?.exit_nr && <Chip size="small" label="Exit NR" sx={{ mr: 0.5 }} color="warning" />} - {declared_role?.exit_ipr && <Chip size="small" label="Exit IPR" sx={{ mr: 0.5 }} color="warning" />} - </> -) \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx deleted file mode 100644 index a1b754ccce..0000000000 --- a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, {useMemo} from "react"; -import {MaterialReactTable, MRT_ColumnDef, useMaterialReactTable} from "material-react-table"; -import StyledLink from "../../../components/StyledLink"; -import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; -import {splice} from "@/app/utils"; -import {humanReadableCurrencyToString} from "@/app/utils/currency"; -import {Typography} from "@mui/material"; -import {useTheme} from "@mui/material/styles"; -import WarningIcon from '@mui/icons-material/Warning'; -import { Tooltip } from '@/app/components/Tooltip' - -export const NodeDelegationsTable = ({ node }: { node: any}) => { - const columns = useMemo<MRT_ColumnDef<any>[]>(() => { - return [ - { - id: 'nym-node-delegation-data', - header: 'Nym Node Delegations', - columns: [ - { - id: 'owner', - header: 'Delegator', - accessorKey: 'owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner || "-"}`} - target="_blank" - data-testid="bond_information.node.owner" - color="text.primary" - > - {splice(7, 29, row.original.owner)} - </StyledLink> - ) - }, - }, - { - id: 'amount', - header: 'Amount', - accessorKey: 'amount', - size: 150, - Cell: ({ row }) => ( - <>{humanReadableCurrencyToString(row.original.amount)}</> - ) - }, - { - id: 'height', - header: 'Delegated at height', - accessorKey: 'height', - size: 150, - }, - { - id: 'proxy', - header: 'From vesting account?', - accessorKey: 'proxy', - size: 250, - Cell: ({ row }) => { - if(row.original.proxy?.length) { - return ( - <VestingDelegationWarning>Please re-delegate from your main account</VestingDelegationWarning> - ) - } - } - }, - ] - } - ]; - }, []); - - const table = useMaterialReactTable({ - columns, - data: node ? node.delegations : [], - }); - - return ( - <MaterialReactTable table={table} /> - ); -} - -export const VestingDelegationWarning = ({children, plural}: { plural?: boolean, children: React.ReactNode}) => { - const theme = useTheme(); - return ( - <Tooltip - text={`${plural ? 'These delegations have' : 'This delegation has'} been made with a vesting account. All tokens are liquid, if you are the delegator, please move the tokens into your main account and make the delegation from there.`} - id="delegations" - > - <Typography fontSize="inherit" color={theme.palette.warning.main} display="flex" alignItems="center"> - <WarningIcon sx={{ mr: 0.5 }}/> - {children} - </Typography> - </Tooltip> - ); -} \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx deleted file mode 100644 index 95a172d21e..0000000000 --- a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx +++ /dev/null @@ -1,336 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Alert, AlertTitle, Box, CircularProgress, Grid } from "@mui/material"; -import { useParams } from "next/navigation"; -import { ColumnsType, DetailTable } from "@/app/components/DetailTable"; -import { ComponentError } from "@/app/components/ComponentError"; -import { ContentCard } from "@/app/components/ContentCard"; -import { UptimeChart } from "@/app/components/UptimeChart"; -import { NymNodeContextProvider, useNymNodeContext } from "@/app/context/node"; -import { useMainContext } from "@/app/context/main"; -import { Title } from "@/app/components/Title"; -import Paper from "@mui/material/Paper"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableRow from "@mui/material/TableRow"; -import { humanReadableCurrencyToString } from "@/app/utils/currency"; -import { DeclaredRole } from "@/app/network-components/nodes/DeclaredRole"; -import { - NodeDelegationsTable, - VestingDelegationWarning, -} from "@/app/network-components/nodes/[id]/NodeDelegationsTable"; - -const columns: ColumnsType[] = [ - { - field: "identity_key", - title: "Identity Key", - headerAlign: "left", - width: 230, - }, - { - field: "bond", - title: "Bond", - headerAlign: "left", - }, - { - field: "host", - title: "IP", - headerAlign: "left", - width: 99, - }, - { - field: "location", - title: "Location", - headerAlign: "left", - }, - { - field: "owner", - title: "Owner", - headerAlign: "left", - }, - { - field: "version", - title: "Version", - headerAlign: "left", - }, -]; - -interface NodeEnrichedRowType { - node_id: number; - identity_key: string; - bond: string; - host: string; - location: string; - owner: string; - version: string; -} - -function nodeEnrichedToGridRow(node: any): NodeEnrichedRowType { - return { - node_id: node.node_id, - owner: node.bond_information?.owner || "", - identity_key: node.bond_information?.node?.identity_key || "", - location: node.location?.country_name || "", - bond: node.bond_information?.original_pledge.amount || 0, // TODO: format - host: node.bond_information?.node?.host || "", - version: node.description?.build_information?.build_version || "", - }; -} - -/** - * Shows nym node details - */ -const PageNymNodeDetailsWithState = ({ - selectedNymNode, -}: { - selectedNymNode?: any; -}) => { - const { uptimeHistory } = useNymNodeContext(); - const enrichedData = React.useMemo( - () => (selectedNymNode ? [nodeEnrichedToGridRow(selectedNymNode)] : []), - [] - ); - - const hasVestingContractDelegations = React.useMemo( - () => selectedNymNode?.delegations?.filter((d: any) => d.proxy)?.length, - [selectedNymNode] - ); - - return ( - <Box component="main"> - <Title text="Nym Node Detail" /> - - <Grid container mt={4}> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Node detail table" - rows={enrichedData} - /> - </Grid> - </Grid> - - <Grid container mt={2} spacing={2}> - {selectedNymNode.rewarding_details && ( - <Grid item xs={12} md={4}> - <TableContainer component={Paper}> - <Table> - <TableBody> - <TableRow> - <TableCell colSpan={2}>Delegations and Rewards</TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong>Operator</strong> - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString({ - amount: - selectedNymNode.rewarding_details.operator.split( - "." - )[0], - denom: "unym", - })} - </TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong> - {hasVestingContractDelegations ? ( - <VestingDelegationWarning plural={true}> - Delegates ( - { - selectedNymNode.rewarding_details - .unique_delegations - }{" "} - delegates) - </VestingDelegationWarning> - ) : ( - <> - Delegates ( - { - selectedNymNode.rewarding_details - .unique_delegations - }{" "} - delegates) - </> - )} - </strong> - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString({ - amount: - selectedNymNode.rewarding_details.delegates.split( - "." - )[0], - denom: "unym", - })} - </TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong>Profit margin</strong> - </TableCell> - <TableCell align="right"> - {selectedNymNode.rewarding_details.cost_params - .profit_margin_percent * 100} - % - </TableCell> - </TableRow> - <TableRow> - <TableCell - component="th" - scope="row" - sx={{ color: "inherit" }} - > - <strong>Operator costs</strong> - </TableCell> - <TableCell align="right"> - {humanReadableCurrencyToString( - selectedNymNode.rewarding_details.cost_params - .interval_operating_cost - )} - </TableCell> - </TableRow> - </TableBody> - </Table> - </TableContainer> - </Grid> - )} - - {selectedNymNode.description?.declared_role && ( - <Grid item xs={12} md={4}> - <TableContainer component={Paper}> - <Table> - <TableBody> - <TableRow> - <TableCell colSpan={2}>Node roles</TableCell> - </TableRow> - <TableRow> - <TableCell>Self declared roles</TableCell> - <TableCell> - <DeclaredRole - declared_role={ - selectedNymNode.description?.declared_role - } - /> - </TableCell> - </TableRow> - </TableBody> - </Table> - </TableContainer> - </Grid> - )} - </Grid> - - <Grid container spacing={2} mt={2}> - <Grid item xs={12} md={8}> - {uptimeHistory && ( - <ContentCard title="Routing Score"> - {uptimeHistory.error && ( - <ComponentError text="There was a problem retrieving routing score." /> - )} - <UptimeChart - loading={uptimeHistory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeHistory} - /> - </ContentCard> - )} - </Grid> - </Grid> - - <Box mt={2}> - <NodeDelegationsTable node={selectedNymNode} /> - </Box> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageNymNodeDetailGuard = () => { - const [selectedNode, setSelectedNode] = React.useState<any>(); - const [isLoading, setLoading] = React.useState<boolean>(true); - const [error, setError] = React.useState<string>(); - const { fetchNodeById } = useMainContext(); - const { id } = useParams(); - - React.useEffect(() => { - setSelectedNode(undefined); - setLoading(true); - (async () => { - if (typeof id === "string") { - try { - const res = await fetchNodeById(Number.parseInt(id)); - setSelectedNode(res); - } catch (e: any) { - setError(e.message); - } finally { - setLoading(false); - } - } - })(); - }, [id, fetchNodeById]); - - if (isLoading) { - return <CircularProgress />; - } - - // loaded, but not found - if (error) { - return ( - <Alert severity="warning"> - <AlertTitle>Nym node not found</AlertTitle> - Sorry, we could not find a node with id <code>{id || ""}</code> - </Alert> - ); - } - - if (!selectedNode) { - return ( - <Alert severity="warning"> - <AlertTitle>Legacy Node</AlertTitle> - Unable to load details for the selected Nym node. - </Alert> - ); - } - - return <PageNymNodeDetailsWithState selectedNymNode={selectedNode} />; -}; - -/** - * Wrapper component that adds the node content based on the `id` in the address URL - */ -const PageNymNodeDetail = () => { - const { id } = useParams(); - - if (!id || typeof id !== "string") { - return <Alert severity="error">Oh no! Could not find that node</Alert>; - } - - return ( - <NymNodeContextProvider nymNodeId={id}> - <PageNymNodeDetailGuard /> - </NymNodeContextProvider> - ); -}; - -export default PageNymNodeDetail; diff --git a/explorer-nextjs/app/network-components/nodes/page.tsx b/explorer-nextjs/app/network-components/nodes/page.tsx deleted file mode 100644 index 75e5b97fdd..0000000000 --- a/explorer-nextjs/app/network-components/nodes/page.tsx +++ /dev/null @@ -1,254 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { Box, Card, Grid, Stack, Chip } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -import { diff, gte, rcompare } from 'semver' -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' -import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip' -import { useMainContext } from '@/app/context/main' -import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' -import { Title } from '@/app/components/Title' -import { humanReadableCurrencyToString } from '@/app/utils/currency' -import { Tooltip } from '@/app/components/Tooltip' -import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' -import { splice } from '@/app/utils' - -import StyledLink from '@/app/components/StyledLink' -import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole"; - -function getFlagEmoji(countryCode: string) { - const codePoints = countryCode - .toUpperCase() - .split('') - .map(char => 127397 + char.charCodeAt(0)); - return String.fromCodePoint(...codePoints); -} - -const PageNodes = () => { - const [isLoading, setLoading] = React.useState(true); - const { nodes, fetchNodes } = useMainContext() - - React.useEffect(() => { - (async () => { - try { - await fetchNodes(); - } finally { - setLoading(false); - } - })(); - }, []); - - const columns = useMemo<MRT_ColumnDef<any>[]>(() => { - return [ - { - id: 'nym-node-data', - header: 'Nym Node Data', - columns: [ - { - id: 'node_id', - header: 'Node Id', - accessorKey: 'node_id', - size: 75, - }, - { - id: 'identity_key', - header: 'Identity Key', - accessorKey: 'identity_key', - size: 250, - Cell: ({ row }) => { - return ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={row.original.bond_information.node.identity_key} - tooltip={`Copy identity key ${row.original.bond_information.node.identity_key} to clipboard`} - /> - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - dataTestId="identity-link" - color="text.primary" - > - {splice(7, 29, row.original.bond_information.node.identity_key)} - </StyledLink> - </Stack> - ) - }, - }, - { - id: 'version', - header: 'Version', - accessorKey: 'description.build_information.build_version', - size: 75, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - data-testid="version" - color="text.primary" - > - {row.original.description?.build_information?.build_version || "-"} - </StyledLink> - ) - }, - }, - { - id: 'contract_node_type', - header: 'Kind', - accessorKey: 'contract_node_type', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - data-testid="contract_node_type" - color="text.primary" - > - <code>{row.original.contract_node_type || "-"}</code> - </StyledLink> - ) - }, - }, - { - id: 'declared_role', - header: 'Declare Role', - accessorKey: 'description.declared_role', - size: 250, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="declared_role-button" - > - <DeclaredRole declared_role={row.original.description?.declared_role}/> - </Box> - ) - }, - }, - { - id: 'total_stake', - header: 'Total Stake', - accessorKey: 'description.total_stake', - size: 250, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="total_stake-button" - > - {humanReadableCurrencyToString({ amount: row.original.total_stake || 0, denom: "unym" })} - </Box> - ) - }, - }, - { - id: 'location', - header: 'Location', - accessorKey: 'location.country_name', - size: 75, - Cell: ({ row }) => { - return ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip - text={row.original.location?.country_name || "-"} - id="nym-node-location-text" - > - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {row.original.location?.country_name ? <>{getFlagEmoji(row.original.location.two_letter_iso_country_code.toUpperCase())} {row.original.location.two_letter_iso_country_code}</> : <>-</> } - </Box> - </Tooltip> - </Box> - ) - }, - }, - { - id: 'host', - header: 'IP', - accessorKey: 'bond_information.node.host', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/nodes/${row.original.node_id}`} - data-testid="host" - color="text.primary" - > - {row.original.bond_information?.node?.host || "-"} - </StyledLink> - ) - }, - }, - { - id: 'owner', - header: 'Owner', - accessorKey: 'bond_information.owner', - size: 150, - Cell: ({ row }) => { - return ( - <StyledLink - to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.bond_information?.owner || "-"}`} - target="_blank" - data-testid="bond_information.node.owner" - color="text.primary" - > - {splice(7, 29, row.original.bond_information?.owner)} - </StyledLink> - ) - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: nodes?.data || [], - state: { - isLoading, - showLoadingOverlay: isLoading, - }, - initialState: { - isLoading: true, - showLoadingOverlay: true, - } - }) - - return ( - <> - <Box mb={2}> - <Title text="Nym Nodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <MaterialReactTable table={table} /> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default PageNodes diff --git a/explorer-nextjs/app/nodemap/page.tsx b/explorer-nextjs/app/nodemap/page.tsx deleted file mode 100644 index 674ae57a5e..0000000000 --- a/explorer-nextjs/app/nodemap/page.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { - Alert, - Box, - CircularProgress, - Grid, - SelectChangeEvent, - Typography, -} from '@mui/material' -import { ContentCard } from '@/app/components/ContentCard' -import { TableToolbar } from '@/app/components/TableToolbar' -import { Title } from '@/app/components/Title' -import { WorldMap } from '@/app/components/WorldMap' -import { useMainContext } from '@/app/context/main' -import { CountryDataRowType, countryDataToGridRow } from '@/app/utils' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' - -const PageMixnodesMap = () => { - const { countryData } = useMainContext() - - const data = useMemo(() => { - return countryDataToGridRow(Object.values(countryData?.data || {})) - }, [countryData]) - - const columns = useMemo<MRT_ColumnDef<CountryDataRowType>[]>(() => { - return [ - { - id: 'delegations-data', - header: 'Global Mixnodes Data', - columns: [ - { - id: 'country-name', - header: 'Location', - accessorKey: 'countryName', - }, - { - id: 'nodes', - header: 'Nodes', - accessorKey: 'nodes', - }, - { - id: 'percentage', - header: 'Percentage', - accessorKey: 'percentage', - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data, - }) - - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item data-testid="mixnodes-globe"> - <Title text="Mixnodes Around the Globe" /> - </Grid> - <Grid item> - <Grid container spacing={2}> - <Grid item xs={12}> - <ContentCard title="Distribution of nodes"> - <WorldMap loading={false} countryData={countryData} /> - <Box sx={{ marginTop: 2 }} /> - <TableToolbar /> - <MaterialReactTable table={table} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ) -} - -export default PageMixnodesMap diff --git a/explorer-nextjs/app/page.tsx b/explorer-nextjs/app/page.tsx deleted file mode 100644 index 2c6f9f781c..0000000000 --- a/explorer-nextjs/app/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client' - -import React, { useEffect } from 'react' -import { Box, Grid, Link, Typography } from '@mui/material' -import { useTheme } from '@mui/material/styles' -import OpenInNewIcon from '@mui/icons-material/OpenInNew' -import { PeopleAlt } from '@mui/icons-material' -import { Title } from '@/app/components/Title' -import { StatsCard } from '@/app/components/StatsCard' -import { MixnodesSVG } from '@/app/icons/MixnodesSVG' -import { Icons } from '@/app/components/Icons' -import { GatewaysSVG } from '@/app/icons/GatewaysSVG' -import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG' -import { ContentCard } from '@/app/components/ContentCard' -import { WorldMap } from '@/app/components/WorldMap' -import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' -import { formatNumber } from '@/app/utils' -import { useMainContext } from './context/main' -import { useRouter } from 'next/navigation' - -const PageOverview = () => { - const theme = useTheme() - const router = useRouter() - - const { - summaryOverview, - gateways, - validators, - block, - countryData, - serviceProviders, - } = useMainContext() - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item paddingBottom={3}> - <Title text="Overview" /> - </Grid> - <Grid item> - <Grid container spacing={3}> - {summaryOverview && ( - <> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Nodes" - icon={<MixnodesSVG />} - count={summaryOverview.data?.nymnodes?.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - </> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Mixnodes" - count={summaryOverview.data?.nymnodes?.roles?.mixnode || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Entry Gateways" - count={summaryOverview.data?.nymnodes?.roles?.entry || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="Exit Gateways" - count={summaryOverview.data?.nymnodes?.roles?.exit_ipr || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {summaryOverview && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => router.push('/network-components/nodes')} - title="SOCKS5 Network Requesters" - count={summaryOverview.data?.nymnodes?.roles?.exit_nr || ''} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {validators && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => window.open(`${BLOCK_EXPLORER_BASE_URL}/validators`)} - title="Validators" - count={validators?.data?.count || ''} - errorMsg={validators?.error} - icon={<ValidatorsSVG />} - /> - </Grid> - )} - {block?.data && ( - <Grid item xs={12}> - <Link - href={`${BLOCK_EXPLORER_BASE_URL}/blocks`} - target="_blank" - rel="noreferrer" - underline="none" - color="inherit" - marginY={2} - paddingX={3} - paddingY={0.25} - fontSize={14} - fontWeight={600} - display="flex" - alignItems="center" - > - <Typography fontWeight="inherit" fontSize="inherit"> - Current block height is {formatNumber(block.data)} - </Typography> - <OpenInNewIcon - fontWeight="inherit" - fontSize="inherit" - sx={{ ml: 0.5 }} - /> - </Link> - </Grid> - )} - <Grid item xs={12}> - <ContentCard title="Distribution of nodes around the world"> - <WorldMap loading={false} countryData={countryData} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ) -} - -export default PageOverview diff --git a/explorer-nextjs/app/providers/index.tsx b/explorer-nextjs/app/providers/index.tsx deleted file mode 100644 index 346d2fc88e..0000000000 --- a/explorer-nextjs/app/providers/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import CosmosKitProvider from '@/app/context/cosmos-kit' -import { WalletProvider } from '@/app/context/wallet' -import { NetworkExplorerThemeProvider } from '@/app/theme' -import { MainContextProvider } from '@/app/context/main' - -const Providers = ({ children }: { children: React.ReactNode }) => { - return ( - <MainContextProvider> - <NetworkExplorerThemeProvider> - <CosmosKitProvider> - <WalletProvider>{children}</WalletProvider> - </CosmosKitProvider> - </NetworkExplorerThemeProvider> - </MainContextProvider> - ) -} - -export { Providers } diff --git a/explorer-nextjs/app/theme/index.tsx b/explorer-nextjs/app/theme/index.tsx deleted file mode 100644 index fd072cfc64..0000000000 --- a/explorer-nextjs/app/theme/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import * as React from 'react' -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme' -import { useMainContext } from '../context/main' - -export const NetworkExplorerThemeProvider: FCWithChildren = ({ children }) => { - const { mode } = useMainContext() - - return ( - <NymNetworkExplorerThemeProvider mode={mode}> - {children} - </NymNetworkExplorerThemeProvider> - ) -} diff --git a/explorer-nextjs/app/theme/mui-theme.d.ts b/explorer-nextjs/app/theme/mui-theme.d.ts deleted file mode 100644 index 6d5f9aabf1..0000000000 --- a/explorer-nextjs/app/theme/mui-theme.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme { } - interface ThemeOptions extends Partial<NymTheme> { } - interface Palette extends NymPaletteWithExtensions { } - interface PaletteOptions extends NymPaletteWithExtensionsOptions { } -} diff --git a/explorer-nextjs/app/typeDefs/explorer-api.ts b/explorer-nextjs/app/typeDefs/explorer-api.ts deleted file mode 100644 index b15490008d..0000000000 --- a/explorer-nextjs/app/typeDefs/explorer-api.ts +++ /dev/null @@ -1,294 +0,0 @@ -/* eslint-disable camelcase */ - -export interface ClientConfig { - url: string; - version: string; -} - -export interface SummaryOverviewResponse { - mixnodes: { - count: number; - activeset: { - active: number; - standby: number; - inactive: number; - }; - }; - gateways: { - count: number; - }; - validators: { - count: number; - }; - nymnodes: { - count: number; - roles: { - mixnode: number; - entry: number; - exit_nr: number; - exit_ipr: number; - }; - }; -} - -export interface MixNode { - host: string; - mix_port: number; - http_api_port: number; - verloc_port: number; - sphinx_key: string; - identity_key: string; - version: string; - location: string; -} - -export interface Gateway { - host: string; - mix_port: number; - clients_port: number; - location: string; - sphinx_key: string; - identity_key: string; - version: string; -} - -export interface Amount { - denom: string; - amount: number; -} - -export enum MixnodeStatus { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set -} - -export enum MixnodeStatusWithAll { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set - all = 'all', // any status -} - -export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | undefined => { - if (!status || status === MixnodeStatusWithAll.all) { - return undefined; - } - return status as unknown as MixnodeStatus; -}; - -export interface MixNodeResponseItem { - mix_id: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - layer: string; - status: MixnodeStatus; - location: { - country_name: string; - latitude?: number; - longitude?: number; - three_letter_iso_country_code: string; - two_letter_iso_country_code: string; - }; - mix_node: MixNode; - avg_uptime: number; - node_performance: NodePerformance; - stake_saturation: number; - uncapped_saturation: number; - operating_cost: Amount; - profit_margin_percent: string; - blacklisted: boolean; -} - -export type MixNodeResponse = MixNodeResponseItem[]; - -export interface MixNodeReportResponse { - identity: string; - owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; -} - -export interface StatsResponse { - update_time: Date; - previous_update_time: Date; - packets_received_since_startup: number; - packets_sent_since_startup: number; - packets_explicitly_dropped_since_startup: number; - packets_received_since_last_update: number; - packets_sent_since_last_update: number; - packets_explicitly_dropped_since_last_update: number; -} - -export interface NodePerformance { - most_recent: string; - last_hour: string; - last_24h: string; -} - -export type MixNodeHistoryResponse = StatsResponse; - -export interface GatewayBond { - block_height: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - gateway: Gateway; - node_performance: NodePerformance; - location?: Location; -} - -export interface GatewayBondAnnotated { - gateway_bond: GatewayBond; - node_performance: NodePerformance; -} - -export interface Location { - two_letter_iso_country_code: string; - three_letter_iso_country_code: string; - country_name: string; - latitude?: number; - longitude?: number; -} - -export interface LocatedGateway { - pledge_amount: Amount; - owner: string; - block_height: number; - gateway: Gateway; - proxy?: string; - location?: Location; -} - -export type GatewayResponse = LocatedGateway[]; - -export interface NymNodeReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export interface GatewayReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export type GatewayHistoryResponse = StatsResponse; - -export interface MixNodeDescriptionResponse { - name: string; - description: string; - link: string; - location: string; -} - -export type MixNodeStatsResponse = StatsResponse; - -export interface Validator { - address: string; - proposer_priority: string; - pub_key: { - type: string; - value: string; - }; -} -export interface ValidatorsResponse { - block_height: number; - count: string; - total: string; - validators: Validator[]; -} - -export type CountryData = { - ISO3: string; - nodes: number; -}; - -export type Delegation = { - owner: string; - amount: Amount; - block_height: number; -}; - -export type DelegationUniq = { - owner: string; - amount: Amount; -}; - -export type DelegationsResponse = Delegation[]; - -export type UniqDelegationsResponse = DelegationUniq[]; - -export interface CountryDataResponse { - [threeLetterCountryCode: string]: CountryData; -} - -export type BlockType = number; -export type BlockResponse = BlockType; - -export interface ApiState<RESPONSE> { - isLoading: boolean; - data?: RESPONSE; - error?: Error; -} - -export type StatusResponse = { - pending: boolean; - ports: { - 1789: boolean; - 1790: boolean; - 8000: boolean; - }; -}; - -export type UptimeTime = { - date: string; - uptime: number; -}; - -export type UptimeStoryResponse = { - history: UptimeTime[]; - identity: string; - owner: string; -}; - -export type MixNodeEconomicDynamicsStatsResponse = { - stake_saturation: number; - uncapped_saturation: number; - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - active_set_inclusion_probability: 'High' | 'Good' | 'Low'; - reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; - estimated_total_node_reward: number; - estimated_operator_reward: number; - estimated_delegators_reward: number; - current_interval_uptime: number; -}; - -export type Environment = 'mainnet' | 'sandbox' | 'qa'; - -export type ServiceProviderType = 'Network Requester'; - -export type DirectoryServiceProvider = { - id: string; - description: string; - address: string; - gateway: string; - routing_score: string | null; - service_type: ServiceProviderType; -}; - -export type DirectoryService = { - id: string; - description: string; - items: DirectoryServiceProvider[]; -}; diff --git a/explorer-nextjs/app/typeDefs/filters.ts b/explorer-nextjs/app/typeDefs/filters.ts deleted file mode 100644 index 53de60cb87..0000000000 --- a/explorer-nextjs/app/typeDefs/filters.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Mark } from '@mui/base'; - -export enum EnumFilterKey { - profitMargin = 'profitMargin', - stakeSaturation = 'stakeSaturation', - routingScore = 'routingScore', -} - -export type TFilterItem = { - label: string; - id: EnumFilterKey; - value: number[]; - isSmooth?: boolean; - marks: Mark[]; - min?: number; - max?: number; - scale?: (value: number) => number; - tooltipInfo?: string; -}; - -export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/explorer-nextjs/app/typeDefs/network.ts b/explorer-nextjs/app/typeDefs/network.ts deleted file mode 100644 index f8615cd992..0000000000 --- a/explorer-nextjs/app/typeDefs/network.ts +++ /dev/null @@ -1 +0,0 @@ -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/explorer-nextjs/app/typeDefs/tables.ts b/explorer-nextjs/app/typeDefs/tables.ts deleted file mode 100644 index 1ab2ffead4..0000000000 --- a/explorer-nextjs/app/typeDefs/tables.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TableHeading = { - id: string; - numeric: boolean; - disablePadding: boolean; - label: string; -}; - -export type TableHeadingsType = TableHeading[]; diff --git a/explorer-nextjs/app/typings/FC.d.ts b/explorer-nextjs/app/typings/FC.d.ts deleted file mode 100644 index 08ebdfa298..0000000000 --- a/explorer-nextjs/app/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>; diff --git a/explorer-nextjs/app/typings/jpeg.d.ts b/explorer-nextjs/app/typings/jpeg.d.ts deleted file mode 100644 index af2ed72913..0000000000 --- a/explorer-nextjs/app/typings/jpeg.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} - -declare module '*.jpg' { - const value: any; - export default value; -} diff --git a/explorer-nextjs/app/typings/json.d.ts b/explorer-nextjs/app/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/explorer-nextjs/app/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/typings/png.d.ts b/explorer-nextjs/app/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/explorer-nextjs/app/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/typings/react-identicons.d.ts b/explorer-nextjs/app/typings/react-identicons.d.ts deleted file mode 100644 index 6c460eff99..0000000000 --- a/explorer-nextjs/app/typings/react-identicons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'react-identicons' { - import * as React from 'react'; - - interface IdenticonProps { - string: string; - size?: number; - padding?: number; - bg?: string; - fg?: string; - palette?: string[]; - count?: number; - // getColor: Function; - } - - declare function Identicon(props: IdenticonProps): React.ReactElement<IdenticonProps>; - - export default Identicon; -} diff --git a/explorer-nextjs/app/typings/react-tooltip.d.ts b/explorer-nextjs/app/typings/react-tooltip.d.ts deleted file mode 100644 index 98cb6d592d..0000000000 --- a/explorer-nextjs/app/typings/react-tooltip.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-tooltip'; diff --git a/explorer-nextjs/app/typings/svg.d.ts b/explorer-nextjs/app/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/explorer-nextjs/app/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/explorer-nextjs/app/utils/currency.ts b/explorer-nextjs/app/utils/currency.ts deleted file mode 100644 index 07bdc890de..0000000000 --- a/explorer-nextjs/app/utils/currency.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { printableCoin } from '@nymproject/nym-validator-client'; -import Big from 'big.js'; -import { DecCoin, isValidRawCoin } from '@nymproject/types'; - -const DENOM = process.env.CURRENCY_DENOM || 'unym'; -const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; - -export const toDisplay = (val: string | number | Big, dp = 4) => { - let displayValue; - try { - displayValue = Big(val).toFixed(dp); - } catch (e: any) { - console.warn(`${displayValue} not a valid decimal number: ${e}`); - } - return displayValue; -}; - -export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string; dp?: number; denom?: string }) => { - if (!dp) { - printableCoin({ - amount, - denom, - }); - } - - const [printableAmount, printableDenom] = printableCoin({ - amount, - denom, - }).split(/\s+/); - - return `${toDisplay(printableAmount, dp)} ${printableDenom}`; -}; - -function addThousandsSeparator(value: string) { - return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); -} - -export const humanReadableCurrencyToString = ({ amount, denom }: { amount: string, denom: string }) => { - const str = currencyToString({ amount, denom, dp: 2 }); - const parts = str.split('.'); - return [addThousandsSeparator(parts[0]), parts[1]].join('.'); -} - -export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => - printableCoin({ - amount, - denom, - }); - -/** - * Converts a decimal number to a pretty representation - * with fixed decimal places. - * - * @param val - a decimal number of string form - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number - */ - -/** - * Converts a decimal number of μNYM (micro NYM) to NYM. - * - * @param unym - a decimal number of μNYM - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns The corresponding decimal number in NYM - */ -export const unymToNym = (unym: string | number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return nym; -}; - -export const validateAmount = async ( - majorAmountAsString: DecCoin['amount'], - minimumAmountAsString: DecCoin['amount'], -): Promise<boolean> => { - // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc - if (!Number(majorAmountAsString)) { - return false; - } - - if (!isValidRawCoin(majorAmountAsString)) { - return false; - } - - const majorValueFloat = parseInt(majorAmountAsString, Number(10)); - - return majorValueFloat >= parseInt(minimumAmountAsString, Number(10)); -}; - -/** - * Takes a DecCoin and prettify its amount to a representation - * with fixed decimal places. - * - * @param coin - a DecCoin - * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) - * @returns A DecCoin with prettified amount - */ -export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { - const displayCoin = { ...coin }; - try { - displayCoin.amount = Big(coin.amount).toFixed(dp); - } catch (e: any) { - console.warn(`${coin.amount} not a valid decimal number: ${e}`); - } - return displayCoin; -}; diff --git a/explorer-nextjs/app/utils/index.ts b/explorer-nextjs/app/utils/index.ts deleted file mode 100644 index e3c3e4ae19..0000000000 --- a/explorer-nextjs/app/utils/index.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* eslint-disable camelcase */ -import { MutableRefObject } from 'react'; -import { Theme } from '@mui/material/styles'; -import { registerLocale, getName } from 'i18n-iso-countries'; -import Big from 'big.js'; -import { CountryData } from '@/app/typeDefs/explorer-api'; -import { EconomicsRowsType } from '@/app/components/MixNodes/Economics/types'; -import { Network } from '../typeDefs/network'; - -registerLocale(require('i18n-iso-countries/langs/en.json')); - -export function formatNumber(num: number): string { - return new Intl.NumberFormat().format(num); -} - -export function scrollToRef(ref: MutableRefObject<HTMLDivElement | undefined>): void { - if (ref?.current) ref.current.scrollIntoView(); -} - -export type CountryDataRowType = { - id: number; - ISO3: string; - nodes: number; - countryName: string; - percentage: string; -}; - -export function countryDataToGridRow(countriesData: CountryData[]): CountryDataRowType[] { - const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0); - const formatted = countriesData.map((each: CountryData, index: number) => { - const updatedCountryRecord: CountryDataRowType = { - ...each, - id: index, - countryName: getName(each.ISO3, 'en', { select: 'alias' }), - percentage: ((each.nodes * 100) / totalNodes).toFixed(1), - }; - return updatedCountryRecord; - }); - - const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1)); - return sorted; -} - -export const splice = (start: number, deleteCount: number, address?: string): string => { - if (address) { - const array = address.split(''); - array.splice(start, deleteCount, '...'); - return array.join(''); - } - return ''; -}; - -export const trimAddress = (address = '', trimBy = 6) => `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`; - -/** - * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). - * - * @param value - the percentage to convert - * @returns A stringified integer - */ -export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); -export const toPercentInteger = (value: string) => Math.round(Number(value) * 100); - -export const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { - const progressBarValue = value?.progressBarValue || 0; - const fieldValue = value.value; - - if (progressBarValue > 100) { - return theme.palette.warning.main; - } - if (field === 'selectionChance') { - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - switch (fieldValue) { - case 'High': - case 'VeryHigh': - return theme.palette.nym.networkExplorer.selectionChance.overModerate; - case 'Good': - case 'Moderate': - return theme.palette.nym.networkExplorer.selectionChance.moderate; - case 'Low': - case 'VeryLow': - return theme.palette.nym.networkExplorer.selectionChance.underModerate; - default: - return theme.palette.nym.wallet.fee; - } - } - return theme.palette.nym.wallet.fee; -}; - -export const isGreaterThan = (a: number, b: number) => a > b; - -export const isLessThan = (a: number, b: number) => a < b; - -/** - * - * Checks if the user's balance is enough to pay the fee - * @param balance - The user's current balance - * @param fee - The fee for the tx - * @param tx - The amount of the tx - * @returns boolean - * - */ - -export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - console.log('balance', balance, fee, tx); - try { - return Big(balance).gte(Big(fee).plus(Big(tx))); - } catch (e) { - console.log(e); - return false; - } -}; - -export const urls = (networkName?: Network) => - networkName === 'MAINNET' - ? { - mixnetExplorer: 'https://mixnet.explorers.guru/', - blockExplorer: 'https://blocks.nymtech.net', - networkExplorer: 'https://explorer.nymtech.net', - } - : { - blockExplorer: `https://${networkName}-blocks.nymtech.net`, - networkExplorer: `https://${networkName}-explorer.nymtech.net`, - }; diff --git a/explorer-nextjs/next.config.mjs b/explorer-nextjs/next.config.mjs deleted file mode 100644 index d642290145..0000000000 --- a/explorer-nextjs/next.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - async redirects() { - return [ - { - source: '/network-components/mixnode/:id', // Match the old URL - destination: '/network-components/nodes/:id', // Redirect to the new URL - permanent: true, - }, - ]; - }, -}; - -export default nextConfig; diff --git a/explorer-nextjs/package.json b/explorer-nextjs/package.json deleted file mode 100644 index 2f60c7bb87..0000000000 --- a/explorer-nextjs/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@nymproject/network-explorer", - "version": "1.0.0", - "scripts": { - "dev": "next dev", - "build": "next build", - "build:prod": "yarn --cwd .. build && next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@nymproject/react": "^1.0.0", - "@nymproject/nym-validator-client": "0.18.0", - "next": "14.2.21", - "react": "^18", - "react-dom": "^18", - "react-error-boundary": "^4.0.13", - "material-react-table": "^2.12.1", - "@mui/x-date-pickers": "7.1.1", - "@mui/x-data-grid": "7.1.1", - "@mui/x-charts": "^7.22.3" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "eslint": "^8", - "eslint-config-next": "14.1.4", - "typescript": "^5" - } -} diff --git a/explorer-nextjs/public/favicon.ico b/explorer-nextjs/public/favicon.ico deleted file mode 100644 index bc951a1c6f..0000000000 Binary files a/explorer-nextjs/public/favicon.ico and /dev/null differ diff --git a/explorer-nextjs/public/next.svg b/explorer-nextjs/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/explorer-nextjs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file diff --git a/explorer-nextjs/public/vercel.svg b/explorer-nextjs/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/explorer-nextjs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> \ No newline at end of file diff --git a/explorer-v2/next.config.js b/explorer-v2/next.config.js new file mode 100644 index 0000000000..ba18864d7e --- /dev/null +++ b/explorer-v2/next.config.js @@ -0,0 +1,24 @@ +// @ts-check + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + + basePath: "/explorer", + assetPrefix: "/explorer", + trailingSlash: false, + + async redirects() { + return [ + // Change the basePath to /explorer + { + source: "/", + destination: "/explorer", + basePath: false, + permanent: true, + }, + ]; + }, +}; + +module.exports = nextConfig \ No newline at end of file diff --git a/explorer-v2/next.config.ts b/explorer-v2/next.config.ts deleted file mode 100644 index 02e12b3e1c..0000000000 --- a/explorer-v2/next.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - reactStrictMode: true, - - basePath: "/explorer", - assetPrefix: "/explorer", - trailingSlash: false, - - async redirects() { - return [ - // Change the basePath to /explorer - { - source: "/", - destination: "/explorer", - basePath: false, - permanent: true, - }, - ]; - }, -}; - -export default nextConfig; diff --git a/explorer-v2/package.json b/explorer-v2/package.json index c415692754..08eaf9b4ed 100644 --- a/explorer-v2/package.json +++ b/explorer-v2/package.json @@ -41,7 +41,7 @@ "i18next-resources-to-backend": "^1.2.1", "isomorphic-dompurify": "^2.21.0", "material-react-table": "^3.0.3", - "next": "^15.2.0", + "next": "^14.2.26", "openapi-fetch": "^0.13.4", "qrcode.react": "^4.1.0", "qs": "^6.14.0", diff --git a/explorer/.babelrc b/explorer/.babelrc deleted file mode 100644 index 85e1f1ba26..0000000000 --- a/explorer/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/env", "@babel/react"] -} \ No newline at end of file diff --git a/explorer/.editorconfig b/explorer/.editorconfig deleted file mode 100644 index c35daeee9b..0000000000 --- a/explorer/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# See http://editorconfig.org/ -# EditorConfig helps developers define and maintain consistent coding styles -# between different editors and IDEs. The EditorConfig project consists of a -# file format for defining coding styles and a collection of text editor plugins -# that enable editors to read the file format and adhere to defined styles. -# EditorConfig files are easily readable and they work nicely with version -# control systems. - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/explorer/.env.dev b/explorer/.env.dev deleted file mode 100644 index 3cc86ae00c..0000000000 --- a/explorer/.env.dev +++ /dev/null @@ -1,9 +0,0 @@ -# When running the explorer API locally -#EXPLORER_API_URL=http://localhost:8000/v1 - -EXPLORER_API_URL=https://sandbox-explorer.nymtech.net/api/v1 -NYM_API_URL=https://sandbox-nym-api1.nymtech.net -VALIDATOR_URL=https://rpc.sandbox.nymtech.net -BIG_DIPPER_URL=https://sandbox-blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.env.prod b/explorer/.env.prod deleted file mode 100644 index 2e927b2947..0000000000 --- a/explorer/.env.prod +++ /dev/null @@ -1,6 +0,0 @@ -EXPLORER_API_URL=https://explorer.nymtech.net/api/v1 -NYM_API_URL=https://validator.nymtech.net -VALIDATOR_URL=https://rpc.nymtech.net -BIG_DIPPER_URL=https://blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx \ No newline at end of file diff --git a/explorer/.env.qa b/explorer/.env.qa deleted file mode 100644 index dd7052ce4d..0000000000 --- a/explorer/.env.qa +++ /dev/null @@ -1,6 +0,0 @@ -EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1 -NYM_API_URL=https://qa-validator-api.nymtech.net -VALIDATOR_URL=https://qa-validator.nymtech.net -BIG_DIPPER_URL=https://qa-blocks.nymtech.net -CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.eslintrc.js b/explorer/.eslintrc.js deleted file mode 100644 index 5feba99084..0000000000 --- a/explorer/.eslintrc.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - extends: [ - '@nymproject/eslint-config-react-typescript' - ], - overrides: [ - { - files: ['*.ts'], - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - } - } - ] -} diff --git a/explorer/.gitignore b/explorer/.gitignore deleted file mode 100644 index b8b1827577..0000000000 --- a/explorer/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -dist -.DS_Store -detailAPI.md -!.env.dev diff --git a/explorer/.nvmrc b/explorer/.nvmrc deleted file mode 100644 index 3c032078a4..0000000000 --- a/explorer/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -18 diff --git a/explorer/.prettierrc b/explorer/.prettierrc deleted file mode 100644 index ccf75b89de..0000000000 --- a/explorer/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "all", - "singleQuote": true, - "printWidth": 120, - "tabWidth": 2 -} diff --git a/explorer/.storybook/main.js b/explorer/.storybook/main.js deleted file mode 100644 index 1c2a975321..0000000000 --- a/explorer/.storybook/main.js +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-param-reassign */ -const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); - -module.exports = { - stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: '@storybook/react', - core: { - builder: 'webpack5', - }, - // webpackFinal: async (config, { configType }) => { - // // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' - // // You can change the configuration based on that. - // // 'PRODUCTION' is used when building the static version of storybook. - webpackFinal: async (config) => { - config.module.rules.forEach((rule) => { - // look for SVG import rule and replace - // NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/ - if (rule.test?.toString().includes('svg')) { - rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/; - } - }); - - // handle asset loading with this - config.module.rules.unshift({ - test: /\.svg(\?.*)?$/i, - issuer: /\.[jt]sx?$/, - use: ['@svgr/webpack'], - }); - - config.resolve.extensions = ['.tsx', '.ts', '.js']; - config.resolve.plugins = [new TsconfigPathsPlugin()]; - - config.resolve.fallback = { - fs: false, - tls: false, - path: false, - http: false, - https: false, - stream: false, - crypto: false, - net: false, - zlib: false, - }; - - config.plugins.push(new ForkTsCheckerWebpackPlugin({ - typescript: { - mode: 'write-references', - diagnosticOptions: { - semantic: true, - syntactic: true, - }, - }, - })); - - // Return the altered config - return config; - }, - features: { - emotionAlias: false, - }, -}; diff --git a/explorer/.storybook/preview.js b/explorer/.storybook/preview.js deleted file mode 100644 index 5d97f3b5ab..0000000000 --- a/explorer/.storybook/preview.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable react/react-in-jsx-scope */ -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme'; -import { Box } from '@mui/material'; - -export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, - }, - }, -} - -const withThemeProvider = (Story, context) => ( - <div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}> - <div> - <NymNetworkExplorerThemeProvider mode="light"> - <Box - p={4} - sx={{ - display: 'grid', - gridTemplateRows: '80vh 2rem', - background: (theme) => theme.palette.background.default, - color: (theme) => theme.palette.text.primary, - }} - > - <Box sx={{ overflowY: 'auto' }}> - <Story {...context} /> - </Box> - <h4 style={{ textAlign: 'center' }}>Light mode</h4> - </Box> - </NymNetworkExplorerThemeProvider> - </div> - <div> - <NymNetworkExplorerThemeProvider mode="dark"> - <Box - p={4} - sx={{ - display: 'grid', - gridTemplateRows: '80vh 2rem', - background: (theme) => theme.palette.background.default, - color: (theme) => theme.palette.text.primary, - }} - > - <Box sx={{ overflowY: 'auto' }}> - <Story {...context} /> - </Box> - <h4 style={{ textAlign: 'center' }}>Dark mode</h4> - </Box> - </NymNetworkExplorerThemeProvider> - </div> - </div> -); - -export const decorators = [withThemeProvider]; diff --git a/explorer/.vscode/settings.json b/explorer/.vscode/settings.json deleted file mode 100644 index 246e9c6e89..0000000000 --- a/explorer/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - } -} diff --git a/explorer/CHANGELOG.md b/explorer/CHANGELOG.md deleted file mode 100644 index 8446a3c5b2..0000000000 --- a/explorer/CHANGELOG.md +++ /dev/null @@ -1,47 +0,0 @@ -## UNRELEASED - -- nothing yet - -## [nym-explorer-v1.0.6](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.6) (2023-02-21) - -- Allow user to filter gateways but current and old versions -- Add copy buttons to gateway identity keys - -## [nym-explorer-v1.0.5](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.5) (2023-02-14) - -- NE - link `Owner` field on the node detail page to the account details on NG explorer ([#2923]) -- NE - Upgrade Sandbox and make below changes: ([#2332]) - -[#2923]: https://github.com/nymtech/nym/issues/2923 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.4](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.4) (2023-01-31) - -- Add routing score on gateway list ([#2913]) -- Add gateway's last Routing Score to the gateways list page ([#2186]) -- Upgrade Sandbox and make below changes: ([#2332]) - -[#2913]: https://github.com/nymtech/nym/pull/2913 -[#2186]: https://github.com/nymtech/nym/issues/2186 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.3](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.3) (2023-01-24) - -- Stake Saturation tooltip on node list and node pages updated ([#2877]) -- Sandbox Upgrade : Name change from "Network Explorer" to "Sandbox Explorer", button to mainnet explorer and link to faucet ([#2332)]) - -[#2877]: https://github.com/nymtech/nym/issues/2877 -[#2332]: https://github.com/nymtech/nym/issues/2332 - -## [nym-explorer-v1.0.2](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.2) (2023-01-17) - -- changing the explorers guru link ([#2820]) - -[#2820]: https://github.com/nymtech/nym/pull/2820 - -## [nym-explorer-v1.0.1](https://github.com/nymtech/nym/tree/nym-explorer-v1.0.1) (2023-01-10) - -- Feat/2161 ne gate version by @gala1234 in https://github.com/nymtech/nym/pull/2743 -- fix(explorer): set gateway bond 6 decimals by @doums in https://github.com/nymtech/nym/pull/2741 -- Feat/2130 tables update rebase by @gala1234 in https://github.com/nymtech/nym/pull/2742 -- fix(explorer,explorer-api): mixnode location by @doums in https://github.com/nymtech/nym-api diff --git a/explorer/README.md b/explorer/README.md deleted file mode 100644 index a7bbc2346a..0000000000 --- a/explorer/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Nym Network Explorer - -The network explorer lets you explore the Nym network. - -## Getting started - -You will need: - -- NodeJS (use `nvm install` to automatically install the correct version) -- `npm` -- `yarn` - -> **Note**: This project ipart of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them. - -From the [root of the repository](../README.md) run the following to build shared packages: - -``` -yarn -yarn build -``` - -From the `explorer` directory of the `nym` monorepo, run: - -``` -cd explorer -yarn start -``` - -You can then open a browser to http://localhost:3000 and start development. - -## Development - -Documentation for developers [can be found here](./docs). - -Several environment variables are required. They can be -provisioned via a `.env` file. For convenience a `.env.dev` is -provided, just copy its content into `.env`. - -#### Required env vars - -``` -EXPLORER_API_URL -NYM_API_URL -VALIDATOR_URL -BIG_DIPPER_URL -CURRENCY_DENOM -CURRENCY_STAKING_DENOM -``` - -## Deployment - -Build the UI with (starting in the repository root): - -``` -yarn -yarn build -cd explorer -yarn build -``` - -The output will be in the `dist` directory. Serve this with `nginx` or `httpd`. - -Make sure you have built the [explorer-api](./explorer-api) and are running it as a service proxied through -`nginx` or `httpd` so that both the UI and API are running on the same host. - -## License - -Please see the [project README](./README.md) and the [LICENSES directory](../LICENSES) for license details for all Nym software. - -## Contributing - -If you would like to contribute to the Network Explorer send us a PR or -[raise an issue on GitHub](https://github.com/nymtech/nym/issues) and tag them with `network-explorer`. - -## Development - -Please see [development docs](./docs) here for more information on the structure and design of this app. diff --git a/explorer/docs/README.md b/explorer/docs/README.md deleted file mode 100644 index bb0d0be99a..0000000000 --- a/explorer/docs/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Nym Network Explorer - Development Docs - -## Getting started - -You will need: - -- NodeJS -- `nvm` - -We use the following: - -- Typescript -- `eslint` -- `webpack` -- `jest` -- `react-material-ui` -- `react` 17 - -## Development mode - -Copy the `.env.prod` file to `.env` to configure your environment. Using the live sandbox Explorer API is the best way to do development, so the prod settings are good. - -Run the following: - -``` -npm install -npm run start -``` - -A development server with hot reloading will be running on http://localhost:3000. - -## Linting - -`eslint` and `prettier` are configured. - -You can lint the code by running: - -``` -npm run lint -``` - -> **Note:** this will only show linting errors and will not fix them - -To fix all linting errors automatically run: - -``` -npm run lint:fix -``` diff --git a/explorer/jest.config.js b/explorer/jest.config.js deleted file mode 100644 index e86e13bab9..0000000000 --- a/explorer/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; diff --git a/explorer/package.json b/explorer/package.json deleted file mode 100644 index d2426f6122..0000000000 --- a/explorer/package.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "name": "@nym/network-explorer", - "version": "1.0.7", - "private": true, - "license": "Apache-2.0", - "scripts": { - "start": "webpack serve --progress --port 3000", - "build": "webpack build --progress --config webpack.prod.js", - "ci:dev": "echo 'Building CI for branch preview...' && yarn --cwd .. build && cp .env.dev .env && yarn build", - "ci:prod": "echo 'Building CI for prod preview...' && yarn --cwd .. build && cp .env.prod .env && yarn build", - "ci": "[ \"$CI_PROD\" = true ] && yarn ci:prod || yarn ci:dev", - "build:serve": "npx serve dist", - "test": "jest", - "test:watch": "jest --watch", - "tsc": "tsc --noEmit true", - "tsc:watch": "tsc --watch --noEmit true", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "prestorybook": "yarn --cwd .. build", - "storybook": "start-storybook -p 6006", - "storybook:build": "build-storybook" - }, - "dependencies": { - "@chain-registry/types": "^0.18.0", - "@cosmjs/cosmwasm-stargate": "^0.32.0", - "@cosmjs/crypto": "^0.32.0", - "@cosmjs/encoding": "^0.32.0", - "@cosmjs/math": "^0.26.2", - "@cosmjs/stargate": "^0.32.0", - "@cosmjs/tendermint-rpc": "^0.32.0", - "@cosmos-kit/core": "^2.8.9", - "@cosmos-kit/keplr-extension": "^2.7.9", - "@cosmos-kit/react": "^2.10.11", - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "@interchain-ui/react": "^1.14.2", - "@mui/icons-material": "^5.0.0", - "@mui/material": "^5.0.1", - "@mui/styles": "^5.0.1", - "@mui/system": "^5.0.1", - "@mui/x-data-grid": "^5.0.0-beta.5", - "@nymproject/contract-clients": "1.2.4-rc.1", - "@nymproject/mui-theme": "^1.0.0", - "@nymproject/node-tester": "^1.0.0", - "@nymproject/nym-validator-client": "^0.18.0", - "@nymproject/react": "^1.0.0", - "@nymproject/types": "^1.0.0", - "@tauri-apps/api": "^1.5.1", - "big.js": "^6.2.1", - "bs58": "^5.0.0", - "buffer": "^6.0.3", - "chain-registry": "^1.29.1", - "cosmjs-types": "^0.9.0", - "d3-scale": "^4.0.0", - "date-fns": "^2.24.0", - "i18n-iso-countries": "^6.8.0", - "lodash": "^4.17.21", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^3.1.4", - "react-google-charts": "^3.0.15", - "react-identicons": "^1.2.5", - "react-router-dom": "6", - "react-simple-maps": "^2.3.0", - "react-tooltip": "^4.2.21", - "semver": "^7.3.8", - "use-clipboard-copy": "^0.2.0" - }, - "devDependencies": { - "@babel/core": "^7.15.0", - "@nymproject/eslint-config-react-typescript": "^1.0.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", - "@storybook/addon-actions": "^6.5.8", - "@storybook/addon-essentials": "^6.5.8", - "@storybook/addon-interactions": "^6.5.8", - "@storybook/addon-links": "^6.5.8", - "@storybook/react": "^6.5.15", - "@storybook/testing-library": "^0.0.9", - "@svgr/webpack": "^6.1.1", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^12.0.0", - "@types/d3-fetch": "^3.0.1", - "@types/d3-geo": "^3.0.2", - "@types/d3-scale": "^4.0.1", - "@types/geojson": "^7946.0.8", - "@types/jest": "^27.0.1", - "@types/node": "^16.7.13", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.10", - "@types/react-simple-maps": "^1.0.6", - "@types/react-tooltip": "^4.2.4", - "@types/topojson-client": "^3.1.0", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "babel-loader": "^8.3.0", - "babel-plugin-root-import": "^6.6.0", - "clean-webpack-plugin": "^4.0.0", - "css-loader": "^6.7.3", - "css-minimizer-webpack-plugin": "^3.0.2", - "dotenv-webpack": "^7.0.3", - "eslint": "^8.10.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^16.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-root-import": "^1.0.4", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^26.1.1", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.2", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-storybook": "^0.5.12", - "favicons-webpack-plugin": "^5.0.2", - "file-loader": "^6.2.0", - "fork-ts-checker-webpack-plugin": "^7.2.1", - "html-webpack-plugin": "^5.3.2", - "jest": "^27.1.0", - "mini-css-extract-plugin": "^2.2.2", - "prettier": "^2.8.7", - "react-refresh": "^0.10.0", - "react-refresh-typescript": "^2.0.2", - "style-loader": "^3.3.1", - "ts-jest": "^27.0.5", - "ts-loader": "^9.4.2", - "tsconfig-paths-webpack-plugin": "^3.5.2", - "typescript": "^4.6.2", - "url-loader": "^4.1.1", - "webpack": "^5.75.0", - "webpack-cli": "^4.8.0", - "webpack-dev-server": "^4.5.0", - "webpack-favicons": "^1.3.8", - "webpack-merge": "^5.8.0" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx deleted file mode 100644 index 4e6fcc41e8..0000000000 --- a/explorer/src/App.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { Nav } from './components/Nav'; -import { MobileNav } from './components/MobileNav'; -import { Routes } from './routes/index'; -import { useIsMobile } from './hooks/useIsMobile'; - -export const App: FCWithChildren = () => { - const isMobile = useIsMobile(); - - if (isMobile) { - return ( - <MobileNav> - <Routes /> - </MobileNav> - ); - } - return ( - <Nav> - <Routes /> - </Nav> - ); -}; diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts deleted file mode 100644 index fbb8c18d6b..0000000000 --- a/explorer/src/api/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -// master APIs -export const API_BASE_URL = process.env.EXPLORER_API_URL; -export const NYM_API_BASE_URL = process.env.NYM_API_URL; -export const VALIDATOR_BASE_URL = process.env.VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BIG_DIPPER = process.env.BIG_DIPPER_URL; - -// specific API routes -export const OVERVIEW_API = `${API_BASE_URL}/overview`; -export const MIXNODE_PING = `${API_BASE_URL}/ping`; -export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; -export const MIXNODE_API = `${API_BASE_URL}/mix-node`; -export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`; -export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`; -export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`; -export const BLOCK_API = `${NYM_API_BASE_URL}/block`; -export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; -export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. -export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this -export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`; - -// errors -export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; - -export const NYM_WEBSITE = 'https://nymtech.net'; - -export const NYM_BIG_DIPPER = 'https://mixnet.explorers.guru'; - -export const NYM_MIXNET_CONTRACT = - process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; -export const COSMOS_KIT_USE_CHAIN = process.env.COSMOS_KIT_USE_CHAIN || 'sandbox'; -export const WALLET_CONNECT_PROJECT_ID = process.env.WALLET_CONNECT_PROJECT_ID || ''; diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts deleted file mode 100644 index a2b134d4ae..0000000000 --- a/explorer/src/api/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import keyBy from 'lodash/keyBy'; -import { - API_BASE_URL, - BLOCK_API, - COUNTRY_DATA_API, - GATEWAYS_API, - UPTIME_STORY_API_GATEWAY, - MIXNODE_API, - MIXNODE_PING, - MIXNODES_API, - OVERVIEW_API, - UPTIME_STORY_API, - VALIDATORS_API, - SERVICE_PROVIDERS, - GATEWAYS_EXPLORER_API, -} from './constants'; - -import { - CountryDataResponse, - DelegationsResponse, - UniqDelegationsResponse, - GatewayReportResponse, - UptimeStoryResponse, - MixNodeDescriptionResponse, - MixNodeResponse, - MixNodeResponseItem, - MixnodeStatus, - MixNodeEconomicDynamicsStatsResponse, - StatsResponse, - StatusResponse, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - GatewayBondAnnotated, - GatewayBond, - DirectoryServiceProvider, - LocatedGateway, -} from '../typeDefs/explorer-api'; - -function getFromCache(key: string) { - const ts = Number(localStorage.getItem('ts')); - const hasExpired = Date.now() - ts > 5000; - const curr = localStorage.getItem(key); - if (curr && !hasExpired) { - return JSON.parse(curr); - } - return undefined; -} - -function storeInCache(key: string, data: any) { - localStorage.setItem(key, data); - localStorage.setItem('ts', Date.now().toString()); -} - -export class Api { - static fetchOverviewSummary = async (): Promise<SummaryOverviewResponse> => { - const cache = getFromCache('overview-summary'); - if (cache) { - return cache; - } - const res = await fetch(`${OVERVIEW_API}/summary`); - const json = await res.json(); - storeInCache('overview-summary', JSON.stringify(json)); - return json; - }; - - static fetchMixnodes = async (): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache('mixnodes'); - if (cachedMixnodes) { - return cachedMixnodes; - } - - const res = await fetch(MIXNODES_API); - const json = await res.json(); - storeInCache('mixnodes', JSON.stringify(json)); - return json; - }; - - static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => { - const cachedMixnodes = getFromCache(`mixnodes-${status}`); - if (cachedMixnodes) { - return cachedMixnodes; - } - const res = await fetch(`${MIXNODES_API}/active-set/${status}`); - const json = await res.json(); - storeInCache(`mixnodes-${status}`, JSON.stringify(json)); - return json; - }; - - static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => { - const response = await fetch(`${MIXNODE_API}/${id}`); - - // when the mixnode is not found, returned undefined - if (response.status === 404) { - return undefined; - } - - return response.json(); - }; - - static fetchGateways = async (): Promise<GatewayBond[]> => { - const res = await fetch(GATEWAYS_API); - const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); - const res2 = await fetch(GATEWAYS_EXPLORER_API); - const locatedGateways: LocatedGateway[] = await res2.json(); - const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); - return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ - ...gateway_bond, - node_performance, - location: locatedGatewaysByOwner[gateway_bond.owner]?.location, - })); - }; - - static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); - - static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> => - (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); - - static fetchValidators = async (): Promise<ValidatorsResponse> => { - const res = await fetch(VALIDATORS_API); - const json = await res.json(); - return json.result; - }; - - static fetchBlock = async (): Promise<number> => { - const res = await fetch(BLOCK_API); - const json = await res.json(); - const { height } = json.result.block.header; - return height; - }; - - static fetchCountryData = async (): Promise<CountryDataResponse> => { - const result: CountryDataResponse = {}; - const res = await fetch(COUNTRY_DATA_API); - const json = await res.json(); - Object.keys(json).forEach((ISO3) => { - result[ISO3] = { ISO3, nodes: json[ISO3] }; - }); - return result; - }; - - static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations`)).json(); - - static fetchUniqDelegationsById = async (id: string): Promise<UniqDelegationsResponse> => - (await fetch(`${MIXNODE_API}/${id}/delegations/summed`)).json(); - - static fetchStatsById = async (id: string): Promise<StatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/stats`)).json(); - - static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> => - (await fetch(`${MIXNODE_API}/${id}/description`)).json(); - - static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> => - (await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json(); - - static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json(); - - static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => - (await fetch(`${UPTIME_STORY_API}/${id}/history`)).json(); - - static fetchServiceProviders = async (): Promise<DirectoryServiceProvider[]> => { - const res = await fetch(SERVICE_PROVIDERS); - const json = await res.json(); - return json; - }; -} - -export const getEnvironment = (): Environment => { - const matchEnv = (env: Environment) => API_BASE_URL?.toLocaleLowerCase().includes(env) && env; - return matchEnv('sandbox') || matchEnv('qa') || 'mainnet'; -}; diff --git a/explorer/src/assets/world-110m.json b/explorer/src/assets/world-110m.json deleted file mode 100644 index 5c999c82ca..0000000000 --- a/explorer/src/assets/world-110m.json +++ /dev/null @@ -1,39718 +0,0 @@ -{ - "type": "Topology", - "arcs": [ - [ - [ - 16814, - 15074 - ], - [ - 71, - -45 - ], - [ - 53, - 16 - ], - [ - 15, - 54 - ], - [ - 55, - 18 - ], - [ - 39, - 37 - ], - [ - 14, - 95 - ], - [ - 59, - 24 - ], - [ - 11, - 42 - ], - [ - 32, - -32 - ], - [ - 21, - -3 - ] - ], - [ - [ - 17184, - 15280 - ], - [ - 39, - -1 - ], - [ - 53, - -26 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 21, - -14 - ], - [ - 51, - 38 - ], - [ - 23, - -23 - ], - [ - 23, - 55 - ], - [ - 41, - -2 - ], - [ - 11, - 17 - ], - [ - 7, - 49 - ], - [ - 30, - 41 - ], - [ - 38, - -27 - ], - [ - -8, - -37 - ], - [ - 22, - -5 - ], - [ - -7, - -101 - ], - [ - 28, - -39 - ], - [ - 24, - 25 - ], - [ - 31, - 12 - ], - [ - 43, - 53 - ], - [ - 48, - -8 - ], - [ - 72, - -1 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - 13, - -34 - ] - ], - [ - [ - 17787, - 15252 - ], - [ - -41, - -13 - ], - [ - -35, - -23 - ], - [ - -80, - -14 - ], - [ - -75, - -25 - ], - [ - -41, - -52 - ], - [ - 17, - -51 - ], - [ - 8, - -60 - ], - [ - -35, - -50 - ], - [ - 3, - -46 - ], - [ - -19, - -43 - ], - [ - -67, - 4 - ], - [ - 28, - -80 - ], - [ - -45, - -30 - ], - [ - -29, - -73 - ], - [ - 4, - -72 - ], - [ - -28, - -33 - ], - [ - -26, - 11 - ], - [ - -53, - -16 - ], - [ - -7, - -33 - ], - [ - -52, - 0 - ], - [ - -39, - -68 - ], - [ - -3, - -102 - ], - [ - -90, - -50 - ], - [ - -49, - 10 - ], - [ - -14, - -26 - ], - [ - -42, - 15 - ], - [ - -69, - -17 - ], - [ - -117, - 61 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 63, - 109 - ], - [ - -6, - 77 - ], - [ - -52, - 20 - ], - [ - -6, - 76 - ], - [ - -23, - 96 - ], - [ - 30, - 66 - ], - [ - -30, - 17 - ], - [ - 19, - 88 - ], - [ - 28, - 149 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - -128, - -49 - ], - [ - -169, - 17 - ], - [ - -48, - 58 - ], - [ - -283, - -6 - ], - [ - -11, - -8 - ], - [ - -41, - 54 - ], - [ - -45, - 4 - ], - [ - -42, - -21 - ], - [ - -34, - -22 - ] - ], - [ - [ - 13365, - 8722 - ], - [ - -6, - 75 - ], - [ - 10, - 105 - ], - [ - 24, - 110 - ], - [ - 3, - 52 - ], - [ - 23, - 108 - ], - [ - 16, - 49 - ], - [ - 41, - 79 - ], - [ - 22, - 53 - ], - [ - 7, - 89 - ], - [ - -3, - 68 - ], - [ - -21, - 43 - ], - [ - -19, - 72 - ], - [ - -17, - 72 - ], - [ - 4, - 25 - ], - [ - 21, - 48 - ], - [ - -21, - 116 - ], - [ - -14, - 80 - ], - [ - -35, - 76 - ], - [ - 6, - 23 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - 29, - 16 - ], - [ - 20, - -2 - ], - [ - 25, - 15 - ], - [ - 206, - -2 - ], - [ - 17, - -89 - ], - [ - 20, - -72 - ], - [ - 16, - -39 - ], - [ - 27, - -63 - ], - [ - 46, - 10 - ], - [ - 23, - 17 - ], - [ - 38, - -17 - ], - [ - 11, - 30 - ], - [ - 17, - 70 - ], - [ - 43, - 4 - ], - [ - 4, - 21 - ], - [ - 36, - 1 - ], - [ - -6, - -44 - ], - [ - 84, - 2 - ], - [ - 1, - -76 - ], - [ - 15, - -46 - ], - [ - -11, - -73 - ], - [ - 5, - -73 - ], - [ - 24, - -45 - ], - [ - -4, - -143 - ], - [ - 17, - 11 - ], - [ - 30, - -3 - ], - [ - 44, - 18 - ], - [ - 31, - -7 - ] - ], - [ - [ - 14214, - 9486 - ], - [ - 8, - -37 - ], - [ - -8, - -58 - ], - [ - 12, - -56 - ], - [ - -10, - -45 - ], - [ - 6, - -42 - ], - [ - -146, - 2 - ], - [ - -3, - -382 - ], - [ - 47, - -98 - ], - [ - 46, - -75 - ] - ], - [ - [ - 13397, - 10103 - ], - [ - -19, - 90 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - 28, - 52 - ], - [ - 21, - 20 - ], - [ - 26, - -41 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - -25, - -26 - ], - [ - -11, - -30 - ], - [ - -3, - -53 - ], - [ - -17, - -12 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - -2, - -31 - ], - [ - -22, - -18 - ], - [ - -4, - -39 - ], - [ - -33, - -58 - ] - ], - [ - [ - 13952, - 15551 - ], - [ - -12, - 8 - ], - [ - -1, - 27 - ], - [ - -39, - 40 - ], - [ - -6, - 57 - ], - [ - 6, - 82 - ], - [ - 10, - 37 - ], - [ - -12, - 19 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -5, - 38 - ], - [ - 30, - 59 - ], - [ - 5, - -22 - ], - [ - 19, - 11 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 14, - -33 - ], - [ - 17, - -12 - ], - [ - 5, - -43 - ] - ], - [ - [ - 13983, - 15819 - ], - [ - -9, - -41 - ], - [ - 10, - -52 - ], - [ - 29, - -29 - ] - ], - [ - [ - 16143, - 13706 - ], - [ - 12, - 6 - ], - [ - 3, - -33 - ], - [ - 55, - 19 - ], - [ - 57, - -3 - ], - [ - 42, - -4 - ], - [ - 48, - 81 - ], - [ - 52, - 77 - ], - [ - 44, - 74 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 13, - -41 - ] - ], - [ - [ - 16469, - 13882 - ], - [ - 10, - -95 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - -36, - 0 - ], - [ - -5, - -78 - ], - [ - 12, - -17 - ], - [ - -32, - -24 - ], - [ - 0, - -49 - ], - [ - -20, - -49 - ], - [ - -2, - -49 - ] - ], - [ - [ - 16396, - 13521 - ], - [ - -14, - -25 - ], - [ - -210, - 61 - ], - [ - -26, - 121 - ], - [ - -3, - 28 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -42, - 4 - ], - [ - -75, - 0 - ], - [ - 0, - 267 - ] - ], - [ - [ - 7763, - 4482 - ], - [ - 27, - -55 - ], - [ - 35, - -90 - ], - [ - 90, - -72 - ], - [ - 98, - -30 - ], - [ - -31, - -60 - ], - [ - -67, - -6 - ], - [ - -35, - 42 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -64, - 19 - ], - [ - -169, - 16 - ], - [ - -28, - 70 - ], - [ - 1, - 90 - ], - [ - -47, - -8 - ], - [ - -24, - 43 - ], - [ - -6, - 128 - ], - [ - 53, - 52 - ], - [ - 22, - 76 - ], - [ - -8, - 61 - ], - [ - 37, - 102 - ], - [ - 26, - 159 - ], - [ - -8, - 71 - ], - [ - 31, - 22 - ], - [ - -8, - 46 - ], - [ - -32, - 24 - ], - [ - 23, - 50 - ], - [ - -32, - 46 - ], - [ - -16, - 138 - ], - [ - 28, - 24 - ], - [ - -12, - 147 - ], - [ - 17, - 122 - ], - [ - 18, - 107 - ], - [ - 42, - 44 - ], - [ - -21, - 117 - ], - [ - 0, - 110 - ], - [ - 52, - 79 - ], - [ - -1, - 100 - ], - [ - 40, - 117 - ], - [ - 0, - 110 - ], - [ - -18, - 22 - ], - [ - -32, - 207 - ], - [ - 43, - 124 - ], - [ - -7, - 116 - ], - [ - 25, - 109 - ], - [ - 46, - 113 - ], - [ - 49, - 74 - ], - [ - -21, - 47 - ], - [ - 14, - 39 - ], - [ - -2, - 200 - ], - [ - 76, - 59 - ], - [ - 24, - 125 - ], - [ - -8, - 30 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - 58, - 108 - ], - [ - 91, - -29 - ], - [ - 41, - -87 - ], - [ - 27, - 97 - ], - [ - 80, - -5 - ], - [ - 11, - -26 - ] - ], - [ - [ - 8178, - 8128 - ], - [ - 128, - -196 - ], - [ - 57, - -18 - ], - [ - 85, - -89 - ], - [ - 72, - -47 - ], - [ - 10, - -52 - ], - [ - -69, - -183 - ], - [ - 71, - -32 - ], - [ - 78, - -19 - ], - [ - 55, - 20 - ], - [ - 63, - 91 - ], - [ - 12, - 106 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 34, - 23 - ], - [ - 35, - -69 - ], - [ - -1, - -96 - ], - [ - -59, - -66 - ], - [ - -47, - -49 - ], - [ - -78, - -116 - ], - [ - -93, - -164 - ] - ], - [ - [ - 8531, - 7172 - ], - [ - -18, - -96 - ], - [ - -19, - -123 - ], - [ - 1, - -120 - ], - [ - -15, - -26 - ], - [ - -5, - -78 - ] - ], - [ - [ - 8475, - 6729 - ], - [ - -5, - -63 - ], - [ - 88, - -102 - ], - [ - -9, - -83 - ], - [ - 43, - -52 - ], - [ - -3, - -59 - ], - [ - -67, - -154 - ], - [ - -103, - -64 - ], - [ - -140, - -25 - ], - [ - -77, - 12 - ], - [ - 15, - -71 - ], - [ - -14, - -90 - ], - [ - 12, - -61 - ], - [ - -41, - -42 - ], - [ - -72, - -17 - ], - [ - -67, - 44 - ], - [ - -27, - -31 - ], - [ - 10, - -119 - ], - [ - 47, - -37 - ], - [ - 38, - 38 - ], - [ - 21, - -62 - ], - [ - -64, - -37 - ], - [ - -56, - -75 - ], - [ - -10, - -121 - ], - [ - -17, - -64 - ], - [ - -66, - 0 - ], - [ - -54, - -62 - ], - [ - -20, - -90 - ], - [ - 68, - -87 - ], - [ - 67, - -25 - ], - [ - -24, - -107 - ], - [ - -83, - -68 - ], - [ - -45, - -141 - ], - [ - -63, - -47 - ], - [ - -29, - -56 - ], - [ - 22, - -125 - ], - [ - 47, - -69 - ], - [ - -30, - 6 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - 96, - 19 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 15, - -32 - ], - [ - 26, - -21 - ], - [ - -14, - -30 - ], - [ - 38, - -41 - ], - [ - -20, - -38 - ], - [ - 29, - -33 - ], - [ - 32, - -19 - ], - [ - 1, - -84 - ] - ], - [ - [ - 15789, - 15448 - ], - [ - -25, - -3 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -28, - 69 - ], - [ - 0, - 19 - ], - [ - -31, - 0 - ], - [ - -20, - 32 - ], - [ - -15, - -3 - ] - ], - [ - [ - 15670, - 15562 - ], - [ - -27, - 35 - ], - [ - -52, - 29 - ], - [ - 6, - 59 - ], - [ - -11, - 42 - ] - ], - [ - [ - 8395, - 1195 - ], - [ - -21, - -61 - ], - [ - -20, - -54 - ], - [ - -146, - 16 - ], - [ - -156, - -7 - ], - [ - -87, - 40 - ], - [ - 0, - 5 - ], - [ - -38, - 35 - ], - [ - 157, - -5 - ], - [ - 150, - -11 - ], - [ - 52, - 49 - ], - [ - 36, - 42 - ], - [ - 73, - -49 - ] - ], - [ - [ - 1449, - 1260 - ], - [ - -133, - -16 - ], - [ - -92, - 42 - ], - [ - -41, - 42 - ], - [ - -3, - 7 - ], - [ - -45, - 33 - ], - [ - 43, - 45 - ], - [ - 129, - -19 - ], - [ - 70, - -38 - ], - [ - 53, - -42 - ], - [ - 19, - -54 - ] - ], - [ - [ - 9400, - 1434 - ], - [ - 86, - -52 - ], - [ - 30, - -73 - ], - [ - 8, - -51 - ], - [ - 3, - -61 - ], - [ - -108, - -38 - ], - [ - -113, - -31 - ], - [ - -131, - -28 - ], - [ - -147, - -23 - ], - [ - -165, - 7 - ], - [ - -91, - 40 - ], - [ - 12, - 49 - ], - [ - 149, - 33 - ], - [ - 60, - 40 - ], - [ - 44, - 52 - ], - [ - 31, - 44 - ], - [ - 42, - 43 - ], - [ - 45, - 49 - ], - [ - 36, - 0 - ], - [ - 104, - 26 - ], - [ - 105, - -26 - ] - ], - [ - [ - 4098, - 1979 - ], - [ - 90, - -18 - ], - [ - 83, - 21 - ], - [ - -39, - -43 - ], - [ - -66, - -30 - ], - [ - -97, - 9 - ], - [ - -69, - 43 - ], - [ - 15, - 40 - ], - [ - 83, - -22 - ] - ], - [ - [ - 3795, - 1982 - ], - [ - 106, - -47 - ], - [ - -41, - 4 - ], - [ - -90, - 12 - ], - [ - -95, - 33 - ], - [ - 50, - 26 - ], - [ - 70, - -28 - ] - ], - [ - [ - 5648, - 2167 - ], - [ - 76, - -16 - ], - [ - 77, - 14 - ], - [ - 41, - -68 - ], - [ - -55, - 9 - ], - [ - -85, - -4 - ], - [ - -86, - 4 - ], - [ - -94, - -7 - ], - [ - -71, - 24 - ], - [ - -37, - 49 - ], - [ - 44, - 21 - ], - [ - 89, - -16 - ], - [ - 101, - -10 - ] - ], - [ - [ - 7776, - 2285 - ], - [ - 8, - -54 - ], - [ - -12, - -47 - ], - [ - -19, - -45 - ], - [ - -82, - -16 - ], - [ - -78, - -24 - ], - [ - -92, - 2 - ], - [ - 35, - 47 - ], - [ - -82, - -16 - ], - [ - -78, - -17 - ], - [ - -53, - 36 - ], - [ - -5, - 49 - ], - [ - 77, - 47 - ], - [ - 48, - 14 - ], - [ - 80, - -5 - ], - [ - 21, - 62 - ], - [ - 4, - 44 - ], - [ - -2, - 97 - ], - [ - 40, - 56 - ], - [ - 64, - 19 - ], - [ - 37, - -45 - ], - [ - 17, - -44 - ], - [ - 30, - -55 - ], - [ - 23, - -51 - ], - [ - 19, - -54 - ] - ], - [ - [ - 8462, - 3101 - ], - [ - -30, - -26 - ], - [ - -52, - 19 - ], - [ - -58, - -12 - ], - [ - -47, - -28 - ], - [ - -51, - -31 - ], - [ - -34, - -35 - ], - [ - -10, - -47 - ], - [ - 4, - -45 - ], - [ - 33, - -40 - ], - [ - -48, - -28 - ], - [ - -65, - -9 - ], - [ - -38, - -40 - ], - [ - -41, - -38 - ], - [ - -44, - -51 - ], - [ - -11, - -45 - ], - [ - 25, - -50 - ], - [ - 37, - -37 - ], - [ - 57, - -28 - ], - [ - 53, - -38 - ], - [ - 29, - -47 - ], - [ - 15, - -45 - ], - [ - 20, - -47 - ], - [ - 33, - -40 - ], - [ - 21, - -44 - ], - [ - 9, - -111 - ], - [ - 21, - -44 - ], - [ - 5, - -47 - ], - [ - 22, - -47 - ], - [ - -10, - -64 - ], - [ - -38, - -49 - ], - [ - -41, - -40 - ], - [ - -93, - -17 - ], - [ - -31, - -42 - ], - [ - -42, - -40 - ], - [ - -106, - -45 - ], - [ - -92, - -18 - ], - [ - -88, - -26 - ], - [ - -94, - -26 - ], - [ - -56, - -50 - ], - [ - -112, - -4 - ], - [ - -123, - 4 - ], - [ - -110, - -9 - ], - [ - -118, - 0 - ], - [ - 22, - -47 - ], - [ - 107, - -21 - ], - [ - 77, - -33 - ], - [ - 44, - -42 - ], - [ - -78, - -38 - ], - [ - -120, - 12 - ], - [ - -100, - -31 - ], - [ - -4, - -49 - ], - [ - -2, - -47 - ], - [ - 82, - -40 - ], - [ - 15, - -45 - ], - [ - 88, - -44 - ], - [ - 148, - -19 - ], - [ - 125, - -33 - ], - [ - 100, - -38 - ], - [ - 127, - -37 - ], - [ - 173, - -19 - ], - [ - 171, - -33 - ], - [ - 119, - -35 - ], - [ - 130, - -40 - ], - [ - 68, - -57 - ], - [ - 34, - -44 - ], - [ - 85, - 42 - ], - [ - 114, - 35 - ], - [ - 122, - 38 - ], - [ - 144, - 30 - ], - [ - 125, - 33 - ], - [ - 173, - 3 - ], - [ - 171, - -17 - ], - [ - 140, - -28 - ], - [ - 45, - 52 - ], - [ - 97, - 35 - ], - [ - 177, - 2 - ], - [ - 137, - 26 - ], - [ - 131, - 26 - ], - [ - 145, - 16 - ], - [ - 154, - 22 - ], - [ - 108, - 30 - ], - [ - -49, - 42 - ], - [ - -30, - 43 - ], - [ - 0, - 44 - ], - [ - -135, - -4 - ], - [ - -143, - -19 - ], - [ - -137, - 0 - ], - [ - -19, - 45 - ], - [ - 10, - 89 - ], - [ - 31, - 26 - ], - [ - 100, - 28 - ], - [ - 117, - 28 - ], - [ - 85, - 35 - ], - [ - 84, - 36 - ], - [ - 63, - 47 - ], - [ - 96, - 21 - ], - [ - 94, - 16 - ], - [ - 48, - 10 - ], - [ - 108, - 4 - ], - [ - 102, - 17 - ], - [ - 86, - 23 - ], - [ - 85, - 29 - ], - [ - 76, - 28 - ], - [ - 97, - 37 - ], - [ - 61, - 40 - ], - [ - 66, - 36 - ], - [ - 20, - 47 - ], - [ - -73, - 28 - ], - [ - 24, - 49 - ], - [ - 47, - 38 - ], - [ - 72, - 23 - ], - [ - 77, - 29 - ], - [ - 71, - 37 - ], - [ - 54, - 47 - ], - [ - 34, - 57 - ], - [ - 51, - 33 - ], - [ - 83, - -7 - ], - [ - 34, - -40 - ], - [ - 83, - -5 - ], - [ - 3, - 45 - ], - [ - 36, - 47 - ], - [ - 75, - -12 - ], - [ - 18, - -45 - ], - [ - 83, - -7 - ], - [ - 90, - 21 - ], - [ - 87, - 14 - ], - [ - 80, - -7 - ], - [ - 30, - -49 - ], - [ - 76, - 40 - ], - [ - 71, - 21 - ], - [ - 79, - 16 - ], - [ - 78, - 17 - ], - [ - 71, - 28 - ], - [ - 78, - 19 - ], - [ - 60, - 26 - ], - [ - 42, - 42 - ], - [ - 52, - -30 - ], - [ - 72, - 16 - ], - [ - 51, - -56 - ], - [ - 40, - -43 - ], - [ - 79, - 24 - ], - [ - 31, - 47 - ], - [ - 71, - 33 - ], - [ - 92, - -7 - ], - [ - 27, - -45 - ], - [ - 57, - 45 - ], - [ - 75, - 14 - ], - [ - 82, - 4 - ], - [ - 74, - -2 - ], - [ - 78, - -14 - ], - [ - 75, - -7 - ], - [ - 33, - -40 - ], - [ - 45, - -35 - ], - [ - 76, - 21 - ], - [ - 82, - 5 - ], - [ - 79, - 0 - ], - [ - 78, - 2 - ], - [ - 70, - 16 - ], - [ - 74, - 15 - ], - [ - 61, - 32 - ], - [ - 65, - 22 - ], - [ - 71, - 11 - ], - [ - 54, - 33 - ], - [ - 38, - 66 - ], - [ - 40, - 40 - ], - [ - 72, - -19 - ], - [ - 27, - -42 - ], - [ - 60, - -28 - ], - [ - 73, - 9 - ], - [ - 49, - -42 - ], - [ - 52, - -31 - ], - [ - 71, - 28 - ], - [ - 24, - 52 - ], - [ - 63, - 21 - ], - [ - 72, - 40 - ], - [ - 69, - 17 - ], - [ - 82, - 23 - ], - [ - 54, - 26 - ], - [ - 58, - 28 - ], - [ - 54, - 26 - ], - [ - 66, - -14 - ], - [ - 63, - 42 - ], - [ - 45, - 33 - ], - [ - 65, - -2 - ], - [ - 57, - 28 - ], - [ - 14, - 42 - ], - [ - 59, - 33 - ], - [ - 57, - 24 - ], - [ - 70, - 19 - ], - [ - 64, - 9 - ], - [ - 61, - -7 - ], - [ - 66, - -12 - ], - [ - 56, - -33 - ], - [ - 7, - -51 - ], - [ - 61, - -40 - ], - [ - 42, - -33 - ], - [ - 84, - -14 - ], - [ - 46, - -33 - ], - [ - 58, - -33 - ], - [ - 66, - -7 - ], - [ - 56, - 23 - ], - [ - 60, - 50 - ], - [ - 66, - -26 - ], - [ - 68, - -14 - ], - [ - 66, - -14 - ], - [ - 68, - -10 - ], - [ - 70, - 0 - ], - [ - 57, - -124 - ], - [ - -3, - -31 - ], - [ - -8, - -54 - ], - [ - -67, - -31 - ], - [ - -54, - -44 - ], - [ - 9, - -47 - ], - [ - 78, - 2 - ], - [ - -10, - -47 - ], - [ - -35, - -45 - ], - [ - -33, - -49 - ], - [ - 53, - -38 - ], - [ - 81, - -11 - ], - [ - 81, - 21 - ], - [ - 38, - 47 - ], - [ - 23, - 45 - ], - [ - 38, - 37 - ], - [ - 44, - 35 - ], - [ - 18, - 43 - ], - [ - 36, - 58 - ], - [ - 44, - 12 - ], - [ - 79, - 5 - ], - [ - 70, - 14 - ], - [ - 71, - 19 - ], - [ - 34, - 47 - ], - [ - 21, - 45 - ], - [ - 47, - 44 - ], - [ - 69, - 31 - ], - [ - 58, - 23 - ], - [ - 39, - 40 - ], - [ - 39, - 21 - ], - [ - 51, - 19 - ], - [ - 69, - -12 - ], - [ - 63, - 12 - ], - [ - 68, - 14 - ], - [ - 77, - -7 - ], - [ - 50, - 33 - ], - [ - 36, - 80 - ], - [ - 26, - -33 - ], - [ - 33, - -56 - ], - [ - 58, - -24 - ], - [ - 67, - -9 - ], - [ - 67, - 14 - ], - [ - 71, - -9 - ], - [ - 66, - -3 - ], - [ - 43, - 12 - ], - [ - 59, - -7 - ], - [ - 53, - -26 - ], - [ - 63, - 16 - ], - [ - 75, - 0 - ], - [ - 64, - 17 - ], - [ - 73, - -17 - ], - [ - 46, - 40 - ], - [ - 36, - 40 - ], - [ - 47, - 33 - ], - [ - 88, - 90 - ], - [ - 45, - -17 - ], - [ - 53, - -33 - ], - [ - 46, - -42 - ], - [ - 89, - -73 - ], - [ - 69, - -2 - ], - [ - 64, - 0 - ], - [ - 75, - 14 - ], - [ - 75, - 16 - ], - [ - 57, - 33 - ], - [ - 48, - 35 - ], - [ - 78, - 5 - ], - [ - 52, - 26 - ], - [ - 54, - -23 - ], - [ - 36, - -38 - ], - [ - 49, - -38 - ], - [ - 76, - 5 - ], - [ - 48, - -31 - ], - [ - 83, - -30 - ], - [ - 88, - -12 - ], - [ - 72, - 10 - ], - [ - 55, - 37 - ], - [ - 46, - 38 - ], - [ - 63, - 9 - ], - [ - 63, - -16 - ], - [ - 72, - -12 - ], - [ - 66, - 19 - ], - [ - 63, - 0 - ], - [ - 61, - -12 - ], - [ - 64, - -12 - ], - [ - 63, - 21 - ], - [ - 75, - 19 - ], - [ - 71, - 5 - ], - [ - 79, - 0 - ], - [ - 64, - 12 - ], - [ - 63, - 9 - ], - [ - 19, - 59 - ], - [ - 3, - 49 - ], - [ - 44, - -33 - ], - [ - 12, - -54 - ], - [ - 23, - -49 - ], - [ - 29, - -40 - ], - [ - 59, - -21 - ], - [ - 79, - 7 - ], - [ - 91, - 2 - ], - [ - 63, - 7 - ], - [ - 92, - 0 - ], - [ - 65, - 3 - ], - [ - 92, - -5 - ], - [ - 77, - -10 - ], - [ - 50, - -37 - ], - [ - -14, - -45 - ], - [ - 45, - -35 - ], - [ - 75, - -28 - ], - [ - 78, - -31 - ], - [ - 90, - -21 - ], - [ - 94, - -19 - ], - [ - 71, - -19 - ], - [ - 79, - -2 - ], - [ - 45, - 40 - ], - [ - 62, - -33 - ], - [ - 53, - -38 - ], - [ - 62, - -28 - ], - [ - 84, - -12 - ], - [ - 81, - -14 - ], - [ - 34, - -47 - ], - [ - 79, - -28 - ], - [ - 53, - -42 - ], - [ - 78, - -19 - ], - [ - 81, - 2 - ], - [ - 75, - -7 - ], - [ - 83, - 3 - ], - [ - 83, - -10 - ], - [ - 78, - -16 - ], - [ - 73, - -28 - ], - [ - 72, - -24 - ], - [ - 49, - -35 - ], - [ - -8, - -47 - ], - [ - -37, - -42 - ], - [ - -31, - -55 - ], - [ - -25, - -42 - ], - [ - -33, - -49 - ], - [ - -91, - -19 - ], - [ - -41, - -42 - ], - [ - -90, - -26 - ], - [ - -32, - -47 - ], - [ - -47, - -45 - ], - [ - -51, - -38 - ], - [ - -29, - -49 - ], - [ - -17, - -45 - ], - [ - -7, - -54 - ], - [ - 1, - -44 - ], - [ - 40, - -47 - ], - [ - 15, - -45 - ], - [ - 32, - -42 - ], - [ - 130, - -17 - ], - [ - 27, - -51 - ], - [ - -125, - -19 - ], - [ - -107, - -26 - ], - [ - -132, - -5 - ], - [ - -59, - -68 - ], - [ - -12, - -56 - ], - [ - -30, - -45 - ], - [ - -37, - -45 - ], - [ - 93, - -40 - ], - [ - 35, - -49 - ], - [ - 60, - -45 - ], - [ - 85, - -40 - ], - [ - 97, - -37 - ], - [ - 105, - -38 - ], - [ - 160, - -38 - ], - [ - 35, - -58 - ], - [ - 201, - -26 - ], - [ - 14, - -9 - ], - [ - 52, - -36 - ], - [ - 192, - 31 - ], - [ - 160, - -38 - ], - [ - 120, - -29 - ], - [ - 0, - -634 - ], - [ - -25095, - 0 - ], - [ - 0, - 634 - ], - [ - 4, - -1 - ], - [ - 62, - 70 - ], - [ - 125, - -38 - ], - [ - 8, - 5 - ], - [ - 74, - 38 - ], - [ - 10, - -1 - ], - [ - 8, - -1 - ], - [ - 101, - -50 - ], - [ - 88, - 50 - ], - [ - 16, - 6 - ], - [ - 204, - 22 - ], - [ - 67, - -28 - ], - [ - 33, - -15 - ], - [ - 105, - -40 - ], - [ - 198, - -30 - ], - [ - 157, - -38 - ], - [ - 269, - -28 - ], - [ - 200, - 33 - ], - [ - 297, - -24 - ], - [ - 168, - -37 - ], - [ - 184, - 35 - ], - [ - 194, - 33 - ], - [ - 15, - 56 - ], - [ - -275, - 5 - ], - [ - -225, - 28 - ], - [ - -59, - 47 - ], - [ - -187, - 26 - ], - [ - 13, - 54 - ], - [ - 25, - 50 - ], - [ - 26, - 44 - ], - [ - -13, - 50 - ], - [ - -116, - 33 - ], - [ - -54, - 42 - ], - [ - -107, - 37 - ], - [ - 169, - -7 - ], - [ - 161, - 19 - ], - [ - 101, - -40 - ], - [ - 124, - 36 - ], - [ - 115, - 44 - ], - [ - 56, - 40 - ], - [ - -25, - 50 - ], - [ - -90, - 32 - ], - [ - -102, - 36 - ], - [ - -143, - 7 - ], - [ - -126, - 16 - ], - [ - -135, - 12 - ], - [ - -45, - 45 - ], - [ - -90, - 37 - ], - [ - -55, - 43 - ], - [ - -22, - 136 - ], - [ - 34, - -12 - ], - [ - 63, - -37 - ], - [ - 115, - 11 - ], - [ - 110, - 17 - ], - [ - 58, - -52 - ], - [ - 110, - 12 - ], - [ - 93, - 26 - ], - [ - 87, - 33 - ], - [ - 80, - 39 - ], - [ - 105, - 12 - ], - [ - -3, - 45 - ], - [ - -24, - 45 - ], - [ - 20, - 42 - ], - [ - 90, - 21 - ], - [ - 41, - -40 - ], - [ - 107, - 24 - ], - [ - 80, - 30 - ], - [ - 100, - 3 - ], - [ - 94, - 11 - ], - [ - 94, - 28 - ], - [ - 75, - 26 - ], - [ - 85, - 26 - ], - [ - 55, - -7 - ], - [ - 47, - -9 - ], - [ - 104, - 16 - ], - [ - 93, - -21 - ], - [ - 95, - 2 - ], - [ - 92, - 17 - ], - [ - 94, - -12 - ], - [ - 104, - -12 - ], - [ - 97, - 5 - ], - [ - 101, - -2 - ], - [ - 104, - -3 - ], - [ - 95, - 5 - ], - [ - 71, - 35 - ], - [ - 85, - 19 - ], - [ - 87, - -26 - ], - [ - 84, - 21 - ], - [ - 75, - 43 - ], - [ - 45, - -38 - ], - [ - 24, - -42 - ], - [ - 45, - -40 - ], - [ - 73, - 35 - ], - [ - 83, - -45 - ], - [ - 94, - -14 - ], - [ - 81, - -33 - ], - [ - 98, - 7 - ], - [ - 89, - 22 - ], - [ - 105, - -5 - ], - [ - 94, - -17 - ], - [ - 96, - -21 - ], - [ - 37, - 52 - ], - [ - -46, - 40 - ], - [ - -34, - 42 - ], - [ - -90, - 10 - ], - [ - -39, - 44 - ], - [ - -15, - 45 - ], - [ - -25, - 89 - ], - [ - 53, - -16 - ], - [ - 92, - -7 - ], - [ - 90, - 7 - ], - [ - 82, - -19 - ], - [ - 71, - -35 - ], - [ - 30, - -42 - ], - [ - 94, - -8 - ], - [ - 90, - 17 - ], - [ - 96, - 23 - ], - [ - 86, - 15 - ], - [ - 71, - -29 - ], - [ - 93, - 10 - ], - [ - 60, - 91 - ], - [ - 56, - -54 - ], - [ - 80, - -21 - ], - [ - 88, - 12 - ], - [ - 57, - -47 - ], - [ - 91, - -5 - ], - [ - 85, - -14 - ], - [ - 83, - -26 - ], - [ - 55, - 45 - ], - [ - 27, - 42 - ], - [ - 70, - -47 - ], - [ - 95, - 12 - ], - [ - 71, - -26 - ], - [ - 48, - -40 - ], - [ - 93, - 12 - ], - [ - 72, - 26 - ], - [ - 71, - 30 - ], - [ - 85, - 17 - ], - [ - 98, - 14 - ], - [ - 89, - 16 - ], - [ - 68, - 26 - ], - [ - 41, - 38 - ], - [ - 17, - 52 - ], - [ - -8, - 49 - ], - [ - -22, - 47 - ], - [ - -25, - 47 - ], - [ - -22, - 47 - ], - [ - -18, - 42 - ], - [ - -4, - 47 - ], - [ - 7, - 47 - ], - [ - 33, - 45 - ], - [ - 27, - 49 - ], - [ - 11, - 47 - ], - [ - -13, - 52 - ], - [ - -9, - 47 - ], - [ - 35, - 54 - ], - [ - 38, - 35 - ], - [ - 45, - 45 - ], - [ - 48, - 38 - ], - [ - 56, - 35 - ], - [ - 27, - 52 - ], - [ - 38, - 33 - ], - [ - 44, - 30 - ], - [ - 67, - 7 - ], - [ - 43, - 38 - ], - [ - 50, - 23 - ], - [ - 57, - 14 - ], - [ - 50, - 31 - ], - [ - 40, - 38 - ], - [ - 55, - 14 - ], - [ - 41, - -31 - ], - [ - -26, - -40 - ], - [ - -71, - -35 - ] - ], - [ - [ - 17353, - 4964 - ], - [ - 45, - -38 - ], - [ - 66, - -15 - ], - [ - 2, - -23 - ], - [ - -19, - -54 - ], - [ - -107, - -8 - ], - [ - -2, - 64 - ], - [ - 10, - 49 - ], - [ - 5, - 25 - ] - ], - [ - [ - 22683, - 5903 - ], - [ - 67, - -41 - ], - [ - 38, - 16 - ], - [ - 55, - 23 - ], - [ - 41, - -8 - ], - [ - 5, - -142 - ], - [ - -23, - -41 - ], - [ - -8, - -97 - ], - [ - -24, - 33 - ], - [ - -48, - -84 - ], - [ - -15, - 7 - ], - [ - -43, - 4 - ], - [ - -43, - 102 - ], - [ - -9, - 79 - ], - [ - -40, - 105 - ], - [ - 1, - 55 - ], - [ - 46, - -11 - ] - ], - [ - [ - 22555, - 9146 - ], - [ - 25, - -94 - ], - [ - 45, - 45 - ], - [ - 23, - -51 - ], - [ - 33, - -47 - ], - [ - -7, - -53 - ], - [ - 15, - -103 - ], - [ - 11, - -59 - ], - [ - 17, - -15 - ], - [ - 19, - -103 - ], - [ - -7, - -62 - ], - [ - 23, - -81 - ], - [ - 75, - -63 - ], - [ - 50, - -57 - ], - [ - 46, - -52 - ], - [ - -9, - -29 - ], - [ - 40, - -75 - ], - [ - 27, - -130 - ], - [ - 28, - 26 - ], - [ - 28, - -52 - ], - [ - 17, - 19 - ], - [ - 12, - -128 - ], - [ - 50, - -73 - ], - [ - 32, - -46 - ], - [ - 55, - -97 - ], - [ - 19, - -97 - ], - [ - 2, - -68 - ], - [ - -5, - -74 - ], - [ - 34, - -102 - ], - [ - -4, - -106 - ], - [ - -12, - -56 - ], - [ - -19, - -107 - ], - [ - 1, - -69 - ], - [ - -14, - -86 - ], - [ - -30, - -109 - ], - [ - -52, - -59 - ], - [ - -26, - -93 - ], - [ - -23, - -59 - ], - [ - -20, - -104 - ], - [ - -27, - -59 - ], - [ - -18, - -90 - ], - [ - -9, - -83 - ], - [ - 4, - -38 - ], - [ - -40, - -41 - ], - [ - -78, - -5 - ], - [ - -65, - -49 - ], - [ - -32, - -46 - ], - [ - -42, - -52 - ], - [ - -58, - 53 - ], - [ - -42, - 21 - ], - [ - 10, - 63 - ], - [ - -38, - -23 - ], - [ - -61, - -87 - ], - [ - -60, - 33 - ], - [ - -39, - 19 - ], - [ - -40, - 8 - ], - [ - -68, - 35 - ], - [ - -45, - 74 - ], - [ - -13, - 91 - ], - [ - -16, - 61 - ], - [ - -34, - 48 - ], - [ - -67, - 15 - ], - [ - 23, - 58 - ], - [ - -17, - 89 - ], - [ - -34, - -83 - ], - [ - -62, - -22 - ], - [ - 36, - 66 - ], - [ - 11, - 70 - ], - [ - 27, - 58 - ], - [ - -6, - 89 - ], - [ - -57, - -102 - ], - [ - -43, - -41 - ], - [ - -27, - -96 - ], - [ - -54, - 50 - ], - [ - 2, - 63 - ], - [ - -44, - 87 - ], - [ - -37, - 45 - ], - [ - 14, - 28 - ], - [ - -90, - 73 - ], - [ - -49, - 3 - ], - [ - -67, - 59 - ], - [ - -125, - -12 - ], - [ - -90, - -43 - ], - [ - -79, - -40 - ], - [ - -67, - 8 - ], - [ - -74, - -61 - ], - [ - -60, - -28 - ], - [ - -14, - -63 - ], - [ - -25, - -49 - ], - [ - -60, - -2 - ], - [ - -43, - -11 - ], - [ - -62, - 22 - ], - [ - -50, - -13 - ], - [ - -48, - -6 - ], - [ - -41, - -64 - ], - [ - -21, - 6 - ], - [ - -35, - -34 - ], - [ - -33, - -38 - ], - [ - -51, - 4 - ], - [ - -47, - 0 - ], - [ - -74, - 77 - ], - [ - -37, - 23 - ], - [ - 1, - 68 - ], - [ - 35, - 17 - ], - [ - 12, - 27 - ], - [ - -3, - 43 - ], - [ - 9, - 84 - ], - [ - -8, - 71 - ], - [ - -37, - 121 - ], - [ - -11, - 68 - ], - [ - 3, - 69 - ], - [ - -28, - 78 - ], - [ - -2, - 35 - ], - [ - -31, - 48 - ], - [ - -8, - 94 - ], - [ - -40, - 95 - ], - [ - -10, - 51 - ], - [ - 31, - -52 - ], - [ - -24, - 111 - ], - [ - 35, - -34 - ], - [ - 20, - -47 - ], - [ - -1, - 62 - ], - [ - -34, - 94 - ], - [ - -7, - 38 - ], - [ - -16, - 36 - ], - [ - 8, - 69 - ], - [ - 14, - 30 - ], - [ - 9, - 60 - ], - [ - -7, - 70 - ], - [ - 29, - 86 - ], - [ - 5, - -91 - ], - [ - 29, - 82 - ], - [ - 57, - 40 - ], - [ - 34, - 52 - ], - [ - 53, - 44 - ], - [ - 32, - 9 - ], - [ - 19, - -15 - ], - [ - 55, - 45 - ], - [ - 42, - 13 - ], - [ - 11, - 27 - ], - [ - 18, - 10 - ], - [ - 39, - -2 - ], - [ - 73, - 35 - ], - [ - 38, - 53 - ], - [ - 18, - 64 - ], - [ - 41, - 61 - ], - [ - 3, - 48 - ], - [ - 2, - 65 - ], - [ - 49, - 102 - ], - [ - 29, - -103 - ], - [ - 30, - 23 - ], - [ - -25, - 57 - ], - [ - 22, - 58 - ], - [ - 30, - -26 - ], - [ - 9, - 92 - ], - [ - 38, - 59 - ], - [ - 17, - 47 - ], - [ - 35, - 20 - ], - [ - 1, - 34 - ], - [ - 30, - -14 - ], - [ - 2, - 30 - ], - [ - 30, - 17 - ], - [ - 34, - 16 - ], - [ - 52, - -55 - ], - [ - 38, - -71 - ], - [ - 44, - 0 - ], - [ - 44, - -12 - ], - [ - -15, - 66 - ], - [ - 34, - 96 - ], - [ - 31, - 32 - ], - [ - -11, - 30 - ], - [ - 31, - 68 - ], - [ - 42, - 43 - ], - [ - 36, - -15 - ], - [ - 58, - 23 - ], - [ - -1, - 61 - ], - [ - -51, - 40 - ], - [ - 37, - 17 - ], - [ - 46, - -30 - ], - [ - 37, - -49 - ], - [ - 59, - -31 - ], - [ - 20, - 13 - ], - [ - 43, - -37 - ], - [ - 41, - 34 - ], - [ - 26, - -10 - ], - [ - 16, - 23 - ], - [ - 32, - -60 - ], - [ - -18, - -64 - ], - [ - -27, - -48 - ], - [ - -24, - -4 - ], - [ - 8, - -48 - ], - [ - -20, - -60 - ], - [ - -25, - -59 - ], - [ - 5, - -34 - ], - [ - 55, - -66 - ], - [ - 54, - -39 - ], - [ - 36, - -41 - ], - [ - 50, - -71 - ], - [ - 20, - 0 - ], - [ - 37, - -31 - ], - [ - 10, - -37 - ], - [ - 67, - -41 - ], - [ - 46, - 41 - ], - [ - 13, - 65 - ], - [ - 14, - 53 - ], - [ - 9, - 66 - ], - [ - 21, - 95 - ], - [ - -9, - 58 - ], - [ - 5, - 35 - ], - [ - -8, - 69 - ], - [ - 9, - 90 - ], - [ - 13, - 25 - ], - [ - -11, - 40 - ], - [ - 17, - 63 - ], - [ - 13, - 66 - ], - [ - 2, - 34 - ], - [ - 26, - 45 - ], - [ - 20, - -58 - ], - [ - 5, - -76 - ], - [ - 17, - -14 - ], - [ - 3, - -51 - ], - [ - 25, - -61 - ], - [ - 5, - -67 - ], - [ - -2, - -44 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - -5, - -50 - ], - [ - -39, - 0 - ], - [ - 13, - -26 - ], - [ - -23, - -77 - ] - ], - [ - [ - 13677, - 16418 - ], - [ - -13, - -20 - ], - [ - -61, - -3 - ], - [ - -35, - -27 - ], - [ - -58, - 9 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -100, - 31 - ], - [ - -15, - 42 - ], - [ - -69, - -21 - ], - [ - -8, - -23 - ], - [ - -43, - 17 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -35, - 3 - ], - [ - -32, - 22 - ], - [ - 11, - 29 - ], - [ - -3, - 22 - ] - ], - [ - [ - 13216, - 16499 - ], - [ - 21, - 6 - ], - [ - 36, - -33 - ], - [ - 10, - 32 - ], - [ - 61, - -5 - ], - [ - 50, - 21 - ], - [ - 33, - -4 - ], - [ - 22, - -24 - ], - [ - 7, - 20 - ], - [ - -10, - 78 - ], - [ - 25, - 16 - ], - [ - 24, - 55 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - 52, - -39 - ], - [ - 39, - 49 - ], - [ - 25, - 9 - ], - [ - 54, - -36 - ], - [ - 33, - 6 - ], - [ - 32, - -23 - ] - ], - [ - [ - 13730, - 16627 - ], - [ - -6, - -15 - ], - [ - 7, - -41 - ] - ], - [ - [ - 15682, - 15746 - ], - [ - 18, - 19 - ], - [ - 51, - -34 - ], - [ - 38, - -7 - ], - [ - 10, - 14 - ], - [ - -35, - 65 - ], - [ - 18, - 16 - ] - ], - [ - [ - 15782, - 15819 - ], - [ - 20, - -4 - ], - [ - 48, - -73 - ], - [ - 31, - -8 - ], - [ - 12, - 31 - ], - [ - 41, - 48 - ] - ], - [ - [ - 15934, - 15813 - ], - [ - 37, - -63 - ], - [ - 35, - -85 - ], - [ - 33, - -6 - ], - [ - 21, - -32 - ], - [ - -57, - -10 - ], - [ - -12, - -93 - ], - [ - -12, - -42 - ], - [ - -26, - -28 - ], - [ - 2, - -60 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - -17, - -6 - ], - [ - -44, - 63 - ], - [ - 24, - 60 - ], - [ - -20, - 35 - ], - [ - -26, - -9 - ], - [ - -83, - -89 - ] - ], - [ - [ - 15764, - 15445 - ], - [ - -48, - 16 - ], - [ - -35, - 55 - ], - [ - -11, - 46 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - -5, - 145 - ], - [ - -17, - 55 - ] - ], - [ - [ - 14571, - 10457 - ], - [ - 42, - -10 - ], - [ - 21, - 68 - ], - [ - 37, - -7 - ] - ], - [ - [ - 14671, - 10508 - ], - [ - 5, - -48 - ], - [ - 15, - -27 - ], - [ - 0, - -39 - ], - [ - -17, - -25 - ], - [ - -27, - -62 - ], - [ - -25, - -44 - ], - [ - -29, - -6 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -8, - -81 - ] - ], - [ - [ - 12969, - 16811 - ], - [ - -18, - -5 - ], - [ - -8, - -67 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - -61, - 55 - ], - [ - -36, - -9 - ], - [ - -48, - 56 - ], - [ - -33, - 48 - ], - [ - -32, - 2 - ], - [ - -10, - 42 - ] - ], - [ - [ - 12723, - 16933 - ], - [ - 56, - 24 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 51, - -9 - ], - [ - 64, - 25 - ], - [ - 44, - -53 - ], - [ - 39, - -28 - ] - ], - [ - [ - 12735, - 11548 - ], - [ - -57, - -14 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -18, - 83 - ], - [ - 4, - 275 - ], - [ - -15, - 25 - ], - [ - -2, - 59 - ], - [ - -24, - 42 - ], - [ - -22, - 35 - ], - [ - 9, - 64 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - 24, - 13 - ], - [ - 14, - 53 - ], - [ - 34, - 11 - ], - [ - 16, - 36 - ] - ], - [ - [ - 12698, - 12230 - ], - [ - 23, - 35 - ], - [ - 25, - 0 - ], - [ - 53, - -69 - ] - ], - [ - [ - 12799, - 12196 - ], - [ - -2, - -40 - ], - [ - 15, - -71 - ], - [ - -14, - -48 - ], - [ - 8, - -33 - ], - [ - -34, - -74 - ], - [ - -21, - -37 - ], - [ - -14, - -75 - ], - [ - 2, - -77 - ], - [ - -4, - -193 - ] - ], - [ - [ - 12610, - 12117 - ], - [ - -61, - 2 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -32, - 10 - ], - [ - -23, - -20 - ], - [ - -30, - 9 - ], - [ - -121, - -6 - ], - [ - -2, - -68 - ], - [ - 9, - -90 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - -47, - 31 - ], - [ - -33, - -5 - ], - [ - -24, - -30 - ], - [ - -32, - 26 - ], - [ - -12, - 39 - ], - [ - -31, - 26 - ] - ], - [ - [ - 12171, - 12041 - ], - [ - -5, - 70 - ], - [ - 19, - 51 - ], - [ - -1, - 40 - ], - [ - 55, - 100 - ], - [ - 10, - 82 - ], - [ - 19, - 29 - ], - [ - 34, - -16 - ], - [ - 29, - 25 - ], - [ - 10, - 31 - ], - [ - 54, - 53 - ], - [ - 13, - 38 - ], - [ - 65, - 50 - ], - [ - 39, - 17 - ], - [ - 17, - -23 - ], - [ - 45, - 0 - ] - ], - [ - [ - 12574, - 12588 - ], - [ - -6, - -58 - ], - [ - 9, - -55 - ], - [ - 40, - -78 - ], - [ - 2, - -58 - ], - [ - 80, - -27 - ], - [ - -1, - -82 - ] - ], - [ - [ - 19008, - 13441 - ], - [ - -2, - -86 - ], - [ - -24, - 19 - ], - [ - 4, - -97 - ] - ], - [ - [ - 18986, - 13277 - ], - [ - -20, - 63 - ], - [ - -4, - 61 - ], - [ - -13, - 57 - ], - [ - -29, - 70 - ], - [ - -64, - 5 - ], - [ - 6, - -49 - ], - [ - -22, - -67 - ], - [ - -29, - 24 - ], - [ - -11, - -22 - ], - [ - -19, - 13 - ], - [ - -27, - 11 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -11, - 99 - ], - [ - -24, - 90 - ], - [ - 12, - 72 - ], - [ - -43, - 33 - ], - [ - 15, - 43 - ], - [ - 44, - 45 - ], - [ - -51, - 64 - ], - [ - 25, - 81 - ], - [ - 55, - -52 - ], - [ - 34, - -6 - ], - [ - 6, - -83 - ], - [ - 66, - -17 - ], - [ - 65, - 2 - ], - [ - 40, - -20 - ], - [ - -32, - -102 - ], - [ - -31, - -7 - ], - [ - -22, - -68 - ], - [ - 38, - -62 - ], - [ - 12, - 76 - ], - [ - 19, - 1 - ], - [ - 37, - -191 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - 20, - -49 - ], - [ - 27, - 8 - ], - [ - 54, - -18 - ], - [ - 102, - -7 - ], - [ - 34, - 31 - ], - [ - 83, - 28 - ], - [ - 50, - -44 - ], - [ - 41, - -12 - ] - ], - [ - [ - 14538, - 16041 - ], - [ - -36, - -50 - ], - [ - -25, - -86 - ], - [ - 22, - -68 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - -60, - 16 - ], - [ - -71, - -38 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - -1, - -60 - ], - [ - -63, - -11 - ], - [ - -49, - 42 - ], - [ - -56, - -33 - ], - [ - -52, - 3 - ] - ], - [ - [ - 14147, - 15756 - ], - [ - -4, - 80 - ], - [ - -35, - 38 - ] - ], - [ - [ - 14108, - 15874 - ], - [ - 11, - 17 - ], - [ - -7, - 15 - ], - [ - 11, - 38 - ], - [ - 27, - 37 - ], - [ - -34, - 52 - ], - [ - -6, - 44 - ], - [ - 17, - 27 - ] - ], - [ - [ - 7143, - 13648 - ], - [ - -17, - -6 - ], - [ - -18, - 69 - ], - [ - -26, - 35 - ], - [ - 15, - 76 - ], - [ - 21, - -5 - ], - [ - 24, - -100 - ], - [ - 1, - -69 - ] - ], - [ - [ - 7123, - 13986 - ], - [ - -76, - -19 - ], - [ - -5, - 44 - ], - [ - 33, - 10 - ], - [ - 46, - -4 - ], - [ - 2, - -31 - ] - ], - [ - [ - 7180, - 13987 - ], - [ - -12, - -85 - ], - [ - -13, - 15 - ], - [ - 1, - 63 - ], - [ - -31, - 47 - ], - [ - 0, - 14 - ], - [ - 55, - -54 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - -13, - -11 - ], - [ - -23, - -28 - ], - [ - -10, - -66 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -61, - 45 - ], - [ - -27, - 50 - ], - [ - -26, - 27 - ], - [ - -32, - 45 - ], - [ - -15, - 37 - ], - [ - -35, - 56 - ], - [ - 15, - 50 - ], - [ - 25, - -28 - ], - [ - 15, - 25 - ], - [ - 33, - 3 - ], - [ - 60, - -20 - ], - [ - 48, - 2 - ], - [ - 31, - -27 - ] - ], - [ - [ - 13872, - 16179 - ], - [ - 26, - 0 - ], - [ - -18, - -52 - ], - [ - 34, - -47 - ], - [ - -10, - -56 - ], - [ - -17, - -5 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - 67, - -1 - ], - [ - 76, - 45 - ], - [ - 16, - 68 - ], - [ - 57, - 39 - ], - [ - -7, - 53 - ] - ], - [ - [ - 14394, - 17469 - ], - [ - 43, - 20 - ], - [ - 75, - 47 - ] - ], - [ - [ - 14512, - 17536 - ], - [ - 73, - -30 - ], - [ - 10, - -30 - ], - [ - 37, - 14 - ], - [ - 68, - -28 - ], - [ - 6, - -57 - ], - [ - -14, - -32 - ], - [ - 43, - -79 - ], - [ - 29, - -22 - ], - [ - -5, - -21 - ], - [ - 47, - -21 - ], - [ - 21, - -32 - ], - [ - -28, - -27 - ], - [ - -56, - 5 - ], - [ - -13, - -12 - ], - [ - 16, - -39 - ], - [ - 17, - -77 - ] - ], - [ - [ - 14763, - 17048 - ], - [ - -60, - -7 - ], - [ - -21, - -27 - ], - [ - -5, - -60 - ], - [ - -27, - 12 - ], - [ - -63, - -6 - ], - [ - -18, - 28 - ], - [ - -27, - -21 - ], - [ - -26, - 17 - ], - [ - -55, - 3 - ], - [ - -78, - 28 - ], - [ - -70, - 10 - ], - [ - -54, - -3 - ], - [ - -38, - -32 - ], - [ - -33, - -5 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - -2, - 53 - ], - [ - -21, - 56 - ], - [ - 42, - 24 - ], - [ - 0, - 48 - ], - [ - -19, - 46 - ], - [ - -3, - 53 - ] - ], - [ - [ - 6333, - 12934 - ], - [ - 0, - 17 - ], - [ - 8, - 6 - ], - [ - 13, - -14 - ], - [ - 25, - 72 - ], - [ - 13, - 2 - ] - ], - [ - [ - 6392, - 13017 - ], - [ - 1, - -18 - ], - [ - 13, - -1 - ], - [ - -1, - -32 - ], - [ - -12, - -52 - ], - [ - 6, - -19 - ], - [ - -7, - -43 - ], - [ - 4, - -11 - ], - [ - -8, - -61 - ], - [ - -13, - -31 - ], - [ - -13, - -4 - ], - [ - -14, - -42 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - -21, - 0 - ], - [ - 6, - 136 - ], - [ - 0, - 95 - ] - ], - [ - [ - 7870, - 8070 - ], - [ - -51, - -17 - ], - [ - -27, - 166 - ], - [ - -37, - 134 - ], - [ - 22, - 116 - ], - [ - -37, - 51 - ], - [ - -9, - 87 - ], - [ - -35, - 81 - ] - ], - [ - [ - 7696, - 8688 - ], - [ - 44, - 130 - ], - [ - -30, - 100 - ], - [ - 16, - 41 - ], - [ - -12, - 44 - ], - [ - 27, - 60 - ], - [ - 2, - 102 - ], - [ - 3, - 85 - ], - [ - 15, - 40 - ], - [ - -60, - 193 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - 52, - -10 - ], - [ - 35, - 3 - ], - [ - 16, - 36 - ], - [ - 61, - 49 - ], - [ - 37, - 45 - ], - [ - 91, - 20 - ], - [ - -8, - -90 - ], - [ - 9, - -46 - ], - [ - -6, - -80 - ], - [ - 76, - -108 - ], - [ - 78, - -20 - ], - [ - 28, - -44 - ], - [ - 47, - -24 - ], - [ - 29, - -35 - ], - [ - 43, - 1 - ], - [ - 41, - -35 - ], - [ - 3, - -70 - ], - [ - 14, - -35 - ], - [ - 0, - -52 - ], - [ - -20, - -2 - ], - [ - 27, - -139 - ], - [ - 134, - -5 - ], - [ - -11, - -70 - ], - [ - 8, - -47 - ], - [ - 38, - -34 - ], - [ - 16, - -74 - ], - [ - -12, - -95 - ], - [ - -19, - -52 - ], - [ - 7, - -69 - ], - [ - -22, - -24 - ] - ], - [ - [ - 8493, - 8377 - ], - [ - -1, - 37 - ], - [ - -65, - 61 - ], - [ - -65, - 2 - ], - [ - -122, - -35 - ], - [ - -33, - -106 - ], - [ - -2, - -64 - ], - [ - -27, - -144 - ] - ], - [ - [ - 8740, - 7709 - ], - [ - 13, - 70 - ], - [ - 10, - 70 - ], - [ - 0, - 66 - ], - [ - -25, - 22 - ], - [ - -26, - -19 - ], - [ - -26, - 5 - ], - [ - -9, - 46 - ], - [ - -6, - 110 - ], - [ - -13, - 36 - ], - [ - -47, - 33 - ], - [ - -29, - -24 - ], - [ - -73, - 23 - ], - [ - 4, - 163 - ], - [ - -20, - 67 - ] - ], - [ - [ - 7701, - 9483 - ], - [ - -40, - -20 - ], - [ - -31, - 13 - ], - [ - 4, - 183 - ], - [ - -57, - -71 - ], - [ - -61, - 3 - ], - [ - -27, - 64 - ], - [ - -46, - 7 - ], - [ - 15, - 52 - ], - [ - -39, - 73 - ], - [ - -29, - 108 - ], - [ - 18, - 22 - ], - [ - 0, - 50 - ], - [ - 42, - 35 - ], - [ - -7, - 65 - ], - [ - 18, - 41 - ], - [ - 5, - 56 - ], - [ - 80, - 82 - ], - [ - 57, - 23 - ], - [ - 10, - 18 - ], - [ - 62, - -5 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - 32, - 328 - ], - [ - 1, - 53 - ], - [ - -11, - 68 - ], - [ - -31, - 44 - ], - [ - 1, - 87 - ], - [ - 39, - 20 - ], - [ - 14, - -13 - ], - [ - 2, - 46 - ], - [ - -40, - 13 - ], - [ - -1, - 75 - ], - [ - 135, - -3 - ], - [ - 24, - 42 - ], - [ - 19, - -38 - ], - [ - 14, - -71 - ], - [ - 13, - 15 - ] - ], - [ - [ - 7886, - 10948 - ], - [ - 38, - -64 - ], - [ - 54, - 8 - ], - [ - 14, - 37 - ], - [ - 52, - 28 - ], - [ - 28, - 19 - ], - [ - 8, - 51 - ], - [ - 50, - 34 - ], - [ - -4, - 25 - ], - [ - -59, - 11 - ], - [ - -9, - 75 - ], - [ - 2, - 81 - ], - [ - -31, - 31 - ], - [ - 13, - 11 - ], - [ - 52, - -15 - ], - [ - 55, - -30 - ], - [ - 21, - 28 - ], - [ - 50, - 19 - ], - [ - 78, - 44 - ], - [ - 25, - 46 - ], - [ - -9, - 34 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - 36, - 5 - ], - [ - 16, - -27 - ], - [ - -9, - -53 - ], - [ - 24, - -18 - ], - [ - 16, - -56 - ], - [ - -19, - -42 - ], - [ - -11, - -102 - ], - [ - 18, - -61 - ], - [ - 5, - -55 - ], - [ - 43, - -57 - ], - [ - 34, - -6 - ], - [ - 7, - 24 - ], - [ - 23, - 5 - ], - [ - 31, - 21 - ], - [ - 23, - 32 - ], - [ - 38, - -10 - ], - [ - 17, - 4 - ] - ], - [ - [ - 8606, - 11025 - ], - [ - 38, - -10 - ], - [ - 6, - 25 - ], - [ - -11, - 24 - ], - [ - 7, - 34 - ], - [ - 28, - -10 - ], - [ - 33, - 12 - ], - [ - 40, - -25 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 30, - -25 - ], - [ - 22, - 32 - ], - [ - 15, - -5 - ], - [ - 10, - -33 - ], - [ - 33, - 8 - ], - [ - 27, - 46 - ], - [ - 21, - 88 - ], - [ - 42, - 110 - ] - ], - [ - [ - 8947, - 11296 - ], - [ - 23, - 5 - ], - [ - 18, - -66 - ], - [ - 39, - -210 - ], - [ - 37, - -19 - ], - [ - 2, - -83 - ], - [ - -53, - -99 - ], - [ - 22, - -36 - ], - [ - 123, - -19 - ], - [ - 3, - -120 - ], - [ - 53, - 78 - ], - [ - 87, - -43 - ], - [ - 116, - -73 - ], - [ - 34, - -70 - ], - [ - -11, - -67 - ], - [ - 81, - 37 - ], - [ - 136, - -63 - ], - [ - 104, - 5 - ], - [ - 103, - -100 - ], - [ - 89, - -134 - ], - [ - 53, - -35 - ], - [ - 60, - -5 - ], - [ - 25, - -37 - ], - [ - 24, - -153 - ], - [ - 12, - -73 - ], - [ - -28, - -198 - ], - [ - -36, - -78 - ], - [ - -98, - -167 - ], - [ - -44, - -136 - ], - [ - -52, - -104 - ], - [ - -17, - -2 - ], - [ - -20, - -89 - ], - [ - 5, - -224 - ], - [ - -19, - -185 - ], - [ - -8, - -79 - ], - [ - -22, - -48 - ], - [ - -12, - -160 - ], - [ - -71, - -157 - ], - [ - -12, - -124 - ], - [ - -56, - -52 - ], - [ - -16, - -71 - ], - [ - -76, - 0 - ], - [ - -110, - -46 - ], - [ - -49, - -54 - ], - [ - -78, - -35 - ], - [ - -82, - -95 - ], - [ - -59, - -119 - ], - [ - -10, - -90 - ], - [ - 11, - -66 - ], - [ - -13, - -121 - ], - [ - -15, - -59 - ], - [ - -49, - -66 - ], - [ - -77, - -211 - ], - [ - -62, - -95 - ], - [ - -47, - -56 - ], - [ - -32, - -114 - ], - [ - -46, - -69 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -19, - 68 - ], - [ - 30, - 57 - ], - [ - -40, - 82 - ], - [ - -55, - 66 - ], - [ - -71, - 77 - ], - [ - -26, - -4 - ], - [ - -70, - 93 - ], - [ - -45, - -13 - ] - ], - [ - [ - 20508, - 11340 - ], - [ - 28, - 45 - ], - [ - 59, - 66 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - -3, - -59 - ], - [ - -4, - -77 - ], - [ - -33, - 4 - ], - [ - -15, - -41 - ], - [ - -32, - 62 - ] - ], - [ - [ - 18940, - 14129 - ], - [ - 28, - -38 - ], - [ - -5, - -74 - ], - [ - -57, - -4 - ], - [ - -59, - 8 - ], - [ - -44, - -18 - ], - [ - -63, - 45 - ], - [ - -1, - 24 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - 46, - 89 - ], - [ - 37, - 31 - ], - [ - 50, - -28 - ], - [ - 37, - -3 - ], - [ - 31, - -32 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - -98, - -88 - ], - [ - -63, - -90 - ], - [ - -23, - -80 - ], - [ - -21, - -45 - ], - [ - -38, - -10 - ], - [ - -12, - -57 - ], - [ - -7, - -37 - ], - [ - -45, - -28 - ], - [ - -57, - 6 - ], - [ - -33, - 33 - ], - [ - -29, - 15 - ], - [ - -34, - -28 - ], - [ - -18, - -58 - ], - [ - -33, - -36 - ], - [ - -34, - -53 - ], - [ - -50, - -12 - ], - [ - -16, - 42 - ], - [ - 7, - 73 - ], - [ - -42, - 114 - ], - [ - -19, - 18 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - 350 - ], - [ - 69, - 4 - ], - [ - 2, - 427 - ], - [ - 52, - 4 - ], - [ - 108, - 42 - ], - [ - 26, - -49 - ], - [ - 45, - 47 - ], - [ - 21, - 0 - ], - [ - 39, - 27 - ] - ], - [ - [ - 14296, - 8678 - ], - [ - 13, - -9 - ] - ], - [ - [ - 14309, - 8669 - ], - [ - 26, - -96 - ], - [ - 14, - -21 - ], - [ - 22, - -69 - ], - [ - 79, - -132 - ], - [ - 30, - -13 - ], - [ - 0, - -42 - ], - [ - 21, - -76 - ], - [ - 54, - -19 - ], - [ - 44, - -54 - ] - ], - [ - [ - 13613, - 11688 - ], - [ - 57, - 9 - ], - [ - 13, - 30 - ], - [ - 12, - -2 - ], - [ - 17, - -27 - ], - [ - 88, - 46 - ], - [ - 29, - 47 - ], - [ - 37, - 42 - ], - [ - -7, - 42 - ], - [ - 20, - 11 - ], - [ - 67, - -8 - ], - [ - 65, - 56 - ], - [ - 51, - 131 - ], - [ - 35, - 48 - ], - [ - 44, - 21 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 8, - -51 - ], - [ - 40, - -75 - ], - [ - 1, - -49 - ], - [ - -12, - -50 - ], - [ - 5, - -38 - ], - [ - 24, - -34 - ], - [ - 53, - -53 - ] - ], - [ - [ - 14260, - 11784 - ], - [ - 38, - -48 - ], - [ - 1, - -39 - ], - [ - 47, - -63 - ], - [ - 29, - -51 - ], - [ - 17, - -72 - ], - [ - 53, - -48 - ], - [ - 11, - -38 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - -23, - -13 - ], - [ - -45, - 3 - ], - [ - -52, - 13 - ], - [ - -26, - -11 - ], - [ - -11, - -29 - ], - [ - -22, - -3 - ], - [ - -28, - 25 - ], - [ - -77, - -60 - ], - [ - -32, - 12 - ], - [ - -10, - -9 - ], - [ - -21, - -72 - ], - [ - -52, - 23 - ], - [ - -51, - 12 - ], - [ - -44, - 44 - ], - [ - -57, - 41 - ], - [ - -38, - -39 - ], - [ - -27, - -61 - ], - [ - -6, - -83 - ] - ], - [ - [ - 13834, - 11218 - ], - [ - -45, - 6 - ], - [ - -47, - 20 - ], - [ - -42, - -63 - ], - [ - -36, - -112 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -8, - 35 - ], - [ - -3, - 55 - ], - [ - -32, - 38 - ], - [ - -25, - 62 - ], - [ - -6, - 43 - ], - [ - -33, - 63 - ], - [ - 5, - 36 - ], - [ - -7, - 50 - ], - [ - 6, - 93 - ], - [ - 17, - 22 - ], - [ - 35, - 122 - ] - ], - [ - [ - 8110, - 16382 - ], - [ - 50, - -16 - ], - [ - 65, - 3 - ], - [ - -35, - -49 - ], - [ - -25, - -8 - ], - [ - -89, - 51 - ], - [ - -17, - 40 - ], - [ - 26, - 37 - ], - [ - 25, - -58 - ] - ], - [ - [ - 8239, - 16688 - ], - [ - -34, - -2 - ], - [ - -90, - 38 - ], - [ - -65, - 56 - ], - [ - 24, - 10 - ], - [ - 92, - -30 - ], - [ - 71, - -50 - ], - [ - 2, - -22 - ] - ], - [ - [ - 3938, - 16617 - ], - [ - -35, - -17 - ], - [ - -115, - 55 - ], - [ - -21, - 42 - ], - [ - -62, - 42 - ], - [ - -13, - 34 - ], - [ - -71, - 22 - ], - [ - -27, - 65 - ], - [ - 6, - 28 - ], - [ - 73, - -26 - ], - [ - 43, - -18 - ], - [ - 65, - -13 - ], - [ - 24, - -41 - ], - [ - 34, - -57 - ], - [ - 70, - -50 - ], - [ - 29, - -66 - ] - ], - [ - [ - 8634, - 16878 - ], - [ - -46, - -105 - ], - [ - 46, - 41 - ], - [ - 47, - -26 - ], - [ - -25, - -42 - ], - [ - 62, - -33 - ], - [ - 32, - 29 - ], - [ - 70, - -36 - ], - [ - -22, - -88 - ], - [ - 49, - 20 - ], - [ - 9, - -63 - ], - [ - 21, - -75 - ], - [ - -29, - -106 - ], - [ - -31, - -4 - ], - [ - -46, - 23 - ], - [ - 15, - 98 - ], - [ - -20, - 15 - ], - [ - -80, - -104 - ], - [ - -42, - 4 - ], - [ - 49, - 56 - ], - [ - -67, - 30 - ], - [ - -75, - -8 - ], - [ - -135, - 4 - ], - [ - -11, - 36 - ], - [ - 44, - 42 - ], - [ - -30, - 32 - ], - [ - 58, - 73 - ], - [ - 72, - 191 - ], - [ - 43, - 68 - ], - [ - 61, - 41 - ], - [ - 32, - -5 - ], - [ - -13, - -32 - ], - [ - -38, - -76 - ] - ], - [ - [ - 3264, - 17296 - ], - [ - 33, - -16 - ], - [ - 66, - 10 - ], - [ - -20, - -136 - ], - [ - 60, - -97 - ], - [ - -28, - 0 - ], - [ - -42, - 55 - ], - [ - -25, - 56 - ], - [ - -36, - 37 - ], - [ - -12, - 53 - ], - [ - 4, - 38 - ] - ], - [ - [ - 7022, - 18254 - ], - [ - -27, - -63 - ], - [ - -31, - 10 - ], - [ - -18, - 36 - ], - [ - 3, - 9 - ], - [ - 27, - 36 - ], - [ - 28, - -3 - ], - [ - 18, - -25 - ] - ], - [ - [ - 6839, - 18321 - ], - [ - -82, - -67 - ], - [ - -49, - 3 - ], - [ - -16, - 33 - ], - [ - 52, - 55 - ], - [ - 96, - -1 - ], - [ - -1, - -23 - ] - ], - [ - [ - 6611, - 18674 - ], - [ - 13, - -53 - ], - [ - 36, - 19 - ], - [ - 40, - -32 - ], - [ - 77, - -41 - ], - [ - 79, - -37 - ], - [ - 7, - -57 - ], - [ - 51, - 9 - ], - [ - 50, - -40 - ], - [ - -62, - -37 - ], - [ - -109, - 28 - ], - [ - -39, - 54 - ], - [ - -69, - -63 - ], - [ - -99, - -62 - ], - [ - -24, - 70 - ], - [ - -95, - -12 - ], - [ - 61, - 59 - ], - [ - 9, - 95 - ], - [ - 24, - 110 - ], - [ - 50, - -10 - ] - ], - [ - [ - 7259, - 18853 - ], - [ - -78, - -6 - ], - [ - -18, - 59 - ], - [ - 30, - 67 - ], - [ - 64, - 17 - ], - [ - 54, - -34 - ], - [ - 1, - -51 - ], - [ - -8, - -17 - ], - [ - -45, - -35 - ] - ], - [ - [ - 5880, - 19088 - ], - [ - -43, - -42 - ], - [ - -94, - 36 - ], - [ - -57, - -13 - ], - [ - -95, - 54 - ], - [ - 61, - 37 - ], - [ - 49, - 52 - ], - [ - 74, - -34 - ], - [ - 42, - -21 - ], - [ - 21, - -23 - ], - [ - 42, - -46 - ] - ], - [ - [ - 3985, - 16676 - ], - [ - -10, - 0 - ], - [ - -135, - 118 - ], - [ - -50, - 52 - ], - [ - -126, - 49 - ], - [ - -39, - 106 - ], - [ - 10, - 74 - ], - [ - -89, - 51 - ], - [ - -12, - 97 - ], - [ - -84, - 87 - ], - [ - -2, - 62 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - 39, - 58 - ], - [ - -2, - 75 - ], - [ - -119, - 77 - ], - [ - -71, - 137 - ], - [ - -43, - 86 - ], - [ - -64, - 54 - ], - [ - -47, - 49 - ], - [ - -37, - 62 - ], - [ - -70, - -39 - ], - [ - -68, - -67 - ], - [ - -62, - 79 - ], - [ - -49, - 52 - ], - [ - -68, - 34 - ], - [ - -68, - 3 - ], - [ - 0, - 683 - ], - [ - 1, - 445 - ] - ], - [ - [ - 2720, - 19160 - ], - [ - 130, - -28 - ], - [ - 109, - -58 - ], - [ - 73, - -11 - ], - [ - 61, - 50 - ], - [ - 85, - 37 - ], - [ - 103, - -14 - ], - [ - 105, - 52 - ], - [ - 114, - 30 - ], - [ - 48, - -49 - ], - [ - 52, - 28 - ], - [ - 15, - 56 - ], - [ - 48, - -13 - ], - [ - 118, - -107 - ], - [ - 93, - 81 - ], - [ - 9, - -91 - ], - [ - 86, - 20 - ], - [ - 26, - 35 - ], - [ - 85, - -7 - ], - [ - 106, - -51 - ], - [ - 164, - -44 - ], - [ - 96, - -20 - ], - [ - 68, - 8 - ], - [ - 94, - -61 - ], - [ - -98, - -60 - ], - [ - 126, - -25 - ], - [ - 188, - 14 - ], - [ - 59, - 21 - ], - [ - 75, - -72 - ], - [ - 75, - 61 - ], - [ - -71, - 50 - ], - [ - 45, - 42 - ], - [ - 85, - 5 - ], - [ - 56, - 12 - ], - [ - 56, - -29 - ], - [ - 70, - -65 - ], - [ - 78, - 10 - ], - [ - 123, - -54 - ], - [ - 109, - 19 - ], - [ - 101, - -3 - ], - [ - -8, - 75 - ], - [ - 62, - 20 - ], - [ - 108, - -40 - ], - [ - 0, - -114 - ], - [ - 44, - 96 - ], - [ - 56, - -3 - ], - [ - 32, - 120 - ], - [ - -75, - 74 - ], - [ - -81, - 49 - ], - [ - 5, - 132 - ], - [ - 83, - 87 - ], - [ - 92, - -19 - ], - [ - 70, - -53 - ], - [ - 95, - -135 - ], - [ - -62, - -59 - ], - [ - 130, - -24 - ], - [ - -1, - -123 - ], - [ - 93, - 94 - ], - [ - 84, - -77 - ], - [ - -21, - -89 - ], - [ - 67, - -81 - ], - [ - 73, - 87 - ], - [ - 51, - 103 - ], - [ - 4, - 132 - ], - [ - 99, - -9 - ], - [ - 103, - -18 - ], - [ - 94, - -60 - ], - [ - 4, - -59 - ], - [ - -52, - -64 - ], - [ - 49, - -64 - ], - [ - -9, - -59 - ], - [ - -136, - -83 - ], - [ - -97, - -19 - ], - [ - -72, - 36 - ], - [ - -21, - -60 - ], - [ - -67, - -101 - ], - [ - -21, - -53 - ], - [ - -80, - -81 - ], - [ - -100, - -8 - ], - [ - -55, - -51 - ], - [ - -5, - -78 - ], - [ - -81, - -15 - ], - [ - -85, - -97 - ], - [ - -76, - -135 - ], - [ - -27, - -94 - ], - [ - -4, - -140 - ], - [ - 103, - -20 - ], - [ - 31, - -112 - ], - [ - 33, - -91 - ], - [ - 97, - 24 - ], - [ - 130, - -52 - ], - [ - 69, - -46 - ], - [ - 50, - -57 - ], - [ - 88, - -33 - ], - [ - 73, - -50 - ], - [ - 116, - -7 - ], - [ - 75, - -12 - ], - [ - -11, - -104 - ], - [ - 22, - -120 - ], - [ - 50, - -134 - ], - [ - 104, - -114 - ], - [ - 54, - 39 - ], - [ - 37, - 123 - ], - [ - -36, - 189 - ], - [ - -49, - 64 - ], - [ - 111, - 56 - ], - [ - 79, - 84 - ], - [ - 39, - 84 - ], - [ - -6, - 80 - ], - [ - -47, - 102 - ], - [ - -85, - 90 - ], - [ - 82, - 126 - ], - [ - -30, - 108 - ], - [ - -23, - 188 - ], - [ - 48, - 27 - ], - [ - 120, - -32 - ], - [ - 72, - -12 - ], - [ - 57, - 32 - ], - [ - 65, - -41 - ], - [ - 86, - -70 - ], - [ - 21, - -46 - ], - [ - 124, - -9 - ], - [ - -2, - -101 - ], - [ - 24, - -152 - ], - [ - 63, - -19 - ], - [ - 51, - -70 - ], - [ - 101, - 66 - ], - [ - 66, - 133 - ], - [ - 46, - 56 - ], - [ - 55, - -108 - ], - [ - 91, - -153 - ], - [ - 77, - -143 - ], - [ - -28, - -76 - ], - [ - 92, - -67 - ], - [ - 63, - -69 - ], - [ - 111, - -31 - ], - [ - 45, - -38 - ], - [ - 28, - -102 - ], - [ - 54, - -16 - ], - [ - 28, - -45 - ], - [ - 5, - -135 - ], - [ - -51, - -45 - ], - [ - -50, - -42 - ], - [ - -115, - -43 - ], - [ - -87, - -98 - ], - [ - -118, - -20 - ], - [ - -149, - 26 - ], - [ - -105, - 0 - ], - [ - -72, - -8 - ], - [ - -58, - -86 - ], - [ - -89, - -53 - ], - [ - -101, - -159 - ], - [ - -80, - -111 - ], - [ - 59, - 20 - ], - [ - 112, - 158 - ], - [ - 146, - 100 - ], - [ - 105, - 12 - ], - [ - 61, - -59 - ], - [ - -66, - -81 - ], - [ - 23, - -129 - ], - [ - 22, - -91 - ], - [ - 91, - -60 - ], - [ - 115, - 18 - ], - [ - 70, - 135 - ], - [ - 5, - -87 - ], - [ - 45, - -44 - ], - [ - -86, - -78 - ], - [ - -155, - -72 - ], - [ - -69, - -48 - ], - [ - -78, - -87 - ], - [ - -53, - 9 - ], - [ - -3, - 102 - ], - [ - 122, - 99 - ], - [ - -112, - -4 - ], - [ - -78, - -15 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - -45, - 68 - ], - [ - 0, - 164 - ], - [ - -31, - 34 - ], - [ - -47, - -20 - ], - [ - -23, - 31 - ], - [ - -53, - -90 - ], - [ - -21, - -93 - ], - [ - -25, - -55 - ], - [ - -30, - -19 - ], - [ - -22, - -6 - ], - [ - -7, - -29 - ], - [ - -128, - 0 - ], - [ - -106, - -1 - ], - [ - -32, - -22 - ], - [ - -73, - -87 - ], - [ - -9, - -9 - ], - [ - -22, - -47 - ], - [ - -64, - 0 - ], - [ - -69, - 0 - ], - [ - -31, - -19 - ], - [ - 11, - -24 - ], - [ - 6, - -36 - ], - [ - -1, - -13 - ], - [ - -91, - -59 - ], - [ - -72, - -19 - ], - [ - -81, - -64 - ], - [ - -18, - 0 - ], - [ - -23, - 19 - ], - [ - -8, - 17 - ], - [ - 1, - 12 - ], - [ - 16, - 42 - ], - [ - 32, - 66 - ], - [ - 21, - 71 - ], - [ - -14, - 105 - ], - [ - -15, - 108 - ], - [ - -73, - 57 - ], - [ - 9, - 21 - ], - [ - -10, - 15 - ], - [ - -19, - 0 - ], - [ - -14, - 19 - ], - [ - -4, - 28 - ], - [ - -13, - -12 - ], - [ - -19, - 3 - ], - [ - 4, - 12 - ], - [ - -16, - 12 - ], - [ - -7, - 32 - ], - [ - -54, - 38 - ], - [ - -57, - 40 - ], - [ - -68, - 46 - ], - [ - -65, - 44 - ], - [ - -63, - -34 - ], - [ - -22, - -1 - ], - [ - -86, - 31 - ], - [ - -57, - -16 - ], - [ - -67, - 38 - ], - [ - -71, - 19 - ], - [ - -49, - 7 - ], - [ - -22, - 20 - ], - [ - -12, - 66 - ], - [ - -24, - 0 - ], - [ - 0, - -46 - ], - [ - -144, - 0 - ], - [ - -239, - 0 - ], - [ - -237, - 0 - ], - [ - -209, - 0 - ], - [ - -209, - 0 - ], - [ - -206, - 0 - ], - [ - -212, - 0 - ], - [ - -69, - 0 - ], - [ - -207, - 0 - ], - [ - -197, - 0 - ] - ], - [ - [ - 4589, - 19569 - ], - [ - -35, - -56 - ], - [ - 155, - 37 - ], - [ - 97, - -61 - ], - [ - 79, - 61 - ], - [ - 64, - -39 - ], - [ - 57, - -118 - ], - [ - 35, - 50 - ], - [ - -50, - 123 - ], - [ - 62, - 17 - ], - [ - 69, - -19 - ], - [ - 78, - -48 - ], - [ - 44, - -117 - ], - [ - 21, - -85 - ], - [ - 118, - -59 - ], - [ - 125, - -57 - ], - [ - -7, - -53 - ], - [ - -115, - -9 - ], - [ - 45, - -47 - ], - [ - -24, - -44 - ], - [ - -126, - 19 - ], - [ - -120, - 33 - ], - [ - -81, - -8 - ], - [ - -131, - -40 - ], - [ - -176, - -18 - ], - [ - -124, - -12 - ], - [ - -38, - 57 - ], - [ - -95, - 33 - ], - [ - -62, - -14 - ], - [ - -86, - 95 - ], - [ - 46, - 13 - ], - [ - 108, - 20 - ], - [ - 98, - -5 - ], - [ - 91, - 21 - ], - [ - -135, - 28 - ], - [ - -149, - -10 - ], - [ - -98, - 3 - ], - [ - -37, - 44 - ], - [ - 161, - 48 - ], - [ - -107, - -2 - ], - [ - -122, - 32 - ], - [ - 59, - 90 - ], - [ - 48, - 48 - ], - [ - 187, - 73 - ], - [ - 71, - -24 - ] - ], - [ - [ - 5263, - 19605 - ], - [ - -61, - -79 - ], - [ - -109, - 84 - ], - [ - 24, - 17 - ], - [ - 93, - 5 - ], - [ - 53, - -27 - ] - ], - [ - [ - 7226, - 19567 - ], - [ - 6, - -33 - ], - [ - -74, - 4 - ], - [ - -75, - 2 - ], - [ - -76, - -16 - ], - [ - -21, - 7 - ], - [ - -76, - 64 - ], - [ - 3, - 43 - ], - [ - 33, - 8 - ], - [ - 160, - -13 - ], - [ - 120, - -66 - ] - ], - [ - [ - 6513, - 19574 - ], - [ - 55, - -75 - ], - [ - 65, - 97 - ], - [ - 176, - 49 - ], - [ - 120, - -124 - ], - [ - -10, - -79 - ], - [ - 138, - 35 - ], - [ - 65, - 48 - ], - [ - 155, - -61 - ], - [ - 96, - -57 - ], - [ - 9, - -52 - ], - [ - 130, - 27 - ], - [ - 72, - -77 - ], - [ - 169, - -47 - ], - [ - 60, - -48 - ], - [ - 66, - -113 - ], - [ - -128, - -56 - ], - [ - 164, - -78 - ], - [ - 111, - -26 - ], - [ - 100, - -110 - ], - [ - 110, - -8 - ], - [ - -22, - -85 - ], - [ - -122, - -139 - ], - [ - -86, - 51 - ], - [ - -110, - 116 - ], - [ - -90, - -15 - ], - [ - -9, - -69 - ], - [ - 74, - -70 - ], - [ - 94, - -55 - ], - [ - 29, - -32 - ], - [ - 46, - -119 - ], - [ - -25, - -86 - ], - [ - -87, - 33 - ], - [ - -175, - 96 - ], - [ - 98, - -104 - ], - [ - 73, - -72 - ], - [ - 11, - -42 - ], - [ - -189, - 48 - ], - [ - -149, - 70 - ], - [ - -85, - 58 - ], - [ - 24, - 34 - ], - [ - -104, - 61 - ], - [ - -101, - 59 - ], - [ - 1, - -35 - ], - [ - -202, - -19 - ], - [ - -59, - 41 - ], - [ - 46, - 88 - ], - [ - 131, - 2 - ], - [ - 144, - 16 - ], - [ - -23, - 43 - ], - [ - 24, - 59 - ], - [ - 90, - 117 - ], - [ - -19, - 53 - ], - [ - -27, - 41 - ], - [ - -107, - 59 - ], - [ - -141, - 40 - ], - [ - 45, - 31 - ], - [ - -74, - 74 - ], - [ - -62, - 7 - ], - [ - -54, - 41 - ], - [ - -38, - -35 - ], - [ - -126, - -16 - ], - [ - -254, - 27 - ], - [ - -147, - 35 - ], - [ - -113, - 18 - ], - [ - -58, - 42 - ], - [ - 73, - 55 - ], - [ - -99, - 1 - ], - [ - -23, - 121 - ], - [ - 54, - 107 - ], - [ - 72, - 49 - ], - [ - 180, - 32 - ], - [ - -52, - -77 - ] - ], - [ - [ - 5552, - 19656 - ], - [ - 83, - -25 - ], - [ - 124, - 15 - ], - [ - 18, - -35 - ], - [ - -65, - -57 - ], - [ - 106, - -52 - ], - [ - -13, - -108 - ], - [ - -114, - -46 - ], - [ - -67, - 10 - ], - [ - -48, - 46 - ], - [ - -174, - 92 - ], - [ - 2, - 39 - ], - [ - 142, - -15 - ], - [ - -77, - 78 - ], - [ - 83, - 58 - ] - ], - [ - [ - 6051, - 19528 - ], - [ - -75, - -90 - ], - [ - -79, - 4 - ], - [ - -44, - 106 - ], - [ - 1, - 59 - ], - [ - 37, - 51 - ], - [ - 69, - 33 - ], - [ - 145, - -4 - ], - [ - 133, - -29 - ], - [ - -104, - -107 - ], - [ - -83, - -23 - ] - ], - [ - [ - 4150, - 19361 - ], - [ - -183, - -58 - ], - [ - -37, - 53 - ], - [ - -161, - 63 - ], - [ - 30, - 51 - ], - [ - 48, - 88 - ], - [ - 61, - 78 - ], - [ - -68, - 74 - ], - [ - 235, - 19 - ], - [ - 100, - -25 - ], - [ - 178, - -7 - ], - [ - 68, - -35 - ], - [ - 74, - -50 - ], - [ - -87, - -30 - ], - [ - -171, - -85 - ], - [ - -87, - -84 - ], - [ - 0, - -52 - ] - ], - [ - [ - 6022, - 19792 - ], - [ - -38, - -46 - ], - [ - -101, - 9 - ], - [ - -85, - 31 - ], - [ - 37, - 54 - ], - [ - 101, - 32 - ], - [ - 60, - -42 - ], - [ - 26, - -38 - ] - ], - [ - [ - 5681, - 20001 - ], - [ - 54, - -55 - ], - [ - 2, - -62 - ], - [ - -32, - -89 - ], - [ - -115, - -12 - ], - [ - -75, - 19 - ], - [ - 2, - 70 - ], - [ - -115, - -10 - ], - [ - -4, - 93 - ], - [ - 75, - -4 - ], - [ - 105, - 41 - ], - [ - 98, - -7 - ], - [ - 5, - 16 - ] - ], - [ - [ - 5004, - 19939 - ], - [ - 28, - -43 - ], - [ - 62, - 20 - ], - [ - 73, - -5 - ], - [ - 12, - -59 - ], - [ - -42, - -57 - ], - [ - -237, - -18 - ], - [ - -175, - -52 - ], - [ - -106, - -3 - ], - [ - -9, - 39 - ], - [ - 145, - 53 - ], - [ - -315, - -14 - ], - [ - -98, - 22 - ], - [ - 95, - 117 - ], - [ - 66, - 33 - ], - [ - 196, - -40 - ], - [ - 124, - -71 - ], - [ - 122, - -9 - ], - [ - -100, - 114 - ], - [ - 64, - 44 - ], - [ - 72, - -14 - ], - [ - 23, - -57 - ] - ], - [ - [ - 5947, - 20047 - ], - [ - 78, - -39 - ], - [ - 137, - 0 - ], - [ - 60, - -39 - ], - [ - -16, - -45 - ], - [ - 80, - -27 - ], - [ - 44, - -29 - ], - [ - 94, - -5 - ], - [ - 102, - -10 - ], - [ - 111, - 26 - ], - [ - 142, - 10 - ], - [ - 113, - -8 - ], - [ - 75, - -46 - ], - [ - 15, - -49 - ], - [ - -43, - -32 - ], - [ - -104, - -26 - ], - [ - -89, - 15 - ], - [ - -200, - -19 - ], - [ - -143, - -2 - ], - [ - -113, - 15 - ], - [ - -185, - 38 - ], - [ - -24, - 66 - ], - [ - -9, - 60 - ], - [ - -70, - 52 - ], - [ - -144, - 15 - ], - [ - -81, - 37 - ], - [ - 27, - 49 - ], - [ - 143, - -7 - ] - ], - [ - [ - 4447, - 20112 - ], - [ - -9, - -92 - ], - [ - -54, - -42 - ], - [ - -65, - -5 - ], - [ - -129, - -52 - ], - [ - -112, - -18 - ], - [ - -95, - 26 - ], - [ - 119, - 90 - ], - [ - 143, - 77 - ], - [ - 107, - -1 - ], - [ - 95, - 17 - ] - ], - [ - [ - 6006, - 20097 - ], - [ - -32, - -3 - ], - [ - -130, - 7 - ], - [ - -19, - 34 - ], - [ - 140, - -2 - ], - [ - 49, - -22 - ], - [ - -8, - -14 - ] - ], - [ - [ - 4867, - 20118 - ], - [ - -130, - -34 - ], - [ - -104, - 39 - ], - [ - 57, - 38 - ], - [ - 101, - 12 - ], - [ - 99, - -19 - ], - [ - -23, - -36 - ] - ], - [ - [ - 4903, - 20227 - ], - [ - -85, - -23 - ], - [ - -116, - 0 - ], - [ - 2, - 17 - ], - [ - 71, - 36 - ], - [ - 37, - -6 - ], - [ - 91, - -24 - ] - ], - [ - [ - 5867, - 20162 - ], - [ - -103, - -25 - ], - [ - -57, - 28 - ], - [ - -29, - 45 - ], - [ - -6, - 49 - ], - [ - 90, - -4 - ], - [ - 41, - -8 - ], - [ - 83, - -42 - ], - [ - -19, - -43 - ] - ], - [ - [ - 5572, - 20194 - ], - [ - 28, - -50 - ], - [ - -114, - 13 - ], - [ - -115, - 39 - ], - [ - -155, - 4 - ], - [ - 67, - 36 - ], - [ - -84, - 29 - ], - [ - -5, - 46 - ], - [ - 137, - -16 - ], - [ - 188, - -44 - ], - [ - 53, - -57 - ] - ], - [ - [ - 6481, - 20354 - ], - [ - 85, - -39 - ], - [ - -96, - -36 - ], - [ - -129, - -90 - ], - [ - -123, - -8 - ], - [ - -145, - 15 - ], - [ - -75, - 49 - ], - [ - 1, - 43 - ], - [ - 56, - 32 - ], - [ - -128, - -1 - ], - [ - -77, - 40 - ], - [ - -44, - 55 - ], - [ - 48, - 53 - ], - [ - 49, - 37 - ], - [ - 71, - 8 - ], - [ - -30, - 27 - ], - [ - 162, - 7 - ], - [ - 89, - -65 - ], - [ - 117, - -25 - ], - [ - 114, - -23 - ], - [ - 55, - -79 - ] - ], - [ - [ - 7772, - 20767 - ], - [ - 187, - -9 - ], - [ - 149, - -15 - ], - [ - 128, - -33 - ], - [ - -3, - -32 - ], - [ - -170, - -52 - ], - [ - -169, - -24 - ], - [ - -63, - -27 - ], - [ - 152, - 0 - ], - [ - -165, - -72 - ], - [ - -113, - -34 - ], - [ - -119, - -98 - ], - [ - -144, - -20 - ], - [ - -45, - -25 - ], - [ - -211, - -13 - ], - [ - 96, - -15 - ], - [ - -48, - -21 - ], - [ - 58, - -59 - ], - [ - -66, - -41 - ], - [ - -108, - -34 - ], - [ - -33, - -47 - ], - [ - -97, - -36 - ], - [ - 9, - -27 - ], - [ - 119, - 4 - ], - [ - 2, - -29 - ], - [ - -186, - -72 - ], - [ - -182, - 33 - ], - [ - -205, - -18 - ], - [ - -104, - 14 - ], - [ - -132, - 6 - ], - [ - -8, - 58 - ], - [ - 128, - 27 - ], - [ - -34, - 87 - ], - [ - 43, - 8 - ], - [ - 186, - -52 - ], - [ - -95, - 77 - ], - [ - -113, - 23 - ], - [ - 56, - 47 - ], - [ - 124, - 28 - ], - [ - 20, - 42 - ], - [ - -99, - 47 - ], - [ - -29, - 62 - ], - [ - 190, - -5 - ], - [ - 55, - -13 - ], - [ - 109, - 43 - ], - [ - -157, - 14 - ], - [ - -244, - -7 - ], - [ - -123, - 40 - ], - [ - -58, - 49 - ], - [ - -82, - 35 - ], - [ - -15, - 41 - ], - [ - 104, - 23 - ], - [ - 81, - 4 - ], - [ - 137, - 19 - ], - [ - 102, - 45 - ], - [ - 87, - -6 - ], - [ - 75, - -34 - ], - [ - 53, - 65 - ], - [ - 92, - 19 - ], - [ - 125, - 13 - ], - [ - 213, - 5 - ], - [ - 37, - -13 - ], - [ - 202, - 21 - ], - [ - 151, - -8 - ], - [ - 150, - -8 - ] - ], - [ - [ - 13275, - 16423 - ], - [ - -5, - -49 - ], - [ - -31, - -20 - ], - [ - -51, - 15 - ], - [ - -15, - -49 - ], - [ - -34, - -4 - ], - [ - -12, - 19 - ], - [ - -39, - -40 - ], - [ - -33, - -6 - ], - [ - -30, - 26 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -24, - 52 - ], - [ - -34, - -18 - ], - [ - 1, - 54 - ], - [ - 51, - 67 - ], - [ - -2, - 31 - ], - [ - 32, - -11 - ], - [ - 19, - 20 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 59, - -1 - ], - [ - 15, - 26 - ], - [ - 74, - -36 - ] - ], - [ - [ - 7880, - 4211 - ], - [ - -23, - -48 - ], - [ - -60, - -37 - ], - [ - -34, - 3 - ], - [ - -42, - 10 - ], - [ - -50, - 36 - ], - [ - -73, - 17 - ], - [ - -88, - 67 - ], - [ - -71, - 65 - ], - [ - -96, - 134 - ], - [ - 57, - -25 - ], - [ - 98, - -80 - ], - [ - 93, - -43 - ], - [ - 36, - 55 - ], - [ - 22, - 82 - ], - [ - 65, - 50 - ], - [ - 49, - -15 - ] - ], - [ - [ - 7767, - 4523 - ], - [ - -62, - 1 - ], - [ - -33, - -30 - ], - [ - -63, - -43 - ], - [ - -11, - -112 - ], - [ - -30, - -3 - ], - [ - -78, - 39 - ], - [ - -80, - 84 - ], - [ - -87, - 68 - ], - [ - -22, - 76 - ], - [ - 20, - 71 - ], - [ - -35, - 79 - ], - [ - -9, - 205 - ], - [ - 30, - 115 - ], - [ - 73, - 93 - ], - [ - -106, - 35 - ], - [ - 67, - 106 - ], - [ - 24, - 199 - ], - [ - 77, - -42 - ], - [ - 36, - 249 - ], - [ - -46, - 31 - ], - [ - -22, - -149 - ], - [ - -44, - 17 - ], - [ - 22, - 171 - ], - [ - 24, - 222 - ], - [ - 32, - 82 - ], - [ - -20, - 117 - ], - [ - -6, - 136 - ], - [ - 29, - 3 - ], - [ - 43, - 194 - ], - [ - 48, - 192 - ], - [ - 30, - 179 - ], - [ - -16, - 180 - ], - [ - 20, - 99 - ], - [ - -8, - 148 - ], - [ - 41, - 146 - ], - [ - 12, - 232 - ], - [ - 23, - 249 - ], - [ - 22, - 269 - ], - [ - -6, - 196 - ], - [ - -14, - 169 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - 36, - 31 - ], - [ - 18, - 61 - ] - ], - [ - [ - 17774, - 15286 - ], - [ - -10, - 69 - ], - [ - 2, - 46 - ], - [ - -42, - 28 - ], - [ - -23, - -12 - ], - [ - -18, - 111 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - 20, - 27 - ], - [ - -9, - 28 - ], - [ - 66, - 57 - ], - [ - 48, - 23 - ], - [ - 74, - -16 - ], - [ - 26, - 77 - ], - [ - 90, - 14 - ], - [ - 25, - 48 - ], - [ - 109, - 65 - ], - [ - 10, - 27 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -5, - 68 - ], - [ - 48, - 31 - ], - [ - -63, - 209 - ], - [ - 138, - 48 - ], - [ - 36, - 27 - ], - [ - 50, - 214 - ], - [ - 138, - -39 - ], - [ - 39, - 54 - ], - [ - 3, - 120 - ], - [ - 58, - 12 - ], - [ - 53, - 79 - ] - ], - [ - [ - 18637, - 16701 - ], - [ - 27, - 10 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 19, - -83 - ], - [ - 58, - -64 - ], - [ - 100, - -45 - ], - [ - 48, - -97 - ], - [ - -27, - -140 - ], - [ - 25, - -52 - ], - [ - 83, - -20 - ], - [ - 94, - -17 - ], - [ - 84, - -75 - ], - [ - 43, - -13 - ], - [ - 32, - -111 - ], - [ - 41, - -71 - ], - [ - 77, - 3 - ], - [ - 144, - -27 - ], - [ - 92, - 17 - ], - [ - 69, - -18 - ], - [ - 103, - -73 - ], - [ - 85, - 0 - ], - [ - 30, - -37 - ], - [ - 82, - 64 - ], - [ - 112, - 42 - ], - [ - 105, - 4 - ], - [ - 81, - 42 - ], - [ - 50, - 65 - ], - [ - 49, - 40 - ], - [ - -11, - 40 - ], - [ - -23, - 46 - ], - [ - 37, - 77 - ], - [ - 39, - -11 - ], - [ - 72, - -24 - ], - [ - 69, - 64 - ], - [ - 107, - 46 - ], - [ - 51, - 79 - ], - [ - 49, - 34 - ], - [ - 101, - 16 - ], - [ - 55, - -13 - ], - [ - 8, - 42 - ], - [ - -64, - 84 - ], - [ - -55, - 39 - ], - [ - -54, - -45 - ], - [ - -69, - 19 - ], - [ - -39, - -15 - ], - [ - -18, - 49 - ], - [ - 49, - 120 - ], - [ - 34, - 90 - ] - ], - [ - [ - 20681, - 16782 - ], - [ - 84, - -45 - ], - [ - 98, - 76 - ], - [ - -1, - 53 - ], - [ - 63, - 127 - ], - [ - 39, - 38 - ], - [ - -1, - 67 - ], - [ - -38, - 28 - ], - [ - 57, - 60 - ], - [ - 87, - 21 - ], - [ - 92, - 4 - ], - [ - 105, - -36 - ], - [ - 61, - -44 - ], - [ - 43, - -121 - ], - [ - 26, - -52 - ], - [ - 24, - -74 - ], - [ - 26, - -117 - ], - [ - 122, - -38 - ], - [ - 82, - -86 - ], - [ - 28, - -112 - ], - [ - 106, - -1 - ], - [ - 61, - 48 - ], - [ - 115, - 35 - ], - [ - -37, - -108 - ], - [ - -27, - -44 - ], - [ - -24, - -131 - ], - [ - -47, - -117 - ], - [ - -84, - 21 - ], - [ - -60, - -42 - ], - [ - 18, - -103 - ], - [ - -10, - -142 - ], - [ - -35, - -3 - ], - [ - 0, - -61 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - -45, - 71 - ], - [ - -28, - -67 - ], - [ - -107, - -52 - ], - [ - 11, - -63 - ], - [ - -61, - 4 - ], - [ - -33, - 38 - ], - [ - -48, - -85 - ], - [ - -76, - -65 - ], - [ - -57, - -77 - ] - ], - [ - [ - 21210, - 15587 - ], - [ - -98, - -35 - ], - [ - -51, - -56 - ], - [ - -75, - -32 - ], - [ - 37, - 55 - ], - [ - -15, - 47 - ], - [ - 56, - 81 - ], - [ - -37, - 62 - ], - [ - -61, - -42 - ], - [ - -79, - -83 - ], - [ - -43, - -78 - ], - [ - -68, - -6 - ], - [ - -35, - -55 - ], - [ - 36, - -82 - ], - [ - 57, - -19 - ], - [ - 3, - -54 - ], - [ - 55, - -35 - ], - [ - 78, - 85 - ], - [ - 62, - -46 - ], - [ - 45, - -3 - ], - [ - 11, - -63 - ], - [ - -99, - -34 - ], - [ - -32, - -65 - ], - [ - -68, - -60 - ], - [ - -36, - -84 - ], - [ - 75, - -66 - ], - [ - 28, - -118 - ], - [ - 42, - -110 - ], - [ - 48, - -92 - ], - [ - -2, - -89 - ], - [ - -43, - -33 - ], - [ - 16, - -64 - ], - [ - 41, - -37 - ], - [ - -10, - -98 - ], - [ - -18, - -95 - ], - [ - -39, - -10 - ], - [ - -51, - -130 - ], - [ - -56, - -158 - ], - [ - -65, - -143 - ], - [ - -96, - -111 - ], - [ - -97, - -101 - ], - [ - -79, - -13 - ], - [ - -42, - -54 - ], - [ - -24, - 39 - ], - [ - -40, - -59 - ], - [ - -97, - -60 - ], - [ - -74, - -19 - ], - [ - -24, - -127 - ], - [ - -38, - -7 - ], - [ - -19, - 88 - ], - [ - 17, - 46 - ], - [ - -94, - 38 - ], - [ - -33, - -19 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -70, - 31 - ], - [ - -33, - 49 - ], - [ - 11, - 69 - ], - [ - -64, - 22 - ], - [ - -33, - 45 - ], - [ - -60, - -64 - ], - [ - -67, - -14 - ], - [ - -56, - 1 - ], - [ - -37, - -30 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - -37, - -17 - ], - [ - 11, - -138 - ], - [ - -37, - 4 - ], - [ - -6, - 28 - ] - ], - [ - [ - 19601, - 13369 - ], - [ - -2, - 50 - ], - [ - -52, - -35 - ], - [ - -30, - 22 - ], - [ - -52, - 45 - ], - [ - 21, - 99 - ], - [ - -44, - 24 - ], - [ - -17, - 110 - ], - [ - -74, - -20 - ], - [ - 9, - 142 - ], - [ - 66, - 101 - ], - [ - 3, - 99 - ], - [ - -2, - 91 - ], - [ - -31, - 29 - ], - [ - -23, - 71 - ], - [ - -41, - -9 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - -75, - 18 - ], - [ - 23, - 50 - ], - [ - -32, - 75 - ], - [ - -50, - -51 - ], - [ - -58, - 30 - ], - [ - -81, - -77 - ], - [ - -63, - -89 - ], - [ - -56, - -15 - ] - ], - [ - [ - 18739, - 14072 - ], - [ - -6, - 95 - ], - [ - -43, - -25 - ] - ], - [ - [ - 18690, - 14142 - ], - [ - -81, - 11 - ], - [ - -79, - 28 - ], - [ - -56, - 52 - ], - [ - -55, - 24 - ], - [ - -23, - 58 - ], - [ - -39, - 17 - ], - [ - -71, - 78 - ], - [ - -55, - 37 - ], - [ - -29, - -29 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -97, - 84 - ], - [ - -69, - 76 - ], - [ - -19, - 132 - ], - [ - 50, - -16 - ], - [ - 2, - 61 - ], - [ - -28, - 62 - ], - [ - 7, - 98 - ], - [ - -75, - 140 - ] - ], - [ - [ - 17973, - 15055 - ], - [ - -114, - 49 - ], - [ - -21, - 92 - ], - [ - -51, - 56 - ] - ], - [ - [ - 20239, - 13038 - ], - [ - -60, - -58 - ], - [ - -57, - 38 - ], - [ - -2, - 103 - ], - [ - 34, - 54 - ], - [ - 76, - 34 - ], - [ - 40, - -3 - ], - [ - 16, - -46 - ], - [ - -31, - -53 - ], - [ - -16, - -69 - ] - ], - [ - [ - 12350, - 11954 - ], - [ - 19, - -171 - ], - [ - -29, - -100 - ], - [ - -19, - -136 - ], - [ - 31, - -103 - ], - [ - -4, - -48 - ] - ], - [ - [ - 12348, - 11396 - ], - [ - -31, - -1 - ], - [ - -49, - 24 - ], - [ - -45, - -2 - ], - [ - -82, - -21 - ], - [ - -49, - -34 - ], - [ - -69, - -44 - ], - [ - -13, - 3 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - 5, - 99 - ], - [ - 7, - 15 - ], - [ - -2, - 47 - ], - [ - -30, - 50 - ], - [ - -22, - 8 - ], - [ - -20, - 33 - ], - [ - 15, - 53 - ], - [ - -7, - 58 - ], - [ - 3, - 35 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - 11, - 0 - ], - [ - 4, - 53 - ], - [ - -5, - 23 - ], - [ - 7, - 17 - ], - [ - 26, - 14 - ], - [ - -18, - 96 - ], - [ - -16, - 50 - ], - [ - 6, - 40 - ], - [ - 14, - 10 - ] - ], - [ - [ - 11988, - 12022 - ], - [ - 9, - 11 - ], - [ - 19, - -18 - ], - [ - 54, - -1 - ], - [ - 13, - 35 - ], - [ - 12, - -3 - ], - [ - 20, - 14 - ], - [ - 11, - -52 - ], - [ - 16, - 16 - ], - [ - 29, - 17 - ] - ], - [ - [ - 13664, - 11069 - ], - [ - -5, - -65 - ], - [ - -56, - 29 - ], - [ - -56, - 31 - ], - [ - -88, - 5 - ] - ], - [ - [ - 13459, - 11069 - ], - [ - -9, - 7 - ], - [ - -41, - -16 - ], - [ - -42, - 16 - ], - [ - -33, - -8 - ] - ], - [ - [ - 13334, - 11068 - ], - [ - -114, - 3 - ] - ], - [ - [ - 13220, - 11071 - ], - [ - 10, - 95 - ], - [ - -27, - 79 - ], - [ - -32, - 21 - ], - [ - -14, - 53 - ], - [ - -18, - 18 - ], - [ - 1, - 33 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - 18, - 85 - ], - [ - 33, - 115 - ], - [ - 20, - 1 - ], - [ - 42, - 71 - ], - [ - 26, - 2 - ], - [ - 39, - -50 - ], - [ - 48, - 41 - ], - [ - 7, - 50 - ], - [ - 15, - 48 - ], - [ - 11, - 61 - ], - [ - 38, - 49 - ], - [ - 14, - 84 - ], - [ - 14, - 27 - ], - [ - 10, - 62 - ], - [ - 19, - 77 - ], - [ - 58, - 93 - ], - [ - 4, - 39 - ], - [ - 8, - 22 - ], - [ - -28, - 48 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - 2, - 38 - ], - [ - 20, - 7 - ] - ], - [ - [ - 13558, - 12340 - ], - [ - 28, - -77 - ], - [ - 4, - -79 - ], - [ - -2, - -80 - ], - [ - 38, - -109 - ], - [ - -39, - 1 - ], - [ - -20, - -9 - ], - [ - -32, - 12 - ], - [ - -15, - -56 - ], - [ - 41, - -70 - ], - [ - 31, - -21 - ], - [ - 10, - -49 - ], - [ - 22, - -83 - ], - [ - -11, - -32 - ] - ], - [ - [ - 13406, - 10065 - ], - [ - -9, - 38 - ] - ], - [ - [ - 13453, - 10224 - ], - [ - 19, - -13 - ], - [ - 24, - 46 - ], - [ - 38, - -1 - ], - [ - 4, - -34 - ], - [ - 26, - -21 - ], - [ - 41, - 75 - ], - [ - 41, - 59 - ], - [ - 17, - 38 - ], - [ - -2, - 99 - ], - [ - 30, - 116 - ], - [ - 32, - 62 - ], - [ - 46, - 58 - ], - [ - 8, - 38 - ], - [ - 2, - 44 - ], - [ - 11, - 42 - ], - [ - -3, - 68 - ], - [ - 8, - 106 - ], - [ - 14, - 75 - ], - [ - 21, - 64 - ], - [ - 4, - 73 - ] - ], - [ - [ - 14456, - 11425 - ], - [ - 42, - -99 - ], - [ - 31, - -14 - ], - [ - 19, - 20 - ], - [ - 32, - -8 - ], - [ - 39, - 25 - ], - [ - 17, - -51 - ], - [ - 61, - -80 - ] - ], - [ - [ - 14697, - 11218 - ], - [ - -4, - -140 - ], - [ - 28, - -16 - ], - [ - -23, - -43 - ], - [ - -27, - -32 - ], - [ - -26, - -62 - ], - [ - -15, - -56 - ], - [ - -4, - -96 - ], - [ - -16, - -46 - ], - [ - -1, - -91 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - -20, - -33 - ], - [ - -2, - -72 - ], - [ - -10, - -9 - ], - [ - -6, - -65 - ] - ], - [ - [ - 14593, - 10257 - ], - [ - 12, - -110 - ], - [ - -7, - -62 - ], - [ - 14, - -70 - ], - [ - 41, - -67 - ], - [ - 37, - -151 - ] - ], - [ - [ - 14690, - 9797 - ], - [ - -27, - 12 - ], - [ - -94, - -20 - ], - [ - -18, - -15 - ], - [ - -20, - -76 - ], - [ - 15, - -53 - ], - [ - -12, - -142 - ], - [ - -9, - -121 - ], - [ - 19, - -21 - ], - [ - 49, - -47 - ], - [ - 19, - 22 - ], - [ - 6, - -129 - ], - [ - -54, - 1 - ], - [ - -28, - 66 - ], - [ - -26, - 51 - ], - [ - -53, - 17 - ], - [ - -16, - 63 - ], - [ - -43, - -38 - ], - [ - -55, - 16 - ], - [ - -24, - 55 - ], - [ - -44, - 11 - ], - [ - -33, - -3 - ], - [ - -4, - 37 - ], - [ - -24, - 3 - ] - ], - [ - [ - 13378, - 10193 - ], - [ - -57, - 127 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - 53, - 66 - ], - [ - -26, - 79 - ], - [ - 24, - 31 - ], - [ - 47, - 14 - ], - [ - 5, - 53 - ], - [ - 37, - -57 - ], - [ - 62, - -5 - ], - [ - 21, - 56 - ], - [ - 9, - 80 - ], - [ - -8, - 94 - ], - [ - -33, - 71 - ], - [ - 31, - 139 - ], - [ - -18, - 24 - ], - [ - -52, - -10 - ], - [ - -19, - 62 - ], - [ - 5, - 52 - ] - ], - [ - [ - 7675, - 10282 - ], - [ - -35, - 63 - ], - [ - -20, - 3 - ], - [ - 45, - 122 - ], - [ - -54, - 56 - ], - [ - -42, - -10 - ], - [ - -25, - 21 - ], - [ - -38, - -32 - ], - [ - -52, - 15 - ], - [ - -41, - 126 - ], - [ - -32, - 31 - ], - [ - -23, - 57 - ], - [ - -46, - 56 - ], - [ - -19, - -11 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - -29, - 28 - ], - [ - -35, - 40 - ], - [ - -20, - -19 - ], - [ - -59, - 17 - ], - [ - -17, - 51 - ], - [ - -13, - -2 - ], - [ - -69, - 69 - ] - ], - [ - [ - 7051, - 10963 - ], - [ - -10, - 37 - ], - [ - 26, - 9 - ], - [ - -3, - 60 - ], - [ - 16, - 44 - ], - [ - 35, - 8 - ], - [ - 29, - 75 - ], - [ - 27, - 63 - ], - [ - -26, - 29 - ], - [ - 14, - 69 - ], - [ - -16, - 110 - ], - [ - 15, - 31 - ], - [ - -11, - 102 - ], - [ - -28, - 64 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - 8, - 58 - ], - [ - 23, - -8 - ], - [ - 13, - 35 - ], - [ - -16, - 71 - ], - [ - 8, - 17 - ] - ], - [ - [ - 7155, - 11837 - ], - [ - 36, - -3 - ], - [ - 53, - 83 - ], - [ - 28, - 13 - ], - [ - 1, - 40 - ], - [ - 13, - 101 - ], - [ - 40, - 56 - ], - [ - 44, - 2 - ], - [ - 5, - 25 - ], - [ - 55, - -10 - ], - [ - 55, - 61 - ], - [ - 27, - 26 - ], - [ - 34, - 58 - ], - [ - 24, - -7 - ], - [ - 19, - -32 - ], - [ - -14, - -40 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -45, - -20 - ], - [ - -17, - -60 - ], - [ - -27, - -35 - ], - [ - -21, - -44 - ], - [ - -8, - -86 - ], - [ - -19, - -70 - ], - [ - 36, - -8 - ], - [ - 8, - -55 - ], - [ - 16, - -26 - ], - [ - 5, - -49 - ], - [ - -8, - -44 - ], - [ - 3, - -25 - ], - [ - 17, - -10 - ], - [ - 16, - -42 - ], - [ - 90, - 12 - ], - [ - 40, - -16 - ], - [ - 49, - -103 - ], - [ - 29, - 13 - ], - [ - 50, - -7 - ], - [ - 40, - 14 - ], - [ - 24, - -21 - ], - [ - -12, - -64 - ], - [ - -16, - -40 - ], - [ - -5, - -86 - ], - [ - 14, - -80 - ], - [ - 20, - -36 - ], - [ - 2, - -27 - ], - [ - -35, - -59 - ], - [ - 25, - -27 - ], - [ - 18, - -42 - ], - [ - 22, - -119 - ] - ], - [ - [ - 6764, - 11784 - ], - [ - -38, - 27 - ], - [ - -14, - 25 - ], - [ - 8, - 21 - ], - [ - -2, - 26 - ], - [ - -20, - 29 - ], - [ - -27, - 23 - ], - [ - -24, - 16 - ], - [ - -5, - 35 - ], - [ - -18, - 21 - ], - [ - 4, - -35 - ], - [ - -13, - -28 - ], - [ - -16, - 33 - ], - [ - -23, - 12 - ], - [ - -9, - 24 - ], - [ - 0, - 37 - ], - [ - 9, - 37 - ], - [ - -19, - 17 - ], - [ - 16, - 23 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - 10, - 16 - ], - [ - 46, - -32 - ], - [ - 16, - 16 - ], - [ - 22, - -10 - ], - [ - 12, - -25 - ], - [ - 20, - -8 - ], - [ - 17, - 26 - ] - ], - [ - [ - 6716, - 12110 - ], - [ - 18, - -66 - ], - [ - 27, - -48 - ], - [ - 32, - -51 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - -27, - -11 - ], - [ - 1, - -48 - ], - [ - 14, - -18 - ], - [ - -10, - -14 - ], - [ - 3, - -22 - ], - [ - -6, - -24 - ], - [ - -4, - -24 - ] - ], - [ - [ - 6813, - 13579 - ], - [ - 60, - -8 - ], - [ - 55, - -2 - ], - [ - 65, - -41 - ], - [ - 28, - -44 - ], - [ - 65, - 14 - ], - [ - 25, - -28 - ], - [ - 59, - -75 - ], - [ - 43, - -54 - ], - [ - 23, - 2 - ], - [ - 42, - -24 - ], - [ - -5, - -34 - ], - [ - 51, - -5 - ], - [ - 53, - -49 - ], - [ - -9, - -28 - ], - [ - -46, - -16 - ], - [ - -47, - -6 - ], - [ - -48, - 10 - ], - [ - -100, - -12 - ], - [ - 47, - 67 - ], - [ - -28, - 31 - ], - [ - -45, - 8 - ], - [ - -24, - 35 - ], - [ - -17, - 68 - ], - [ - -39, - -4 - ], - [ - -65, - 32 - ], - [ - -21, - 25 - ], - [ - -91, - 19 - ], - [ - -24, - 23 - ], - [ - 26, - 30 - ], - [ - -69, - 6 - ], - [ - -50, - -62 - ], - [ - -29, - -2 - ], - [ - -10, - -29 - ], - [ - -34, - -13 - ], - [ - -30, - 11 - ], - [ - 37, - 37 - ], - [ - 15, - 43 - ], - [ - 31, - 27 - ], - [ - 36, - 23 - ], - [ - 53, - 12 - ], - [ - 17, - 13 - ] - ], - [ - [ - 14829, - 15013 - ], - [ - 5, - 1 - ], - [ - 10, - 28 - ], - [ - 50, - -1 - ], - [ - 64, - 36 - ], - [ - -47, - -51 - ], - [ - 5, - -23 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - -8, - 4 - ], - [ - -13, - -9 - ], - [ - -10, - 3 - ], - [ - -4, - -5 - ], - [ - -1, - 12 - ], - [ - -5, - 8 - ], - [ - -14, - 1 - ], - [ - -19, - -10 - ], - [ - -13, - 6 - ] - ], - [ - [ - 14916, - 15003 - ], - [ - 2, - -10 - ], - [ - -72, - -48 - ], - [ - -34, - 15 - ], - [ - -16, - 48 - ], - [ - 33, - 5 - ] - ], - [ - [ - 13495, - 16661 - ], - [ - -39, - 52 - ], - [ - -36, - 28 - ], - [ - -7, - 51 - ], - [ - -12, - 36 - ], - [ - 50, - 26 - ], - [ - 26, - 30 - ], - [ - 50, - 23 - ], - [ - 18, - 23 - ], - [ - 18, - -14 - ], - [ - 31, - 12 - ] - ], - [ - [ - 13594, - 16928 - ], - [ - 33, - -38 - ], - [ - 52, - -11 - ], - [ - -4, - -33 - ], - [ - 38, - -24 - ], - [ - 10, - 30 - ], - [ - 48, - -13 - ], - [ - 7, - -37 - ], - [ - 52, - -8 - ], - [ - 32, - -59 - ] - ], - [ - [ - 13862, - 16735 - ], - [ - -21, - 0 - ], - [ - -11, - -22 - ], - [ - -16, - -5 - ], - [ - -4, - -27 - ], - [ - -14, - -6 - ], - [ - -2, - -11 - ], - [ - -23, - -12 - ], - [ - -31, - 2 - ], - [ - -10, - -27 - ] - ], - [ - [ - 13068, - 16510 - ], - [ - 9, - 86 - ], - [ - 35, - 82 - ], - [ - -100, - 22 - ], - [ - -33, - 31 - ] - ], - [ - [ - 12979, - 16731 - ], - [ - 4, - 53 - ], - [ - -14, - 27 - ] - ], - [ - [ - 12977, - 16892 - ], - [ - -12, - 126 - ], - [ - 42, - 0 - ], - [ - 18, - 45 - ], - [ - 17, - 110 - ], - [ - -13, - 40 - ] - ], - [ - [ - 13029, - 17213 - ], - [ - 13, - 26 - ], - [ - 59, - 6 - ], - [ - 13, - -26 - ], - [ - 47, - 59 - ], - [ - -16, - 45 - ], - [ - -3, - 68 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - 53, - -16 - ], - [ - 44, - 18 - ] - ], - [ - [ - 13239, - 17393 - ], - [ - 1, - -46 - ], - [ - 71, - -28 - ], - [ - -1, - -42 - ], - [ - 71, - 22 - ], - [ - 39, - 33 - ], - [ - 79, - -47 - ], - [ - 33, - -39 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 16, - -61 - ], - [ - -19, - -32 - ], - [ - 25, - -42 - ], - [ - 17, - -65 - ], - [ - -5, - -41 - ], - [ - 28, - -77 - ] - ], - [ - [ - 15551, - 12321 - ], - [ - 16, - -37 - ], - [ - -2, - -50 - ], - [ - -40, - -29 - ], - [ - 30, - -33 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - -26, - -64 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - 21 - ], - [ - -17, - -8 - ], - [ - -39, - 2 - ], - [ - -1, - 36 - ], - [ - -5, - 34 - ], - [ - 23, - 56 - ], - [ - 25, - 53 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - 30, - -11 - ], - [ - 21, - 30 - ] - ], - [ - [ - 13142, - 17391 - ], - [ - -28, - 67 - ], - [ - -3, - 122 - ], - [ - 12, - 33 - ], - [ - 20, - 36 - ], - [ - 61, - 7 - ], - [ - 25, - 33 - ], - [ - 56, - 34 - ], - [ - -2, - -62 - ], - [ - -21, - -39 - ], - [ - 8, - -33 - ], - [ - 38, - -19 - ], - [ - -17, - -45 - ], - [ - -21, - 13 - ], - [ - -50, - -86 - ], - [ - 19, - -59 - ] - ], - [ - [ - 13432, - 17469 - ], - [ - -42, - -98 - ], - [ - -73, - 68 - ], - [ - -9, - 50 - ], - [ - 102, - 40 - ], - [ - 22, - -60 - ] - ], - [ - [ - 7549, - 13162 - ], - [ - 8, - 21 - ], - [ - 55, - -1 - ], - [ - 41, - -31 - ], - [ - 18, - 3 - ], - [ - 13, - -42 - ], - [ - 38, - 2 - ], - [ - -2, - -36 - ], - [ - 31, - -4 - ], - [ - 34, - -44 - ], - [ - -26, - -49 - ], - [ - -33, - 26 - ], - [ - -32, - -5 - ], - [ - -23, - 6 - ], - [ - -12, - -22 - ], - [ - -27, - -7 - ], - [ - -11, - 29 - ], - [ - -23, - -17 - ], - [ - -28, - -83 - ], - [ - -18, - 20 - ], - [ - -3, - 34 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - 1, - 33 - ], - [ - -18, - 36 - ], - [ - 17, - 20 - ], - [ - 6, - 46 - ], - [ - -6, - 65 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - -77, - -12 - ], - [ - -1, - 77 - ], - [ - -32, - 19 - ], - [ - -44, - 35 - ], - [ - -16, - 56 - ], - [ - -236, - 262 - ], - [ - -235, - 261 - ] - ], - [ - [ - 12204, - 13793 - ], - [ - -262, - 291 - ] - ], - [ - [ - 11942, - 14084 - ], - [ - 1, - 23 - ], - [ - 0, - 8 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - 0, - 142 - ], - [ - 112, - 89 - ], - [ - 70, - 18 - ], - [ - 57, - 32 - ], - [ - 27, - 60 - ], - [ - 81, - 48 - ], - [ - 3, - 89 - ], - [ - 41, - 10 - ], - [ - 31, - 45 - ], - [ - 91, - 20 - ], - [ - 13, - 46 - ], - [ - -18, - 26 - ], - [ - -24, - 127 - ], - [ - -4, - 72 - ], - [ - -27, - 77 - ] - ], - [ - [ - 12396, - 15016 - ], - [ - 67, - 66 - ], - [ - 76, - 21 - ], - [ - 44, - 49 - ], - [ - 67, - 37 - ], - [ - 118, - 21 - ], - [ - 115, - 10 - ], - [ - 35, - -18 - ], - [ - 66, - 47 - ], - [ - 74, - 1 - ], - [ - 29, - -28 - ], - [ - 48, - 8 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - -15, - -62 - ], - [ - 11, - -114 - ], - [ - -16, - -99 - ], - [ - -43, - -67 - ], - [ - 6, - -91 - ], - [ - 57, - -71 - ], - [ - 1, - -29 - ], - [ - 43, - -48 - ], - [ - 29, - -216 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 23, - -106 - ], - [ - 4, - -56 - ], - [ - -12, - -97 - ], - [ - 5, - -55 - ], - [ - -9, - -66 - ], - [ - 6, - -75 - ], - [ - -28, - -50 - ], - [ - 41, - -88 - ], - [ - 3, - -51 - ], - [ - 25, - -67 - ], - [ - 32, - 22 - ], - [ - 55, - -56 - ], - [ - 31, - -75 - ] - ], - [ - [ - 13384, - 13613 - ], - [ - -239, - -229 - ], - [ - -202, - -235 - ], - [ - -98, - -54 - ] - ], - [ - [ - 7293, - 10779 - ], - [ - 10, - -91 - ], - [ - -22, - -78 - ], - [ - -76, - -126 - ], - [ - -83, - -47 - ], - [ - -43, - -104 - ], - [ - -13, - -81 - ], - [ - -40, - -50 - ], - [ - -29, - 61 - ], - [ - -28, - 13 - ], - [ - -29, - -10 - ], - [ - -2, - 44 - ], - [ - 20, - 29 - ], - [ - -8, - 50 - ] - ], - [ - [ - 6950, - 10389 - ], - [ - 37, - 89 - ], - [ - -15, - 53 - ], - [ - -27, - -56 - ], - [ - -42, - 53 - ], - [ - 15, - 33 - ], - [ - -12, - 109 - ], - [ - 24, - 18 - ], - [ - 13, - 75 - ], - [ - 26, - 77 - ], - [ - -4, - 49 - ], - [ - 38, - 26 - ], - [ - 48, - 48 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - -276, - 0 - ], - [ - -271, - 0 - ], - [ - -280, - 0 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - 441 - ], - [ - 0, - 427 - ], - [ - -21, - 97 - ], - [ - 18, - 74 - ], - [ - -11, - 51 - ], - [ - 26, - 58 - ] - ], - [ - [ - 14302, - 14585 - ], - [ - 92, - 1 - ], - [ - 68, - -31 - ], - [ - 69, - -36 - ], - [ - 32, - -18 - ], - [ - 54, - 38 - ], - [ - 28, - 34 - ], - [ - 62, - 10 - ], - [ - 49, - -15 - ], - [ - 19, - -60 - ], - [ - 17, - 39 - ], - [ - 55, - -28 - ], - [ - 55, - -7 - ], - [ - 34, - 31 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 39, - -175 - ], - [ - 7, - -32 - ] - ], - [ - [ - 14982, - 14336 - ], - [ - -20, - -48 - ], - [ - -15, - -90 - ], - [ - -19, - -63 - ], - [ - -16, - -21 - ], - [ - -23, - 39 - ], - [ - -32, - 53 - ], - [ - -49, - 172 - ], - [ - -7, - -10 - ], - [ - 28, - -127 - ], - [ - 43, - -121 - ], - [ - 53, - -187 - ], - [ - 26, - -65 - ], - [ - 22, - -68 - ], - [ - 63, - -132 - ], - [ - -14, - -21 - ], - [ - 2, - -78 - ], - [ - 81, - -108 - ], - [ - 12, - -24 - ] - ], - [ - [ - 15500, - 12302 - ], - [ - -24, - 39 - ], - [ - -29, - 70 - ], - [ - -31, - 39 - ], - [ - -18, - 41 - ], - [ - -60, - 48 - ], - [ - -48, - 2 - ], - [ - -17, - 25 - ], - [ - -41, - -29 - ], - [ - -42, - 55 - ], - [ - -22, - -90 - ], - [ - -81, - 25 - ] - ], - [ - [ - 15087, - 12527 - ], - [ - -7, - 48 - ], - [ - 30, - 177 - ], - [ - 6, - 79 - ], - [ - 22, - 37 - ], - [ - 52, - 20 - ], - [ - 35, - 68 - ] - ], - [ - [ - 15225, - 12956 - ], - [ - 40, - -138 - ], - [ - 20, - -111 - ], - [ - 38, - -58 - ], - [ - 95, - -113 - ], - [ - 39, - -69 - ], - [ - 38, - -69 - ], - [ - 21, - -41 - ], - [ - 35, - -36 - ] - ], - [ - [ - 11918, - 15822 - ], - [ - 3, - 85 - ], - [ - -28, - 52 - ], - [ - 98, - 87 - ], - [ - 86, - -22 - ], - [ - 93, - 1 - ], - [ - 74, - -21 - ], - [ - 58, - 7 - ], - [ - 113, - -4 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 28, - -47 - ], - [ - 128, - -55 - ], - [ - 25, - 26 - ], - [ - 79, - -54 - ], - [ - 81, - 16 - ] - ], - [ - [ - 12756, - 15893 - ], - [ - 3, - -70 - ], - [ - -66, - -80 - ], - [ - -89, - -25 - ], - [ - -6, - -41 - ], - [ - -43, - -66 - ], - [ - -27, - -98 - ], - [ - 27, - -68 - ], - [ - -40, - -54 - ], - [ - -15, - -78 - ], - [ - -53, - -24 - ], - [ - -49, - -92 - ], - [ - -89, - -2 - ], - [ - -66, - 2 - ], - [ - -44, - -42 - ], - [ - -26, - -45 - ], - [ - -34, - 10 - ], - [ - -26, - 40 - ], - [ - -20, - 69 - ], - [ - -65, - 19 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -6, - 39 - ], - [ - 26, - 45 - ], - [ - 10, - 33 - ], - [ - -25, - 36 - ], - [ - 20, - 79 - ], - [ - -28, - 72 - ], - [ - 30, - 9 - ], - [ - 3, - 57 - ], - [ - 11, - 18 - ], - [ - 1, - 93 - ], - [ - 32, - 33 - ], - [ - -19, - 60 - ], - [ - -41, - 4 - ], - [ - -12, - -15 - ], - [ - -41, - 0 - ], - [ - -18, - 59 - ], - [ - -28, - -18 - ], - [ - -25, - -30 - ] - ], - [ - [ - 14242, - 17731 - ], - [ - 8, - 70 - ], - [ - -25, - -15 - ], - [ - -44, - 43 - ], - [ - -7, - 69 - ], - [ - 89, - 33 - ], - [ - 87, - 18 - ], - [ - 76, - -20 - ], - [ - 72, - 3 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 11, - -21 - ], - [ - -50, - -69 - ], - [ - 21, - -112 - ], - [ - -30, - -38 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - -58, - 1 - ], - [ - -60, - 44 - ], - [ - -30, - 15 - ], - [ - -60, - -21 - ] - ], - [ - [ - 15529, - 12108 - ], - [ - -15, - -42 - ], - [ - 26, - -66 - ], - [ - 26, - -58 - ], - [ - 26, - -43 - ], - [ - 228, - -142 - ], - [ - 59, - 0 - ] - ], - [ - [ - 15879, - 11757 - ], - [ - -197, - -360 - ], - [ - -91, - -5 - ], - [ - -62, - -85 - ], - [ - -45, - -2 - ], - [ - -19, - -38 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -47, - 0 - ], - [ - -29, - 41 - ], - [ - -63, - -50 - ], - [ - -21, - -50 - ], - [ - -46, - 9 - ], - [ - -16, - 14 - ], - [ - -16, - -3 - ], - [ - -22, - 1 - ], - [ - -88, - 102 - ], - [ - -49, - 0 - ], - [ - -24, - 39 - ], - [ - 0, - 68 - ], - [ - -36, - 20 - ] - ], - [ - [ - 15008, - 11458 - ], - [ - -41, - 130 - ], - [ - -32, - 28 - ], - [ - -12, - 48 - ], - [ - -36, - 59 - ], - [ - -42, - 8 - ], - [ - 23, - 68 - ], - [ - 37, - 3 - ], - [ - 11, - 37 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 108 - ], - [ - 21, - 125 - ], - [ - 33, - 34 - ], - [ - 7, - 49 - ], - [ - 29, - 92 - ], - [ - 42, - 59 - ], - [ - 29, - 118 - ], - [ - 11, - 103 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -24, - 47 - ], - [ - -2, - 184 - ], - [ - -108, - 82 - ], - [ - -93, - 59 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - 41, - 31 - ], - [ - 78, - -63 - ], - [ - 91, - 6 - ], - [ - 75, - -29 - ], - [ - 66, - 53 - ], - [ - 34, - 88 - ], - [ - 109, - 41 - ], - [ - 89, - -48 - ], - [ - -29, - -84 - ] - ], - [ - [ - 14541, - 19083 - ], - [ - -11, - -84 - ], - [ - 107, - -80 - ], - [ - -64, - -91 - ], - [ - 81, - -136 - ], - [ - -47, - -103 - ], - [ - 63, - -89 - ], - [ - -29, - -78 - ], - [ - 103, - -83 - ], - [ - -26, - -61 - ], - [ - -65, - -69 - ], - [ - -149, - -153 - ] - ], - [ - [ - 14504, - 18056 - ], - [ - -126, - -10 - ], - [ - -123, - -44 - ], - [ - -113, - -25 - ], - [ - -41, - 65 - ], - [ - -67, - 40 - ], - [ - 15, - 118 - ], - [ - -33, - 108 - ], - [ - 33, - 70 - ], - [ - 63, - 75 - ], - [ - 159, - 130 - ], - [ - 47, - 26 - ], - [ - -7, - 50 - ], - [ - -97, - 57 - ] - ], - [ - [ - 24982, - 8717 - ], - [ - 24, - -35 - ], - [ - -12, - -62 - ], - [ - -43, - -17 - ], - [ - -39, - 15 - ], - [ - -6, - 53 - ], - [ - 27, - 41 - ], - [ - 31, - -15 - ], - [ - 18, - 20 - ] - ], - [ - [ - 25051, - 8782 - ], - [ - -45, - -26 - ], - [ - -9, - 45 - ], - [ - 35, - 25 - ], - [ - 22, - 6 - ], - [ - 41, - 38 - ], - [ - 0, - -59 - ], - [ - -44, - -29 - ] - ], - [ - [ - 6, - 8817 - ], - [ - -6, - -6 - ], - [ - 0, - 59 - ], - [ - 14, - 5 - ], - [ - -8, - -58 - ] - ], - [ - [ - 8281, - 4577 - ], - [ - 84, - 72 - ], - [ - 59, - -30 - ], - [ - 42, - 48 - ], - [ - 56, - -54 - ], - [ - -21, - -42 - ], - [ - -94, - -36 - ], - [ - -32, - 42 - ], - [ - -59, - -54 - ], - [ - -35, - 54 - ] - ], - [ - [ - 12943, - 16739 - ], - [ - 16, - -10 - ], - [ - 20, - 2 - ] - ], - [ - [ - 13025, - 16315 - ], - [ - -3, - -34 - ], - [ - 20, - -45 - ], - [ - -24, - -37 - ], - [ - 18, - -93 - ], - [ - 38, - -15 - ], - [ - -8, - -52 - ] - ], - [ - [ - 13066, - 16039 - ], - [ - -63, - -68 - ], - [ - -138, - 33 - ], - [ - -101, - -39 - ], - [ - -8, - -72 - ] - ], - [ - [ - 12415, - 16007 - ], - [ - 36, - 72 - ], - [ - 13, - 239 - ], - [ - -72, - 125 - ], - [ - -51, - 61 - ], - [ - -107, - 46 - ], - [ - -7, - 88 - ], - [ - 91, - 26 - ], - [ - 117, - -31 - ], - [ - -22, - 136 - ], - [ - 66, - -52 - ], - [ - 162, - 94 - ], - [ - 21, - 98 - ], - [ - 61, - 24 - ] - ], - [ - [ - 8747, - 11075 - ], - [ - 17, - 51 - ], - [ - 6, - 54 - ], - [ - 12, - 52 - ], - [ - -27, - 71 - ], - [ - -5, - 82 - ], - [ - 36, - 103 - ] - ], - [ - [ - 8786, - 11488 - ], - [ - 24, - -13 - ], - [ - 51, - -29 - ], - [ - 74, - -101 - ], - [ - 12, - -49 - ] - ], - [ - [ - 13214, - 15854 - ], - [ - -23, - -92 - ], - [ - -32, - 24 - ], - [ - -16, - 81 - ], - [ - 14, - 44 - ], - [ - 45, - 46 - ], - [ - 12, - -103 - ] - ], - [ - [ - 13321, - 10320 - ], - [ - -72, - 121 - ], - [ - -46, - 99 - ], - [ - -42, - 124 - ], - [ - 2, - 40 - ], - [ - 15, - 38 - ], - [ - 17, - 87 - ], - [ - 14, - 89 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - 24, - 7 - ], - [ - 101, - -1 - ], - [ - 0, - 144 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - -52, - 24 - ], - [ - -43, - -1 - ], - [ - 14, - 64 - ], - [ - -14, - 64 - ] - ], - [ - [ - 12020, - 17411 - ], - [ - 58, - 5 - ], - [ - 75, - -74 - ], - [ - -38, - -82 - ] - ], - [ - [ - 12338, - 17832 - ], - [ - -74, - -130 - ], - [ - 71, - 16 - ], - [ - 76, - 0 - ], - [ - -18, - -98 - ], - [ - -63, - -108 - ], - [ - 72, - -7 - ], - [ - 6, - -13 - ], - [ - 62, - -142 - ], - [ - 47, - -19 - ], - [ - 43, - -136 - ], - [ - 20, - -48 - ], - [ - 85, - -23 - ], - [ - -9, - -76 - ], - [ - -35, - -36 - ], - [ - 28, - -62 - ], - [ - -63, - -63 - ], - [ - -93, - 2 - ], - [ - -119, - -33 - ], - [ - -33, - 23 - ], - [ - -46, - -56 - ], - [ - -64, - 14 - ], - [ - -49, - -46 - ], - [ - -37, - 24 - ], - [ - 102, - 126 - ], - [ - 62, - 26 - ], - [ - 0, - 0 - ], - [ - -109, - 20 - ], - [ - -20, - 48 - ], - [ - 73, - 37 - ], - [ - -38, - 64 - ], - [ - 13, - 79 - ], - [ - 104, - -11 - ], - [ - 10, - 70 - ], - [ - -46, - 74 - ], - [ - -2, - 1 - ], - [ - -84, - 21 - ], - [ - -17, - 33 - ], - [ - 26, - 53 - ], - [ - -23, - 34 - ], - [ - -38, - -57 - ], - [ - -4, - 115 - ], - [ - -35, - 62 - ], - [ - 25, - 124 - ], - [ - 54, - 97 - ], - [ - 56, - -10 - ], - [ - 84, - 11 - ] - ], - [ - [ - 15586, - 15727 - ], - [ - -68, - 59 - ], - [ - -74, - -6 - ] - ], - [ - [ - 15444, - 15780 - ], - [ - 11, - 51 - ], - [ - -18, - 82 - ], - [ - -40, - 44 - ], - [ - -39, - 14 - ], - [ - -25, - 37 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - 8, - 14 - ], - [ - 59, - -20 - ], - [ - 103, - -20 - ], - [ - 95, - -57 - ], - [ - 12, - -23 - ], - [ - 42, - 19 - ], - [ - 65, - -25 - ], - [ - 21, - -49 - ], - [ - 44, - -28 - ] - ], - [ - [ - 12549, - 12119 - ], - [ - -5, - -37 - ], - [ - 29, - -62 - ], - [ - 0, - -87 - ], - [ - 7, - -95 - ], - [ - 17, - -44 - ], - [ - -15, - -108 - ], - [ - 5, - -59 - ], - [ - 19, - -76 - ], - [ - 15, - -43 - ] - ], - [ - [ - 12621, - 11508 - ], - [ - -109, - -70 - ], - [ - -39, - -41 - ], - [ - -62, - -35 - ], - [ - -63, - 34 - ] - ], - [ - [ - 11959, - 11719 - ], - [ - -20, - 3 - ], - [ - -14, - -48 - ], - [ - -19, - 1 - ], - [ - -14, - 25 - ], - [ - 5, - 48 - ], - [ - -30, - 74 - ], - [ - -18, - -14 - ], - [ - -15, - -2 - ] - ], - [ - [ - 11834, - 11806 - ], - [ - -19, - -7 - ], - [ - 1, - 44 - ], - [ - -11, - 31 - ], - [ - 2, - 35 - ], - [ - -15, - 50 - ], - [ - -19, - 43 - ], - [ - -56, - 1 - ], - [ - -16, - -23 - ], - [ - -20, - -3 - ], - [ - -12, - -26 - ], - [ - -8, - -33 - ], - [ - -37, - -53 - ] - ], - [ - [ - 11624, - 11865 - ], - [ - -30, - 71 - ], - [ - -28, - 47 - ], - [ - -17, - 16 - ], - [ - -18, - 24 - ], - [ - -8, - 53 - ], - [ - -10, - 26 - ], - [ - -20, - 20 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - 31, - 58 - ], - [ - 21, - -2 - ], - [ - 18, - 20 - ], - [ - 15, - 0 - ], - [ - 11, - 16 - ], - [ - -5, - 40 - ], - [ - 7, - 12 - ], - [ - 1, - 41 - ] - ], - [ - [ - 11592, - 12307 - ], - [ - 34, - -1 - ], - [ - 50, - -29 - ], - [ - 16, - 2 - ], - [ - 5, - 14 - ], - [ - 38, - -10 - ], - [ - 10, - 7 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 4, - -44 - ], - [ - 11, - 0 - ], - [ - 18, - 16 - ], - [ - 12, - -4 - ], - [ - 19, - -30 - ], - [ - 30, - -10 - ], - [ - 19, - 26 - ], - [ - 23, - 16 - ], - [ - 16, - 17 - ], - [ - 14, - -3 - ], - [ - 16, - -27 - ], - [ - 8, - -33 - ], - [ - 29, - -50 - ], - [ - -15, - -31 - ], - [ - -2, - -39 - ], - [ - 14, - 12 - ], - [ - 9, - -14 - ], - [ - -4, - -36 - ], - [ - 22, - -34 - ] - ], - [ - [ - 11374, - 12375 - ], - [ - 8, - 53 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - 76, - 4 - ], - [ - 16, - 28 - ], - [ - 22, - 2 - ], - [ - 28, - -30 - ], - [ - 21, - 0 - ], - [ - 23, - 20 - ], - [ - 14, - -35 - ], - [ - -30, - -27 - ], - [ - -30, - 3 - ], - [ - -30, - 25 - ], - [ - -26, - -28 - ], - [ - -12, - -1 - ], - [ - -17, - -17 - ], - [ - -63, - 3 - ] - ], - [ - [ - 11493, - 12122 - ], - [ - -37, - 50 - ], - [ - -30, - 8 - ], - [ - -16, - 34 - ], - [ - 1, - 18 - ], - [ - -22, - 25 - ], - [ - -4, - 26 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - 37, - 20 - ], - [ - 23, - -4 - ], - [ - 19, - 13 - ], - [ - 128, - -5 - ] - ], - [ - [ - 13209, - 10918 - ], - [ - -13, - 18 - ], - [ - 24, - 135 - ] - ], - [ - [ - 14013, - 15697 - ], - [ - 45, - 11 - ], - [ - 27, - 26 - ], - [ - 38, - -2 - ], - [ - 11, - 20 - ], - [ - 13, - 4 - ] - ], - [ - [ - 14368, - 15815 - ], - [ - 34, - -32 - ], - [ - -22, - -75 - ], - [ - -16, - -13 - ] - ], - [ - [ - 14364, - 15695 - ], - [ - -43, - 3 - ], - [ - -36, - 12 - ], - [ - -84, - -32 - ], - [ - 48, - -67 - ], - [ - -35, - -20 - ], - [ - -39, - 0 - ], - [ - -37, - 62 - ], - [ - -13, - -26 - ], - [ - 15, - -72 - ], - [ - 35, - -56 - ], - [ - -26, - -27 - ], - [ - 39, - -55 - ], - [ - 34, - -35 - ], - [ - 1, - -67 - ], - [ - -64, - 31 - ], - [ - 20, - -61 - ], - [ - -44, - -12 - ], - [ - 27, - -106 - ], - [ - -47, - -2 - ], - [ - -57, - 52 - ], - [ - -26, - 96 - ], - [ - -12, - 80 - ], - [ - -27, - 55 - ], - [ - -36, - 69 - ], - [ - -5, - 34 - ] - ], - [ - [ - 14200, - 15081 - ], - [ - 38, - -41 - ], - [ - 54, - 7 - ], - [ - 52, - -8 - ], - [ - -2, - -21 - ], - [ - 38, - 14 - ], - [ - -9, - -35 - ], - [ - -100, - -10 - ], - [ - 1, - 19 - ], - [ - -85, - 24 - ], - [ - 13, - 51 - ] - ], - [ - [ - 9288, - 20710 - ], - [ - 234, - 72 - ], - [ - 244, - -6 - ], - [ - 89, - 44 - ], - [ - 247, - 12 - ], - [ - 556, - -15 - ], - [ - 436, - -95 - ], - [ - -128, - -46 - ], - [ - -267, - -6 - ], - [ - -375, - -11 - ], - [ - 35, - -22 - ], - [ - 247, - 13 - ], - [ - 210, - -41 - ], - [ - 135, - 37 - ], - [ - 58, - -43 - ], - [ - -77, - -70 - ], - [ - 178, - 45 - ], - [ - 338, - 46 - ], - [ - 209, - -23 - ], - [ - 39, - -51 - ], - [ - -284, - -86 - ], - [ - -39, - -27 - ], - [ - -223, - -21 - ], - [ - 162, - -6 - ], - [ - -82, - -87 - ], - [ - -56, - -78 - ], - [ - 2, - -134 - ], - [ - 84, - -78 - ], - [ - -109, - -5 - ], - [ - -115, - -38 - ], - [ - 129, - -63 - ], - [ - 16, - -102 - ], - [ - -74, - -11 - ], - [ - 90, - -104 - ], - [ - -155, - -8 - ], - [ - 81, - -49 - ], - [ - -23, - -42 - ], - [ - -98, - -19 - ], - [ - -97, - 0 - ], - [ - 87, - -82 - ], - [ - 1, - -53 - ], - [ - -138, - 50 - ], - [ - -36, - -32 - ], - [ - 94, - -30 - ], - [ - 92, - -74 - ], - [ - 26, - -96 - ], - [ - -124, - -23 - ], - [ - -54, - 46 - ], - [ - -86, - 69 - ], - [ - 24, - -82 - ], - [ - -81, - -63 - ], - [ - 184, - -5 - ], - [ - 96, - -6 - ], - [ - -187, - -105 - ], - [ - -190, - -94 - ], - [ - -204, - -42 - ], - [ - -77, - 0 - ], - [ - -72, - -47 - ], - [ - -97, - -126 - ], - [ - -150, - -84 - ], - [ - -48, - -5 - ], - [ - -93, - -30 - ], - [ - -100, - -28 - ], - [ - -59, - -74 - ], - [ - -1, - -84 - ], - [ - -36, - -79 - ], - [ - -113, - -96 - ], - [ - 28, - -94 - ], - [ - -32, - -99 - ], - [ - -35, - -117 - ], - [ - -99, - -7 - ], - [ - -102, - 98 - ], - [ - -140, - 0 - ], - [ - -67, - 66 - ], - [ - -47, - 117 - ], - [ - -121, - 149 - ], - [ - -35, - 79 - ], - [ - -10, - 107 - ], - [ - -96, - 111 - ], - [ - 25, - 88 - ], - [ - -47, - 43 - ], - [ - 69, - 140 - ], - [ - 105, - 45 - ], - [ - 28, - 50 - ], - [ - 14, - 94 - ], - [ - -79, - -43 - ], - [ - -38, - -18 - ], - [ - -63, - -17 - ], - [ - -85, - 39 - ], - [ - -5, - 82 - ], - [ - 27, - 64 - ], - [ - 65, - 1 - ], - [ - 142, - -32 - ], - [ - -120, - 77 - ], - [ - -62, - 41 - ], - [ - -69, - -17 - ], - [ - -59, - 29 - ], - [ - 78, - 112 - ], - [ - -42, - 45 - ], - [ - -56, - 83 - ], - [ - -83, - 127 - ], - [ - -89, - 47 - ], - [ - 1, - 50 - ], - [ - -187, - 70 - ], - [ - -148, - 9 - ], - [ - -187, - -5 - ], - [ - -170, - -9 - ], - [ - -81, - 38 - ], - [ - -121, - 76 - ], - [ - 183, - 38 - ], - [ - 140, - 6 - ], - [ - -298, - 31 - ], - [ - -157, - 49 - ], - [ - 10, - 47 - ], - [ - 264, - 57 - ], - [ - 255, - 58 - ], - [ - 27, - 44 - ], - [ - -188, - 43 - ], - [ - 60, - 48 - ], - [ - 242, - 83 - ], - [ - 101, - 13 - ], - [ - -29, - 54 - ], - [ - 165, - 32 - ], - [ - 215, - 19 - ], - [ - 214, - 1 - ], - [ - 76, - -38 - ], - [ - 185, - 66 - ], - [ - 166, - -45 - ], - [ - 98, - -9 - ], - [ - 145, - -39 - ], - [ - -166, - 65 - ], - [ - 10, - 51 - ] - ], - [ - [ - 6348, - 12703 - ], - [ - 23, - -22 - ], - [ - 6, - 18 - ], - [ - 20, - -15 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - -31, - -46 - ], - [ - -33, - -33 - ], - [ - -5, - -23 - ], - [ - 5, - -24 - ], - [ - -14, - -30 - ] - ], - [ - [ - 6319, - 12528 - ], - [ - -16, - -8 - ], - [ - 3, - -14 - ], - [ - -13, - -13 - ], - [ - -24, - -30 - ], - [ - -2, - -18 - ] - ], - [ - [ - 6267, - 12445 - ], - [ - -36, - 21 - ], - [ - -43, - 2 - ], - [ - -32, - 24 - ], - [ - -38, - 49 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - 2, - 35 - ], - [ - 8, - 28 - ], - [ - -10, - 23 - ], - [ - 34, - 98 - ], - [ - 89, - 0 - ], - [ - 2, - 41 - ], - [ - -11, - 7 - ], - [ - -8, - 26 - ], - [ - -26, - 28 - ], - [ - -26, - 40 - ], - [ - 32, - 0 - ], - [ - 0, - 68 - ], - [ - 65, - 0 - ], - [ - 64, - -1 - ] - ], - [ - [ - 8314, - 11421 - ], - [ - -47, - 91 - ], - [ - 19, - 33 - ], - [ - -2, - 56 - ], - [ - 43, - 19 - ], - [ - 17, - 22 - ], - [ - -23, - 45 - ], - [ - 6, - 44 - ], - [ - 55, - 70 - ] - ], - [ - [ - 8382, - 11801 - ], - [ - 46, - -44 - ], - [ - 43, - -78 - ], - [ - 2, - -62 - ], - [ - 26, - -3 - ], - [ - 37, - -58 - ], - [ - 28, - -42 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - -11, - -108 - ], - [ - -43, - -31 - ], - [ - 4, - -29 - ], - [ - -13, - -62 - ], - [ - 31, - -87 - ], - [ - 23, - 0 - ], - [ - 9, - -68 - ], - [ - 42, - -104 - ] - ], - [ - [ - 6397, - 12684 - ], - [ - 8, - -5 - ], - [ - 15, - 21 - ], - [ - 20, - 2 - ], - [ - 6, - -10 - ], - [ - 11, - 6 - ], - [ - 33, - -10 - ], - [ - 32, - 3 - ], - [ - 22, - 13 - ], - [ - 8, - 13 - ], - [ - 23, - -6 - ], - [ - 16, - -8 - ], - [ - 19, - 3 - ], - [ - 13, - 10 - ], - [ - 32, - -16 - ], - [ - 11, - -3 - ], - [ - 22, - -23 - ], - [ - 20, - -26 - ], - [ - 25, - -19 - ], - [ - 18, - -33 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -23, - 3 - ], - [ - -10, - -17 - ], - [ - -24, - -15 - ], - [ - -18, - 0 - ], - [ - -15, - -16 - ], - [ - -14, - 6 - ], - [ - -12, - 18 - ], - [ - -7, - -3 - ], - [ - -9, - -29 - ], - [ - -7, - 1 - ], - [ - -1, - -25 - ], - [ - -25, - -33 - ], - [ - -12, - -14 - ], - [ - -8, - -15 - ], - [ - -20, - 24 - ], - [ - -15, - -32 - ], - [ - -15, - 1 - ], - [ - -16, - -3 - ], - [ - 1, - -59 - ], - [ - -10, - -1 - ], - [ - -9, - -27 - ], - [ - -21, - -5 - ] - ], - [ - [ - 6461, - 12355 - ], - [ - -12, - 37 - ], - [ - -21, - 11 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - 4, - 48 - ], - [ - -9, - 13 - ], - [ - -14, - 9 - ], - [ - -31, - -15 - ], - [ - -3, - 16 - ], - [ - -21, - 20 - ], - [ - -15, - 24 - ], - [ - -20, - 10 - ] - ], - [ - [ - 13841, - 15914 - ], - [ - -7, - -21 - ] - ], - [ - [ - 13834, - 15893 - ], - [ - -66, - 45 - ], - [ - -40, - 43 - ], - [ - -64, - 36 - ], - [ - -59, - 88 - ], - [ - 14, - 9 - ], - [ - -31, - 50 - ], - [ - -2, - 41 - ], - [ - -45, - 19 - ], - [ - -21, - -52 - ], - [ - -20, - 40 - ], - [ - 1, - 42 - ], - [ - 3, - 2 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 48, - -4 - ], - [ - 13, - 20 - ], - [ - 24, - -20 - ], - [ - 27, - -2 - ], - [ - 0, - 34 - ], - [ - 24, - 12 - ], - [ - 7, - 48 - ], - [ - 55, - 32 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - 22, - -15 - ], - [ - 52, - -51 - ], - [ - 58, - -23 - ], - [ - 26, - 18 - ] - ], - [ - [ - 13860, - 16305 - ], - [ - 17, - -47 - ], - [ - 22, - -34 - ], - [ - -27, - -45 - ] - ], - [ - [ - 7549, - 12962 - ], - [ - -46, - 20 - ], - [ - -33, - -8 - ], - [ - -43, - 9 - ], - [ - -33, - -23 - ], - [ - -37, - 38 - ], - [ - 6, - 38 - ], - [ - 64, - -16 - ], - [ - 53, - -10 - ], - [ - 25, - 27 - ], - [ - -32, - 52 - ], - [ - 1, - 46 - ], - [ - -44, - 18 - ], - [ - 16, - 33 - ], - [ - 42, - -5 - ], - [ - 61, - -19 - ] - ], - [ - [ - 13731, - 16571 - ], - [ - 36, - -31 - ], - [ - 25, - -13 - ], - [ - 59, - 14 - ], - [ - 5, - 25 - ], - [ - 28, - 3 - ], - [ - 34, - 19 - ], - [ - 8, - -8 - ], - [ - 32, - 15 - ], - [ - 17, - 28 - ], - [ - 23, - 8 - ], - [ - 74, - -37 - ], - [ - 15, - 12 - ] - ], - [ - [ - 14087, - 16606 - ], - [ - 39, - -32 - ], - [ - 5, - -32 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - -43, - -26 - ], - [ - -33, - -81 - ], - [ - -42, - -81 - ], - [ - -56, - -23 - ] - ], - [ - [ - 13957, - 16331 - ], - [ - -43, - 5 - ], - [ - -54, - -31 - ] - ], - [ - [ - 13702, - 16376 - ], - [ - -13, - 41 - ], - [ - -12, - 1 - ] - ], - [ - [ - 20962, - 9569 - ], - [ - -29, - -3 - ], - [ - -92, - 85 - ], - [ - 65, - 23 - ], - [ - 36, - -36 - ], - [ - 25, - -37 - ], - [ - -5, - -32 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 7, - -23 - ], - [ - 1, - -37 - ] - ], - [ - [ - 21267, - 9670 - ], - [ - -45, - -89 - ], - [ - -60, - -27 - ], - [ - -8, - 15 - ], - [ - 6, - 40 - ], - [ - 30, - 74 - ], - [ - 69, - 47 - ] - ], - [ - [ - 20766, - 9826 - ], - [ - 25, - -32 - ], - [ - 43, - 10 - ], - [ - 18, - -51 - ], - [ - -81, - -24 - ], - [ - -48, - -16 - ], - [ - -38, - 1 - ], - [ - 24, - 69 - ], - [ - 38, - 1 - ], - [ - 19, - 42 - ] - ], - [ - [ - 21115, - 9826 - ], - [ - -10, - -67 - ], - [ - -105, - -34 - ], - [ - -93, - 15 - ], - [ - 0, - 44 - ], - [ - 55, - 25 - ], - [ - 44, - -36 - ], - [ - 46, - 9 - ], - [ - 63, - 44 - ] - ], - [ - [ - 20119, - 9984 - ], - [ - 134, - -12 - ], - [ - 15, - 50 - ], - [ - 130, - -58 - ], - [ - 25, - -78 - ], - [ - 105, - -22 - ], - [ - 85, - -71 - ], - [ - -79, - -46 - ], - [ - -77, - 49 - ], - [ - -63, - -4 - ], - [ - -72, - 9 - ], - [ - -66, - 22 - ], - [ - -80, - 46 - ], - [ - -52, - 11 - ], - [ - -29, - -15 - ], - [ - -127, - 50 - ], - [ - -12, - 51 - ], - [ - -64, - 9 - ], - [ - 48, - 115 - ], - [ - 85, - -7 - ], - [ - 56, - -47 - ], - [ - 29, - -9 - ], - [ - 9, - -43 - ] - ], - [ - [ - 21939, - 10052 - ], - [ - -36, - -82 - ], - [ - -7, - 90 - ], - [ - 13, - 43 - ], - [ - 14, - 41 - ], - [ - 16, - -35 - ], - [ - 0, - -57 - ] - ], - [ - [ - 21418, - 10382 - ], - [ - -26, - -40 - ], - [ - -48, - 22 - ], - [ - -14, - 52 - ], - [ - 71, - 6 - ], - [ - 17, - -40 - ] - ], - [ - [ - 21642, - 10426 - ], - [ - 26, - -92 - ], - [ - -59, - 50 - ], - [ - -58, - 10 - ], - [ - -40, - -8 - ], - [ - -48, - 4 - ], - [ - 17, - 66 - ], - [ - 86, - 5 - ], - [ - 76, - -35 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 2, - -391 - ], - [ - 1, - -391 - ] - ], - [ - [ - 22379, - 9703 - ], - [ - -62, - 99 - ], - [ - -71, - 24 - ], - [ - -17, - -34 - ], - [ - -89, - -4 - ], - [ - 30, - 98 - ], - [ - 44, - 33 - ], - [ - -18, - 130 - ], - [ - -34, - 101 - ], - [ - -135, - 102 - ], - [ - -57, - 10 - ], - [ - -105, - 111 - ], - [ - -21, - -59 - ], - [ - -26, - -10 - ], - [ - -16, - 44 - ], - [ - 0, - 52 - ], - [ - -54, - 59 - ], - [ - 75, - 43 - ], - [ - 50, - -2 - ], - [ - -6, - 32 - ], - [ - -102, - 0 - ], - [ - -27, - 71 - ], - [ - -63, - 22 - ], - [ - -29, - 60 - ], - [ - 94, - 29 - ], - [ - 35, - 39 - ], - [ - 112, - -49 - ], - [ - 11, - -45 - ], - [ - 20, - -194 - ], - [ - 72, - -72 - ], - [ - 58, - 127 - ], - [ - 80, - 73 - ], - [ - 62, - 0 - ], - [ - 60, - -42 - ], - [ - 52, - -43 - ], - [ - 74, - -23 - ] - ], - [ - [ - 21278, - 10968 - ], - [ - -56, - -119 - ], - [ - -53, - -24 - ], - [ - -67, - 24 - ], - [ - -116, - -6 - ], - [ - -61, - -17 - ], - [ - -10, - -91 - ], - [ - 63, - -107 - ], - [ - 37, - 55 - ], - [ - 130, - 40 - ], - [ - -5, - -55 - ], - [ - -31, - 18 - ], - [ - -30, - -71 - ], - [ - -61, - -46 - ], - [ - 66, - -154 - ], - [ - -13, - -41 - ], - [ - 63, - -139 - ], - [ - -1, - -79 - ], - [ - -37, - -35 - ], - [ - -28, - 42 - ], - [ - 34, - 99 - ], - [ - -68, - -47 - ], - [ - -18, - 33 - ], - [ - 9, - 47 - ], - [ - -50, - 70 - ], - [ - 5, - 117 - ], - [ - -46, - -37 - ], - [ - 6, - -139 - ], - [ - 3, - -172 - ], - [ - -45, - -17 - ], - [ - -30, - 35 - ], - [ - 20, - 110 - ], - [ - -10, - 116 - ], - [ - -30, - 1 - ], - [ - -21, - 82 - ], - [ - 28, - 79 - ], - [ - 10, - 95 - ], - [ - 35, - 181 - ], - [ - 15, - 49 - ], - [ - 59, - 89 - ], - [ - 55, - -35 - ], - [ - 88, - -17 - ], - [ - 80, - 5 - ], - [ - 69, - 87 - ], - [ - 12, - -26 - ] - ], - [ - [ - 21518, - 10933 - ], - [ - -4, - -105 - ], - [ - -35, - 12 - ], - [ - -11, - -73 - ], - [ - 29, - -63 - ], - [ - -20, - -15 - ], - [ - -28, - 76 - ], - [ - -21, - 154 - ], - [ - 14, - 95 - ], - [ - 23, - 44 - ], - [ - 5, - -65 - ], - [ - 42, - -11 - ], - [ - 6, - -49 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 12, - -80 - ], - [ - 47, - -68 - ], - [ - 45, - 24 - ], - [ - 45, - -8 - ], - [ - 40, - 60 - ], - [ - 34, - 11 - ], - [ - 66, - -34 - ], - [ - 57, - 26 - ], - [ - 35, - 167 - ], - [ - 27, - 41 - ], - [ - 24, - 137 - ], - [ - 80, - 0 - ], - [ - 61, - -20 - ] - ], - [ - [ - 20765, - 11294 - ], - [ - -40, - -109 - ], - [ - 51, - -113 - ], - [ - -12, - -56 - ], - [ - 79, - -111 - ], - [ - -83, - -14 - ], - [ - -23, - -82 - ], - [ - 3, - -108 - ], - [ - -67, - -82 - ], - [ - -2, - -120 - ], - [ - -27, - -183 - ], - [ - -10, - 42 - ], - [ - -79, - -54 - ], - [ - -28, - 74 - ], - [ - -50, - 7 - ], - [ - -35, - 38 - ], - [ - -82, - -43 - ], - [ - -26, - 58 - ], - [ - -46, - -7 - ], - [ - -57, - 14 - ], - [ - -11, - 161 - ], - [ - -34, - 33 - ], - [ - -34, - 103 - ], - [ - -10, - 105 - ], - [ - 9, - 111 - ], - [ - 41, - 80 - ] - ], - [ - [ - 19924, - 10095 - ], - [ - -77, - -2 - ], - [ - -59, - 100 - ], - [ - -90, - 98 - ], - [ - -29, - 73 - ], - [ - -53, - 97 - ], - [ - -35, - 90 - ], - [ - -53, - 168 - ], - [ - -61, - 100 - ], - [ - -20, - 103 - ], - [ - -26, - 94 - ], - [ - -63, - 75 - ], - [ - -36, - 103 - ], - [ - -53, - 67 - ], - [ - -73, - 133 - ], - [ - -6, - 61 - ], - [ - 45, - -5 - ], - [ - 108, - -23 - ], - [ - 62, - -118 - ], - [ - 54, - -81 - ], - [ - 38, - -50 - ], - [ - 66, - -129 - ], - [ - 71, - -2 - ], - [ - 58, - -82 - ], - [ - 41, - -100 - ], - [ - 53, - -55 - ], - [ - -28, - -98 - ], - [ - 40, - -42 - ], - [ - 25, - -3 - ], - [ - 12, - -84 - ], - [ - 24, - -67 - ], - [ - 51, - -10 - ], - [ - 34, - -76 - ], - [ - -17, - -149 - ], - [ - -3, - -186 - ] - ], - [ - [ - 18754, - 13443 - ], - [ - -10, - -44 - ], - [ - -48, - 2 - ], - [ - -86, - -25 - ], - [ - 4, - -90 - ], - [ - -37, - -71 - ], - [ - -100, - -81 - ], - [ - -78, - -141 - ], - [ - -53, - -76 - ], - [ - -69, - -78 - ], - [ - 0, - -56 - ], - [ - -35, - -29 - ], - [ - -63, - -43 - ], - [ - -32, - -6 - ], - [ - -21, - -92 - ], - [ - 14, - -156 - ], - [ - 4, - -99 - ], - [ - -29, - -114 - ], - [ - -1, - -204 - ], - [ - -36, - -6 - ], - [ - -32, - -92 - ], - [ - 22, - -39 - ], - [ - -64, - -34 - ], - [ - -23, - -82 - ], - [ - -28, - -34 - ], - [ - -66, - 112 - ], - [ - -33, - 168 - ], - [ - -26, - 121 - ], - [ - -25, - 57 - ], - [ - -37, - 115 - ], - [ - -17, - 150 - ], - [ - -12, - 75 - ], - [ - -64, - 165 - ], - [ - -28, - 232 - ], - [ - -21, - 154 - ], - [ - 0, - 145 - ], - [ - -14, - 112 - ], - [ - -101, - -72 - ], - [ - -49, - 15 - ], - [ - -91, - 145 - ], - [ - 33, - 44 - ], - [ - -20, - 47 - ], - [ - -82, - 101 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - 46, - 81 - ], - [ - 154, - -1 - ], - [ - -14, - 103 - ], - [ - -39, - 61 - ], - [ - -8, - 92 - ], - [ - -46, - 54 - ], - [ - 77, - 126 - ], - [ - 81, - -9 - ], - [ - 73, - 126 - ], - [ - 44, - 121 - ], - [ - 67, - 121 - ], - [ - -1, - 85 - ], - [ - 60, - 70 - ], - [ - -57, - 59 - ], - [ - -24, - 81 - ], - [ - -25, - 105 - ], - [ - 35, - 52 - ], - [ - 105, - -29 - ], - [ - 78, - 18 - ], - [ - 67, - 100 - ] - ], - [ - [ - 18202, - 14418 - ], - [ - -45, - -54 - ], - [ - -27, - -112 - ], - [ - 68, - -46 - ], - [ - 66, - -59 - ], - [ - 91, - -67 - ], - [ - 95, - -15 - ], - [ - 40, - -61 - ], - [ - 54, - -12 - ], - [ - 84, - -28 - ], - [ - 58, - 2 - ], - [ - 8, - 48 - ], - [ - -9, - 76 - ], - [ - 5, - 52 - ] - ], - [ - [ - 19332, - 14188 - ], - [ - 5, - -46 - ], - [ - -24, - -22 - ], - [ - 6, - -74 - ], - [ - -50, - 22 - ], - [ - -91, - -83 - ], - [ - 3, - -68 - ], - [ - -39, - -101 - ], - [ - -3, - -59 - ], - [ - -31, - -98 - ], - [ - -55, - 27 - ], - [ - -3, - -124 - ], - [ - -15, - -41 - ], - [ - 7, - -51 - ], - [ - -34, - -29 - ] - ], - [ - [ - 12115, - 17260 - ], - [ - 12, - -86 - ], - [ - -53, - -107 - ], - [ - -123, - -71 - ], - [ - -99, - 18 - ], - [ - 57, - 125 - ], - [ - -37, - 122 - ], - [ - 95, - 94 - ], - [ - 53, - 56 - ] - ], - [ - [ - 16791, - 14376 - ], - [ - 34, - -63 - ], - [ - 29, - -73 - ], - [ - 66, - -53 - ], - [ - 2, - -105 - ], - [ - 33, - -20 - ], - [ - 6, - -55 - ], - [ - -100, - -62 - ], - [ - -27, - -139 - ] - ], - [ - [ - 16834, - 13806 - ], - [ - -131, - 36 - ], - [ - -76, - 28 - ], - [ - -78, - 15 - ], - [ - -30, - 147 - ], - [ - -34, - 22 - ], - [ - -53, - -22 - ], - [ - -70, - -58 - ], - [ - -86, - 40 - ], - [ - -70, - 92 - ], - [ - -67, - 34 - ], - [ - -47, - 114 - ], - [ - -51, - 160 - ], - [ - -38, - -19 - ], - [ - -44, - 39 - ], - [ - -26, - -47 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -38, - 64 - ], - [ - -1, - 63 - ], - [ - -22, - 0 - ], - [ - 11, - 87 - ], - [ - -36, - 91 - ], - [ - -85, - 66 - ], - [ - -49, - 114 - ], - [ - 17, - 94 - ], - [ - 35, - 41 - ], - [ - -6, - 70 - ], - [ - -45, - 36 - ], - [ - -45, - 143 - ] - ], - [ - [ - 15669, - 15256 - ], - [ - -39, - 97 - ], - [ - 14, - 37 - ], - [ - -22, - 137 - ], - [ - 48, - 35 - ] - ], - [ - [ - 15955, - 15394 - ], - [ - 22, - -88 - ], - [ - 66, - -25 - ], - [ - 49, - -60 - ], - [ - 99, - -21 - ], - [ - 109, - 32 - ], - [ - 6, - 28 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - 62, - 23 - ], - [ - 49, - 69 - ], - [ - 47, - -4 - ], - [ - 30, - 23 - ], - [ - 50, - -11 - ], - [ - 77, - -61 - ], - [ - 56, - -13 - ], - [ - 79, - -107 - ], - [ - 52, - -4 - ], - [ - 6, - -101 - ] - ], - [ - [ - 15933, - 14387 - ], - [ - -41, - 6 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - -47, - 10 - ], - [ - -51, - -115 - ] - ], - [ - [ - 15794, - 14288 - ], - [ - -130, - 10 - ], - [ - -196, - 241 - ], - [ - -104, - 84 - ], - [ - -84, - 33 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -28, - 146 - ] - ], - [ - [ - 15252, - 14802 - ], - [ - 154, - 124 - ], - [ - 26, - 145 - ], - [ - -6, - 88 - ], - [ - 38, - 30 - ], - [ - 36, - 75 - ] - ], - [ - [ - 15500, - 15264 - ], - [ - 30, - 18 - ], - [ - 81, - -15 - ], - [ - 24, - -31 - ], - [ - 34, - 20 - ] - ], - [ - [ - 11536, - 18770 - ], - [ - -16, - -78 - ], - [ - 79, - -82 - ], - [ - -91, - -91 - ], - [ - -201, - -82 - ], - [ - -60, - -22 - ], - [ - -92, - 17 - ], - [ - -194, - 38 - ], - [ - 68, - 53 - ], - [ - -151, - 59 - ], - [ - 123, - 23 - ], - [ - -3, - 36 - ], - [ - -146, - 27 - ], - [ - 47, - 79 - ], - [ - 106, - 17 - ], - [ - 108, - -81 - ], - [ - 106, - 65 - ], - [ - 88, - -34 - ], - [ - 113, - 64 - ], - [ - 116, - -8 - ] - ], - [ - [ - 14936, - 14543 - ], - [ - 20, - 39 - ], - [ - -4, - 7 - ], - [ - 18, - 56 - ], - [ - 14, - 90 - ], - [ - 10, - 31 - ], - [ - 2, - 1 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 23, - 0 - ], - [ - 7, - 21 - ], - [ - 19, - 1 - ] - ], - [ - [ - 15045, - 14789 - ], - [ - 1, - -49 - ], - [ - -10, - -18 - ], - [ - 1, - -1 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - -12, - -38 - ] - ], - [ - [ - 15025, - 14683 - ], - [ - -25, - 17 - ], - [ - -14, - -80 - ], - [ - 17, - -13 - ], - [ - -18, - -17 - ], - [ - -3, - -31 - ], - [ - 33, - 16 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 2, - -47 - ], - [ - -35, - -192 - ] - ], - [ - [ - 13510, - 16377 - ], - [ - -8, - -59 - ], - [ - 17, - -51 - ] - ], - [ - [ - 13519, - 16267 - ], - [ - -55, - 17 - ], - [ - -57, - -42 - ], - [ - 4, - -60 - ], - [ - -9, - -34 - ], - [ - 23, - -61 - ], - [ - 65, - -61 - ], - [ - 35, - -99 - ], - [ - 78, - -96 - ], - [ - 55, - 0 - ], - [ - 17, - -26 - ], - [ - -20, - -24 - ], - [ - 63, - -44 - ], - [ - 51, - -36 - ], - [ - 60, - -62 - ], - [ - 7, - -23 - ], - [ - -13, - -43 - ], - [ - -39, - 56 - ], - [ - -61, - 20 - ], - [ - -29, - -78 - ], - [ - 50, - -44 - ], - [ - -8, - -63 - ], - [ - -29, - -7 - ], - [ - -37, - -103 - ], - [ - -29, - -9 - ], - [ - 0, - 37 - ], - [ - 14, - 64 - ], - [ - 15, - 26 - ], - [ - -27, - 69 - ], - [ - -21, - 61 - ], - [ - -29, - 15 - ], - [ - -21, - 51 - ], - [ - -44, - 22 - ], - [ - -31, - 49 - ], - [ - -51, - 7 - ], - [ - -55, - 54 - ], - [ - -63, - 79 - ], - [ - -48, - 69 - ], - [ - -21, - 118 - ], - [ - -35, - 14 - ], - [ - -57, - 40 - ], - [ - -32, - -16 - ], - [ - -40, - -56 - ], - [ - -29, - -9 - ] - ], - [ - [ - 13629, - 15384 - ], - [ - -25, - -95 - ], - [ - 11, - -37 - ], - [ - -15, - -62 - ], - [ - -53, - 46 - ], - [ - -36, - 13 - ], - [ - -97, - 61 - ], - [ - 10, - 61 - ], - [ - 81, - -11 - ], - [ - 71, - 13 - ], - [ - 53, - 11 - ] - ], - [ - [ - 13190, - 15741 - ], - [ - 41, - -85 - ], - [ - -9, - -159 - ], - [ - -32, - 8 - ], - [ - -29, - -40 - ], - [ - -26, - 32 - ], - [ - -3, - 144 - ], - [ - -16, - 69 - ], - [ - 39, - -6 - ], - [ - 35, - 37 - ] - ], - [ - [ - 7140, - 13015 - ], - [ - 47, - -10 - ], - [ - 37, - -29 - ], - [ - 12, - -33 - ], - [ - -49, - -2 - ], - [ - -21, - -20 - ], - [ - -39, - 19 - ], - [ - -40, - 44 - ], - [ - 8, - 27 - ], - [ - 29, - 9 - ], - [ - 16, - -5 - ] - ], - [ - [ - 15280, - 14656 - ], - [ - -14, - -19 - ], - [ - -139, - -60 - ], - [ - 69, - -120 - ], - [ - -23, - -20 - ], - [ - -11, - -40 - ], - [ - -53, - -17 - ], - [ - -17, - -43 - ], - [ - -30, - -37 - ], - [ - -78, - 19 - ] - ], - [ - [ - 14984, - 14319 - ], - [ - -2, - 17 - ] - ], - [ - [ - 15015, - 14575 - ], - [ - 10, - 35 - ], - [ - 0, - 73 - ] - ], - [ - [ - 15037, - 14721 - ], - [ - 78, - -47 - ], - [ - 137, - 128 - ] - ], - [ - [ - 21933, - 14894 - ], - [ - 9, - -41 - ], - [ - -39, - -73 - ], - [ - -29, - 39 - ], - [ - -36, - -28 - ], - [ - -18, - -70 - ], - [ - -46, - 34 - ], - [ - 1, - 57 - ], - [ - 38, - 71 - ], - [ - 40, - -14 - ], - [ - 29, - 51 - ], - [ - 51, - -26 - ] - ], - [ - [ - 22375, - 15253 - ], - [ - -27, - -96 - ], - [ - 13, - -60 - ], - [ - -37, - -84 - ], - [ - -89, - -57 - ], - [ - -122, - -7 - ], - [ - -100, - -137 - ], - [ - -46, - 46 - ], - [ - -3, - 90 - ], - [ - -122, - -27 - ], - [ - -82, - -56 - ], - [ - -82, - -3 - ], - [ - 71, - -88 - ], - [ - -47, - -204 - ], - [ - -45, - -50 - ], - [ - -33, - 46 - ], - [ - 17, - 109 - ], - [ - -44, - 34 - ], - [ - -29, - 83 - ], - [ - 66, - 37 - ], - [ - 37, - 75 - ], - [ - 70, - 62 - ], - [ - 51, - 82 - ], - [ - 139, - 36 - ], - [ - 74, - -25 - ], - [ - 73, - 214 - ], - [ - 47, - -58 - ], - [ - 102, - 120 - ], - [ - 40, - 47 - ], - [ - 43, - 147 - ], - [ - -11, - 135 - ], - [ - 29, - 75 - ], - [ - 74, - 22 - ], - [ - 38, - -166 - ], - [ - -2, - -97 - ], - [ - -64, - -121 - ], - [ - 1, - -124 - ] - ], - [ - [ - 22579, - 16097 - ], - [ - 49, - -26 - ], - [ - 50, - 51 - ], - [ - 15, - -135 - ], - [ - -103, - -33 - ], - [ - -61, - -119 - ], - [ - -110, - 82 - ], - [ - -38, - -131 - ], - [ - -77, - -2 - ], - [ - -10, - 120 - ], - [ - 34, - 92 - ], - [ - 75, - 6 - ], - [ - 20, - 166 - ], - [ - 21, - 94 - ], - [ - 82, - -125 - ], - [ - 53, - -40 - ] - ], - [ - [ - 18142, - 15878 - ], - [ - -43, - 17 - ], - [ - -35, - 44 - ], - [ - -103, - 12 - ], - [ - -116, - 3 - ], - [ - -25, - -13 - ], - [ - -99, - 51 - ], - [ - -40, - -25 - ], - [ - -11, - -71 - ], - [ - -114, - 41 - ], - [ - -46, - -17 - ], - [ - -16, - -52 - ] - ], - [ - [ - 17494, - 15868 - ], - [ - -40, - -22 - ], - [ - -92, - -84 - ], - [ - -30, - -86 - ], - [ - -26, - -1 - ], - [ - -19, - 57 - ], - [ - -89, - 4 - ], - [ - -14, - 98 - ], - [ - -34, - 1 - ], - [ - 5, - 121 - ], - [ - -83, - 87 - ], - [ - -120, - -9 - ], - [ - -82, - -18 - ], - [ - -66, - 109 - ], - [ - -57, - 45 - ], - [ - -108, - 86 - ], - [ - -13, - 10 - ], - [ - -180, - -71 - ], - [ - 3, - -442 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - -36, - -6 - ], - [ - -49, - 94 - ], - [ - -47, - 34 - ], - [ - -79, - -25 - ], - [ - -31, - -40 - ] - ], - [ - [ - 16207, - 15810 - ], - [ - -4, - 29 - ], - [ - 18, - 50 - ], - [ - -14, - 42 - ], - [ - -81, - 41 - ], - [ - -31, - 108 - ], - [ - -38, - 30 - ], - [ - -3, - 39 - ], - [ - 68, - -11 - ], - [ - 3, - 87 - ], - [ - 59, - 20 - ], - [ - 61, - -18 - ], - [ - 12, - 117 - ], - [ - -12, - 74 - ], - [ - -70, - -6 - ], - [ - -59, - 30 - ], - [ - -81, - -53 - ], - [ - -65, - -25 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -35, - 19 - ], - [ - 7, - 62 - ], - [ - -45, - 80 - ], - [ - -51, - -3 - ], - [ - -59, - 81 - ], - [ - 40, - 91 - ], - [ - -21, - 24 - ], - [ - 56, - 132 - ], - [ - 72, - -69 - ], - [ - 8, - 87 - ], - [ - 144, - 131 - ], - [ - 109, - 3 - ], - [ - 154, - -83 - ], - [ - 82, - -49 - ], - [ - 74, - 51 - ], - [ - 111, - 2 - ], - [ - 89, - -62 - ], - [ - 20, - 36 - ], - [ - 98, - -6 - ], - [ - 18, - 57 - ], - [ - -113, - 83 - ], - [ - 67, - 58 - ], - [ - -13, - 33 - ], - [ - 67, - 31 - ], - [ - -51, - 82 - ], - [ - 32, - 41 - ], - [ - 261, - 42 - ], - [ - 34, - 30 - ], - [ - 174, - 44 - ], - [ - 63, - 50 - ], - [ - 125, - -26 - ], - [ - 22, - -125 - ], - [ - 73, - 30 - ], - [ - 90, - -41 - ], - [ - -6, - -66 - ], - [ - 67, - 7 - ], - [ - 174, - 113 - ], - [ - -25, - -37 - ], - [ - 89, - -93 - ], - [ - 156, - -305 - ], - [ - 37, - 63 - ], - [ - 96, - -69 - ], - [ - 100, - 31 - ], - [ - 38, - -22 - ], - [ - 34, - -69 - ], - [ - 49, - -23 - ], - [ - 29, - -51 - ], - [ - 90, - 16 - ], - [ - 37, - -74 - ] - ], - [ - [ - 15465, - 11267 - ], - [ - -61, - -136 - ], - [ - 1, - -437 - ], - [ - 41, - -99 - ] - ], - [ - [ - 15446, - 10595 - ], - [ - -48, - -48 - ], - [ - -18, - -50 - ], - [ - -26, - -8 - ], - [ - -10, - -85 - ], - [ - -22, - -48 - ], - [ - -14, - -80 - ], - [ - -28, - -40 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -100, - 120 - ], - [ - -5, - 70 - ], - [ - -252, - 244 - ], - [ - -12, - 13 - ] - ], - [ - [ - 14911, - 10683 - ], - [ - -1, - 127 - ], - [ - 20, - 49 - ], - [ - 34, - 79 - ], - [ - 26, - 88 - ], - [ - -31, - 138 - ], - [ - -8, - 60 - ], - [ - -33, - 83 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - 43, - 72 - ], - [ - 47, - 79 - ] - ], - [ - [ - 17683, - 15528 - ], - [ - -132, - -18 - ], - [ - -86, - 38 - ], - [ - -75, - -9 - ], - [ - 6, - 69 - ], - [ - 76, - -20 - ], - [ - 26, - 37 - ] - ], - [ - [ - 17498, - 15625 - ], - [ - 53, - -12 - ], - [ - 89, - 87 - ], - [ - -83, - 63 - ], - [ - -49, - -30 - ], - [ - -52, - 45 - ], - [ - 59, - 78 - ], - [ - -21, - 12 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -17, - 145 - ], - [ - 45, - 100 - ], - [ - 90, - 23 - ], - [ - 65, - -17 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 58, - -48 - ], - [ - 31, - 83 - ], - [ - 62, - -44 - ] - ], - [ - [ - 20033, - 12501 - ], - [ - 16, - -80 - ], - [ - -8, - -144 - ], - [ - -118, - -92 - ], - [ - 31, - -73 - ], - [ - -73, - -8 - ], - [ - -61, - -49 - ] - ], - [ - [ - 19820, - 12055 - ], - [ - -58, - 18 - ], - [ - -28, - 62 - ], - [ - -35, - 124 - ] - ], - [ - [ - 21495, - 15429 - ], - [ - 60, - -141 - ], - [ - 17, - -78 - ], - [ - 1, - -138 - ], - [ - -27, - -66 - ], - [ - -63, - -23 - ], - [ - -56, - -50 - ], - [ - -62, - -10 - ], - [ - -8, - 65 - ], - [ - 13, - 90 - ], - [ - -31, - 125 - ], - [ - 52, - 20 - ], - [ - -48, - 103 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - 4, - 11 - ], - [ - 31, - -4 - ], - [ - 28, - 54 - ], - [ - 49, - 6 - ], - [ - 30, - 7 - ], - [ - 10, - 29 - ] - ], - [ - [ - 13947, - 15907 - ], - [ - 13, - 26 - ] - ], - [ - [ - 13960, - 15933 - ], - [ - 16, - 9 - ], - [ - 10, - 40 - ], - [ - 12, - 6 - ], - [ - 10, - -16 - ], - [ - 13, - -8 - ], - [ - 9, - -19 - ], - [ - 12, - -6 - ], - [ - 14, - -22 - ], - [ - 9, - 1 - ], - [ - -7, - -29 - ], - [ - -9, - -15 - ], - [ - 3, - -9 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - -16, - -4 - ], - [ - -41, - -19 - ], - [ - -3, - -24 - ], - [ - -9, - 1 - ] - ], - [ - [ - 15892, - 14393 - ], - [ - 14, - -53 - ], - [ - -6, - -27 - ], - [ - 23, - -90 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - -50, - -4 - ], - [ - -17, - 58 - ], - [ - -62, - 11 - ] - ], - [ - [ - 19670, - 13492 - ], - [ - 40, - -94 - ], - [ - 32, - -109 - ], - [ - 85, - -1 - ], - [ - 28, - -105 - ], - [ - -45, - -31 - ], - [ - -20, - -44 - ], - [ - 83, - -71 - ], - [ - 58, - -142 - ], - [ - 44, - -106 - ], - [ - 53, - -83 - ], - [ - 18, - -85 - ], - [ - -13, - -120 - ] - ], - [ - [ - 19882, - 12510 - ], - [ - 23, - 54 - ], - [ - 3, - 101 - ], - [ - -57, - 105 - ], - [ - -4, - 118 - ], - [ - -53, - 98 - ], - [ - -53, - 8 - ], - [ - -14, - -42 - ], - [ - -40, - -3 - ], - [ - -21, - 21 - ], - [ - -74, - -72 - ], - [ - -1, - 108 - ], - [ - 17, - 126 - ], - [ - -47, - 6 - ], - [ - -4, - 72 - ], - [ - -31, - 37 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - 15, - 44 - ], - [ - 60, - 78 - ] - ], - [ - [ - 14996, - 14767 - ], - [ - 25, - 98 - ], - [ - 35, - 84 - ], - [ - 1, - 5 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - 31, - -7 - ], - [ - 12, - -47 - ], - [ - -38, - -45 - ], - [ - -17, - -66 - ] - ], - [ - [ - 12010, - 11321 - ], - [ - -18, - -1 - ], - [ - -72, - 57 - ], - [ - -64, - 91 - ], - [ - -59, - 66 - ], - [ - -47, - 77 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - 17, - 39 - ], - [ - 3, - 35 - ], - [ - 32, - 65 - ], - [ - 32, - 56 - ] - ], - [ - [ - 13208, - 14433 - ], - [ - 34, - 28 - ], - [ - 7, - 51 - ], - [ - -8, - 49 - ], - [ - 48, - 47 - ], - [ - 21, - 38 - ], - [ - 34, - 34 - ], - [ - 4, - 93 - ] - ], - [ - [ - 13348, - 14773 - ], - [ - 82, - -42 - ], - [ - 30, - 11 - ], - [ - 58, - -20 - ], - [ - 92, - -54 - ], - [ - 33, - -107 - ], - [ - 62, - -23 - ], - [ - 99, - -50 - ], - [ - 74, - -60 - ], - [ - 34, - 31 - ], - [ - 33, - 56 - ], - [ - -16, - 91 - ], - [ - 22, - 59 - ], - [ - 50, - 56 - ], - [ - 48, - 16 - ], - [ - 95, - -24 - ], - [ - 23, - -54 - ], - [ - 26, - 0 - ], - [ - 22, - -21 - ], - [ - 70, - -14 - ], - [ - 17, - -39 - ] - ], - [ - [ - 14290, - 13437 - ], - [ - 0, - -240 - ], - [ - -80, - 0 - ], - [ - -1, - -51 - ] - ], - [ - [ - 14209, - 13146 - ], - [ - -278, - 230 - ], - [ - -278, - 230 - ], - [ - -70, - -66 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - -50, - -45 - ], - [ - -39, - 66 - ], - [ - -110, - 52 - ] - ], - [ - [ - 18249, - 11700 - ], - [ - -11, - -125 - ], - [ - -29, - -34 - ], - [ - -61, - -28 - ], - [ - -33, - 96 - ], - [ - -12, - 172 - ], - [ - 31, - 195 - ], - [ - 49, - -67 - ], - [ - 32, - -84 - ], - [ - 34, - -125 - ] - ], - [ - [ - 14568, - 7323 - ], - [ - 24, - -36 - ], - [ - -22, - -58 - ], - [ - -12, - -39 - ], - [ - -38, - -19 - ], - [ - -13, - -38 - ], - [ - -25, - -12 - ], - [ - -52, - 92 - ], - [ - 37, - 76 - ], - [ - 38, - 47 - ], - [ - 32, - 24 - ], - [ - 31, - -37 - ] - ], - [ - [ - 14185, - 17265 - ], - [ - -17, - 37 - ], - [ - -36, - 13 - ] - ], - [ - [ - 14132, - 17315 - ], - [ - -6, - 30 - ], - [ - 8, - 33 - ], - [ - -31, - 19 - ], - [ - -73, - 21 - ] - ], - [ - [ - 14030, - 17418 - ], - [ - -15, - 101 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 80, - 37 - ], - [ - 117, - -8 - ], - [ - 68, - 12 - ], - [ - 10, - -25 - ], - [ - 37, - -8 - ], - [ - 67, - -58 - ] - ], - [ - [ - 14015, - 17519 - ], - [ - 3, - 90 - ], - [ - 34, - 76 - ], - [ - 66, - 41 - ], - [ - 55, - -90 - ], - [ - 56, - 2 - ], - [ - 13, - 93 - ] - ], - [ - [ - 14450, - 17692 - ], - [ - 33, - -27 - ], - [ - 6, - -58 - ], - [ - 23, - -71 - ] - ], - [ - [ - 11943, - 14115 - ], - [ - -10, - 0 - ], - [ - 1, - -64 - ], - [ - -43, - -4 - ], - [ - -22, - -27 - ], - [ - -32, - 0 - ], - [ - -25, - 15 - ], - [ - -59, - -13 - ], - [ - -22, - -93 - ], - [ - -22, - -9 - ], - [ - -33, - -151 - ], - [ - -97, - -130 - ], - [ - -23, - -165 - ], - [ - -28, - -54 - ], - [ - -9, - -43 - ], - [ - -157, - -10 - ], - [ - -1, - 0 - ] - ], - [ - [ - 11361, - 13367 - ], - [ - 3, - 56 - ], - [ - 27, - 32 - ], - [ - 23, - 63 - ], - [ - -5, - 41 - ], - [ - 24, - 84 - ], - [ - 39, - 77 - ], - [ - 24, - 19 - ], - [ - 18, - 70 - ], - [ - 2, - 64 - ], - [ - 25, - 74 - ], - [ - 46, - 44 - ], - [ - 45, - 122 - ], - [ - 1, - 2 - ], - [ - 35, - 46 - ], - [ - 65, - 13 - ], - [ - 55, - 82 - ], - [ - 35, - 32 - ], - [ - 58, - 100 - ], - [ - -18, - 150 - ], - [ - 27, - 103 - ], - [ - 9, - 63 - ], - [ - 45, - 81 - ], - [ - 70, - 55 - ], - [ - 52, - 49 - ], - [ - 46, - 125 - ], - [ - 22, - 73 - ], - [ - 51, - 0 - ], - [ - 42, - -51 - ], - [ - 67, - 8 - ], - [ - 72, - -26 - ], - [ - 30, - -2 - ] - ], - [ - [ - 14403, - 16582 - ], - [ - 17, - 18 - ], - [ - 46, - 12 - ], - [ - 51, - -38 - ], - [ - 29, - -4 - ], - [ - 32, - -32 - ], - [ - -5, - -41 - ], - [ - 25, - -20 - ], - [ - 10, - -50 - ], - [ - 24, - -30 - ], - [ - -5, - -18 - ], - [ - 13, - -12 - ], - [ - -18, - -9 - ], - [ - -41, - 3 - ], - [ - -7, - 17 - ], - [ - -15, - -10 - ], - [ - 5, - -21 - ], - [ - -19, - -38 - ], - [ - -12, - -42 - ], - [ - -17, - -13 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - -13, - 55 - ], - [ - 7, - 51 - ], - [ - -2, - 53 - ], - [ - -40, - 71 - ], - [ - -22, - 51 - ], - [ - -22, - 35 - ], - [ - -21, - 12 - ] - ], - [ - [ - 16001, - 9301 - ], - [ - 19, - -51 - ], - [ - 17, - -79 - ], - [ - 11, - -144 - ], - [ - 18, - -57 - ], - [ - -7, - -57 - ], - [ - -12, - -35 - ], - [ - -24, - 70 - ], - [ - -13, - -36 - ], - [ - 13, - -88 - ], - [ - -6, - -51 - ], - [ - -19, - -28 - ], - [ - -4, - -102 - ], - [ - -28, - -139 - ], - [ - -34, - -166 - ], - [ - -43, - -227 - ], - [ - -27, - -167 - ], - [ - -32, - -139 - ], - [ - -56, - -28 - ], - [ - -61, - -51 - ], - [ - -40, - 30 - ], - [ - -56, - 43 - ], - [ - -19, - 64 - ], - [ - -4, - 106 - ], - [ - -25, - 96 - ], - [ - -6, - 86 - ], - [ - 12, - 86 - ], - [ - 32, - 21 - ], - [ - 0, - 40 - ], - [ - 34, - 91 - ], - [ - 6, - 77 - ], - [ - -16, - 56 - ], - [ - -13, - 76 - ], - [ - -6, - 111 - ], - [ - 24, - 67 - ], - [ - 10, - 76 - ], - [ - 35, - 4 - ], - [ - 38, - 25 - ], - [ - 26, - 21 - ], - [ - 31, - 2 - ], - [ - 40, - 68 - ], - [ - 57, - 74 - ], - [ - 21, - 61 - ], - [ - -10, - 51 - ], - [ - 30, - -14 - ], - [ - 38, - 83 - ], - [ - 2, - 72 - ], - [ - 23, - 54 - ], - [ - 24, - -52 - ] - ], - [ - [ - 6118, - 12541 - ], - [ - -78, - 130 - ], - [ - -36, - 39 - ], - [ - -57, - 31 - ], - [ - -39, - -9 - ], - [ - -56, - -45 - ], - [ - -35, - -12 - ], - [ - -50, - 32 - ], - [ - -52, - 23 - ], - [ - -65, - 55 - ], - [ - -52, - 16 - ], - [ - -79, - 56 - ], - [ - -58, - 58 - ], - [ - -18, - 32 - ], - [ - -39, - 7 - ], - [ - -71, - 38 - ], - [ - -29, - 54 - ], - [ - -75, - 69 - ], - [ - -35, - 75 - ], - [ - -17, - 59 - ], - [ - 23, - 11 - ], - [ - -7, - 35 - ], - [ - 16, - 31 - ], - [ - 1, - 41 - ], - [ - -24, - 54 - ], - [ - -6, - 48 - ], - [ - -24, - 60 - ], - [ - -61, - 120 - ], - [ - -70, - 93 - ], - [ - -34, - 75 - ], - [ - -60, - 49 - ], - [ - -13, - 29 - ], - [ - 11, - 75 - ], - [ - -36, - 28 - ], - [ - -41, - 58 - ], - [ - -17, - 84 - ], - [ - -38, - 9 - ], - [ - -40, - 63 - ], - [ - -33, - 59 - ], - [ - -3, - 37 - ], - [ - -37, - 91 - ], - [ - -25, - 92 - ], - [ - 1, - 46 - ], - [ - -50, - 47 - ], - [ - -24, - -5 - ], - [ - -39, - 33 - ], - [ - -12, - -49 - ], - [ - 12, - -57 - ], - [ - 7, - -90 - ], - [ - 24, - -50 - ], - [ - 51, - -82 - ], - [ - 12, - -29 - ], - [ - 10, - -8 - ], - [ - 10, - -41 - ], - [ - 12, - 1 - ], - [ - 14, - -77 - ], - [ - 21, - -31 - ], - [ - 15, - -42 - ], - [ - 44, - -61 - ], - [ - 23, - -112 - ], - [ - 21, - -52 - ], - [ - 19, - -56 - ], - [ - 4, - -64 - ], - [ - 34, - -4 - ], - [ - 27, - -54 - ], - [ - 26, - -54 - ], - [ - -2, - -21 - ], - [ - -29, - -44 - ], - [ - -13, - 0 - ], - [ - -18, - 73 - ], - [ - -46, - 69 - ], - [ - -50, - 58 - ], - [ - -36, - 30 - ], - [ - 3, - 88 - ], - [ - -11, - 65 - ], - [ - -33, - 37 - ], - [ - -48, - 54 - ], - [ - -9, - -16 - ], - [ - -18, - 31 - ], - [ - -43, - 29 - ], - [ - -41, - 70 - ], - [ - 5, - 9 - ], - [ - 29, - -7 - ], - [ - 26, - 45 - ], - [ - 2, - 54 - ], - [ - -53, - 86 - ], - [ - -41, - 33 - ], - [ - -26, - 75 - ], - [ - -26, - 79 - ], - [ - -32, - 95 - ], - [ - -28, - 108 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - 79, - 10 - ], - [ - 88, - 13 - ], - [ - -6, - -24 - ], - [ - 105, - -58 - ], - [ - 159, - -85 - ], - [ - 139, - 1 - ], - [ - 55, - 0 - ], - [ - 0, - 50 - ], - [ - 121, - 0 - ], - [ - 25, - -43 - ], - [ - 36, - -38 - ], - [ - 42, - -52 - ], - [ - 23, - -63 - ], - [ - 17, - -66 - ], - [ - 36, - -36 - ], - [ - 58, - -36 - ], - [ - 44, - 94 - ], - [ - 57, - 3 - ], - [ - 49, - -48 - ], - [ - 35, - -82 - ], - [ - 24, - -70 - ], - [ - 41, - -69 - ], - [ - 15, - -84 - ], - [ - 20, - -56 - ], - [ - 54, - -37 - ], - [ - 50, - -27 - ], - [ - 27, - 4 - ] - ], - [ - [ - 5776, - 13901 - ], - [ - -27, - -106 - ], - [ - -12, - -86 - ], - [ - -5, - -161 - ], - [ - -7, - -58 - ], - [ - 12, - -66 - ], - [ - 22, - -58 - ], - [ - 14, - -93 - ], - [ - 46, - -90 - ], - [ - 16, - -68 - ], - [ - 27, - -59 - ], - [ - 74, - -32 - ], - [ - 29, - -50 - ], - [ - 61, - 33 - ], - [ - 54, - 13 - ], - [ - 52, - 21 - ], - [ - 44, - 21 - ], - [ - 44, - 49 - ], - [ - 17, - 70 - ], - [ - 5, - 100 - ], - [ - 12, - 36 - ], - [ - 48, - 31 - ], - [ - 73, - 28 - ], - [ - 62, - -4 - ], - [ - 42, - 10 - ], - [ - 17, - -26 - ], - [ - -2, - -57 - ], - [ - -38, - -72 - ], - [ - -16, - -73 - ], - [ - 12, - -21 - ], - [ - -10, - -52 - ], - [ - -17, - -93 - ], - [ - -18, - 31 - ], - [ - -15, - -2 - ] - ], - [ - [ - 14052, - 15865 - ], - [ - 23, - 7 - ], - [ - 33, - 2 - ] - ], - [ - [ - 11745, - 12290 - ], - [ - 3, - 37 - ], - [ - -6, - 47 - ], - [ - -26, - 33 - ], - [ - -14, - 69 - ], - [ - -3, - 75 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - 24, - 22 - ], - [ - 11, - 70 - ], - [ - 22, - 3 - ], - [ - 49, - -33 - ], - [ - 39, - 23 - ], - [ - 27, - -8 - ], - [ - 11, - 27 - ], - [ - 279, - 2 - ], - [ - 16, - 84 - ], - [ - -12, - 15 - ], - [ - -34, - 517 - ], - [ - -33, - 518 - ], - [ - 106, - 2 - ] - ], - [ - [ - 12845, - 13095 - ], - [ - 0, - -276 - ], - [ - -38, - -80 - ], - [ - -6, - -74 - ], - [ - -62, - -19 - ], - [ - -95, - -10 - ], - [ - -26, - -43 - ], - [ - -44, - -5 - ] - ], - [ - [ - 19526, - 13247 - ], - [ - -40, - -28 - ], - [ - -40, - -52 - ], - [ - -49, - -5 - ], - [ - -32, - -130 - ], - [ - -30, - -22 - ], - [ - 34, - -105 - ], - [ - 44, - -88 - ], - [ - 29, - -79 - ], - [ - -26, - -104 - ], - [ - -24, - -22 - ], - [ - 17, - -61 - ], - [ - 46, - -95 - ], - [ - 8, - -67 - ], - [ - -1, - -56 - ], - [ - 28, - -109 - ], - [ - -39, - -112 - ], - [ - -33, - -123 - ] - ], - [ - [ - 19418, - 11989 - ], - [ - -7, - 89 - ], - [ - 21, - 92 - ], - [ - -23, - 71 - ], - [ - 5, - 130 - ], - [ - -28, - 63 - ], - [ - -23, - 143 - ], - [ - -12, - 152 - ], - [ - -30, - 99 - ], - [ - -46, - -60 - ], - [ - -79, - -86 - ], - [ - -40, - 11 - ], - [ - -43, - 28 - ], - [ - 24, - 149 - ], - [ - -14, - 112 - ], - [ - -55, - 139 - ], - [ - 9, - 43 - ], - [ - -41, - 15 - ], - [ - -50, - 98 - ] - ], - [ - [ - 13898, - 15821 - ], - [ - -15, - 9 - ], - [ - -19, - 40 - ], - [ - -30, - 23 - ] - ], - [ - [ - 13887, - 16019 - ], - [ - 19, - -21 - ], - [ - 10, - -17 - ], - [ - 23, - -12 - ], - [ - 26, - -25 - ], - [ - -5, - -11 - ] - ], - [ - [ - 18664, - 16711 - ], - [ - 74, - 21 - ], - [ - 133, - 103 - ], - [ - 106, - 57 - ], - [ - 61, - -37 - ], - [ - 72, - -2 - ], - [ - 47, - -56 - ], - [ - 70, - -4 - ], - [ - 100, - -30 - ], - [ - 68, - 83 - ], - [ - -28, - 71 - ], - [ - 72, - 124 - ], - [ - 78, - -49 - ], - [ - 63, - -14 - ], - [ - 82, - -31 - ], - [ - 14, - -90 - ], - [ - 99, - -51 - ], - [ - 65, - 23 - ], - [ - 89, - 15 - ], - [ - 70, - -15 - ], - [ - 68, - -58 - ], - [ - 42, - -61 - ], - [ - 65, - 1 - ], - [ - 88, - -20 - ], - [ - 64, - 30 - ], - [ - 91, - 20 - ], - [ - 103, - 84 - ], - [ - 41, - -13 - ], - [ - 37, - -40 - ], - [ - 83, - 10 - ] - ], - [ - [ - 14957, - 9415 - ], - [ - 52, - 10 - ], - [ - 84, - -34 - ], - [ - 18, - 15 - ], - [ - 49, - 3 - ], - [ - 24, - 36 - ], - [ - 42, - -2 - ], - [ - 76, - 47 - ], - [ - 56, - 69 - ] - ], - [ - [ - 15358, - 9559 - ], - [ - 11, - -53 - ], - [ - -3, - -120 - ], - [ - 9, - -105 - ], - [ - 3, - -188 - ], - [ - 12, - -58 - ], - [ - -21, - -86 - ], - [ - -27, - -83 - ], - [ - -44, - -75 - ], - [ - -64, - -45 - ], - [ - -79, - -59 - ], - [ - -78, - -128 - ], - [ - -27, - -22 - ], - [ - -49, - -86 - ], - [ - -29, - -27 - ], - [ - -5, - -86 - ], - [ - 33, - -91 - ], - [ - 13, - -70 - ], - [ - 1, - -36 - ], - [ - 13, - 6 - ], - [ - -2, - -118 - ], - [ - -12, - -55 - ], - [ - 17, - -21 - ], - [ - -11, - -50 - ], - [ - -29, - -42 - ], - [ - -57, - -41 - ], - [ - -84, - -65 - ], - [ - -31, - -44 - ], - [ - 6, - -51 - ], - [ - 18, - -8 - ], - [ - -6, - -63 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -53, - 1 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -6, - 53 - ], - [ - -10, - 54 - ] - ], - [ - [ - 14767, - 7697 - ], - [ - -6, - 43 - ], - [ - 12, - 134 - ], - [ - -18, - 85 - ], - [ - -33, - 169 - ] - ], - [ - [ - 14722, - 8128 - ], - [ - 73, - 136 - ], - [ - 19, - 86 - ], - [ - 10, - 11 - ], - [ - 8, - 71 - ], - [ - -11, - 35 - ], - [ - 3, - 90 - ], - [ - 13, - 83 - ], - [ - 0, - 152 - ], - [ - -36, - 39 - ], - [ - -33, - 8 - ], - [ - -15, - 30 - ], - [ - -32, - 25 - ], - [ - -59, - -2 - ], - [ - -4, - 45 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -7, - 85 - ], - [ - 212, - 99 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - 40, - -58 - ], - [ - 19, - 11 - ], - [ - 28, - -30 - ], - [ - 4, - -48 - ], - [ - -15, - -56 - ], - [ - 5, - -84 - ], - [ - 46, - -74 - ], - [ - 21, - 83 - ], - [ - 30, - 25 - ], - [ - -6, - 154 - ], - [ - -29, - 87 - ], - [ - -25, - 39 - ], - [ - -24, - -2 - ], - [ - -20, - 156 - ], - [ - 20, - 91 - ] - ], - [ - [ - 11699, - 12551 - ], - [ - -46, - 82 - ], - [ - -42, - 88 - ], - [ - -46, - 32 - ], - [ - -34, - 35 - ], - [ - -39, - -1 - ], - [ - -34, - -26 - ], - [ - -34, - 10 - ], - [ - -24, - -38 - ] - ], - [ - [ - 11400, - 12733 - ], - [ - -6, - 65 - ], - [ - 19, - 59 - ], - [ - 9, - 113 - ], - [ - -8, - 118 - ], - [ - -8, - 60 - ], - [ - 7, - 60 - ], - [ - -18, - 57 - ], - [ - -37, - 52 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 15, - 40 - ], - [ - 273, - -1 - ], - [ - -13, - 173 - ], - [ - 17, - 62 - ], - [ - 65, - 10 - ], - [ - -2, - 307 - ], - [ - 229, - -6 - ], - [ - 0, - 182 - ] - ], - [ - [ - 14863, - 9121 - ], - [ - -37, - 31 - ], - [ - 21, - 112 - ], - [ - 22, - 41 - ], - [ - -13, - 100 - ], - [ - 14, - 97 - ], - [ - 12, - 32 - ], - [ - -18, - 102 - ], - [ - -33, - 54 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - 68, - -23 - ], - [ - 14, - -33 - ], - [ - 24, - -56 - ], - [ - 20, - -163 - ] - ], - [ - [ - 20595, - 11451 - ], - [ - 54, - 83 - ], - [ - 35, - 94 - ], - [ - 28, - 0 - ], - [ - 36, - -60 - ], - [ - 3, - -52 - ], - [ - 46, - -34 - ], - [ - 58, - -36 - ], - [ - -4, - -47 - ], - [ - -47, - -6 - ], - [ - 12, - -59 - ], - [ - -51, - -40 - ] - ], - [ - [ - 20192, - 11038 - ], - [ - 51, - -41 - ], - [ - 54, - 22 - ], - [ - 14, - 102 - ], - [ - 30, - 22 - ], - [ - 83, - 26 - ], - [ - 50, - 95 - ], - [ - 34, - 76 - ] - ], - [ - [ - 19668, - 11544 - ], - [ - 16, - -12 - ], - [ - 41, - -72 - ], - [ - 29, - -80 - ], - [ - 4, - -81 - ], - [ - -7, - -55 - ], - [ - 6, - -41 - ], - [ - 5, - -71 - ], - [ - 25, - -33 - ], - [ - 27, - -106 - ], - [ - -1, - -41 - ], - [ - -49, - -8 - ], - [ - -66, - 89 - ], - [ - -83, - 95 - ], - [ - -8, - 62 - ], - [ - -40, - 80 - ], - [ - -10, - 99 - ], - [ - -25, - 66 - ], - [ - 8, - 87 - ], - [ - -16, - 51 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - 12, - 21 - ], - [ - 57, - -52 - ], - [ - 6, - -62 - ], - [ - 46, - 14 - ], - [ - 23, - 50 - ] - ], - [ - [ - 14166, - 8695 - ], - [ - 57, - 27 - ], - [ - 45, - -7 - ], - [ - 28, - -27 - ], - [ - 0, - -10 - ] - ], - [ - [ - 13934, - 7826 - ], - [ - 0, - -443 - ], - [ - -62, - -62 - ], - [ - -37, - -8 - ], - [ - -44, - 22 - ], - [ - -31, - 9 - ], - [ - -12, - 51 - ], - [ - -28, - 33 - ], - [ - -33, - -59 - ] - ], - [ - [ - 13687, - 7369 - ], - [ - -52, - 91 - ], - [ - -27, - 87 - ], - [ - -16, - 117 - ], - [ - -17, - 87 - ], - [ - -23, - 185 - ], - [ - -2, - 143 - ], - [ - -9, - 66 - ], - [ - -27, - 49 - ], - [ - -36, - 99 - ], - [ - -36, - 144 - ], - [ - -16, - 75 - ], - [ - -56, - 117 - ], - [ - -5, - 93 - ] - ], - [ - [ - 24104, - 8268 - ], - [ - 57, - -74 - ], - [ - 36, - -55 - ], - [ - -26, - -29 - ], - [ - -39, - 32 - ], - [ - -50, - 54 - ], - [ - -44, - 64 - ], - [ - -47, - 84 - ], - [ - -9, - 41 - ], - [ - 30, - -2 - ], - [ - 39, - -40 - ], - [ - 30, - -41 - ], - [ - 23, - -34 - ] - ], - [ - [ - 13583, - 13540 - ], - [ - 17, - -186 - ], - [ - 26, - -32 - ], - [ - 1, - -38 - ], - [ - 29, - -41 - ], - [ - -15, - -52 - ], - [ - -27, - -243 - ], - [ - -4, - -156 - ], - [ - -89, - -113 - ], - [ - -30, - -158 - ], - [ - 29, - -45 - ], - [ - 0, - -77 - ], - [ - 45, - -3 - ], - [ - -7, - -56 - ] - ], - [ - [ - 13536, - 12295 - ], - [ - -13, - -3 - ], - [ - -47, - 132 - ], - [ - -16, - 4 - ], - [ - -55, - -67 - ], - [ - -54, - 35 - ], - [ - -37, - 7 - ], - [ - -21, - -17 - ], - [ - -40, - 4 - ], - [ - -42, - -51 - ], - [ - -35, - -3 - ], - [ - -84, - 62 - ], - [ - -33, - -29 - ], - [ - -36, - 2 - ], - [ - -26, - 45 - ], - [ - -70, - 45 - ], - [ - -75, - -15 - ], - [ - -18, - -25 - ], - [ - -10, - -69 - ], - [ - -20, - -49 - ], - [ - -5, - -107 - ] - ], - [ - [ - 13140, - 11370 - ], - [ - -72, - -43 - ], - [ - -27, - 6 - ], - [ - -27, - -27 - ], - [ - -55, - 3 - ], - [ - -38, - 75 - ], - [ - -23, - 86 - ], - [ - -49, - 79 - ], - [ - -52, - -1 - ], - [ - -62, - 0 - ] - ], - [ - [ - 6573, - 12127 - ], - [ - -24, - 38 - ], - [ - -33, - 49 - ], - [ - -15, - 40 - ], - [ - -30, - 38 - ], - [ - -35, - 54 - ], - [ - 8, - 19 - ], - [ - 12, - -19 - ], - [ - 5, - 9 - ] - ], - [ - [ - 6751, - 12596 - ], - [ - -6, - -11 - ], - [ - -3, - -27 - ], - [ - 7, - -44 - ], - [ - -16, - -41 - ], - [ - -8, - -48 - ], - [ - -2, - -53 - ], - [ - 4, - -31 - ], - [ - 2, - -54 - ], - [ - -11, - -12 - ], - [ - -6, - -51 - ], - [ - 4, - -32 - ], - [ - -14, - -30 - ], - [ - 3, - -33 - ], - [ - 11, - -19 - ] - ], - [ - [ - 12779, - 16957 - ], - [ - 36, - 33 - ], - [ - 61, - 177 - ], - [ - 95, - 50 - ], - [ - 58, - -4 - ] - ], - [ - [ - 13987, - 19088 - ], - [ - -44, - -5 - ], - [ - -10, - -79 - ], - [ - -131, - 19 - ], - [ - -19, - -67 - ], - [ - -67, - 1 - ], - [ - -46, - -86 - ], - [ - -69, - -133 - ], - [ - -109, - -168 - ], - [ - 26, - -41 - ], - [ - -24, - -48 - ], - [ - -70, - 2 - ], - [ - -45, - -112 - ], - [ - 4, - -160 - ], - [ - 45, - -60 - ], - [ - -23, - -142 - ], - [ - -58, - -82 - ], - [ - -31, - -69 - ] - ], - [ - [ - 13316, - 17858 - ], - [ - -47, - 74 - ], - [ - -137, - -139 - ], - [ - -93, - -28 - ], - [ - -97, - 61 - ], - [ - -24, - 129 - ], - [ - -23, - 277 - ], - [ - 65, - 77 - ], - [ - 184, - 101 - ], - [ - 137, - 124 - ], - [ - 128, - 167 - ], - [ - 167, - 231 - ], - [ - 117, - 91 - ], - [ - 192, - 150 - ], - [ - 153, - 53 - ], - [ - 114, - -7 - ], - [ - 107, - 100 - ], - [ - 127, - -6 - ], - [ - 125, - 24 - ], - [ - 218, - -88 - ], - [ - -90, - -32 - ], - [ - 77, - -75 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - -119, - -48 - ], - [ - -56, - -11 - ] - ], - [ - [ - 14271, - 20137 - ], - [ - -156, - -49 - ], - [ - -123, - 28 - ], - [ - 48, - 31 - ], - [ - -42, - 38 - ], - [ - 145, - 24 - ], - [ - 27, - -45 - ], - [ - 101, - -27 - ] - ], - [ - [ - 13820, - 20359 - ], - [ - 229, - -90 - ], - [ - -175, - -47 - ], - [ - -39, - -88 - ], - [ - -61, - -23 - ], - [ - -33, - -99 - ], - [ - -84, - -5 - ], - [ - -150, - 73 - ], - [ - 63, - 43 - ], - [ - -104, - 35 - ], - [ - -136, - 101 - ], - [ - -54, - 94 - ], - [ - 190, - 43 - ], - [ - 38, - -42 - ], - [ - 99, - 2 - ], - [ - 27, - 41 - ], - [ - 102, - 4 - ], - [ - 88, - -42 - ] - ], - [ - [ - 14321, - 20444 - ], - [ - 137, - -43 - ], - [ - -103, - -64 - ], - [ - -203, - -14 - ], - [ - -205, - 20 - ], - [ - -12, - 33 - ], - [ - -101, - 2 - ], - [ - -76, - 55 - ], - [ - 215, - 33 - ], - [ - 102, - -28 - ], - [ - 70, - 36 - ], - [ - 176, - -30 - ] - ], - [ - [ - 24608, - 5888 - ], - [ - 16, - -49 - ], - [ - 50, - 48 - ], - [ - 20, - -50 - ], - [ - 0, - -51 - ], - [ - -26, - -55 - ], - [ - -45, - -89 - ], - [ - -36, - -48 - ], - [ - 26, - -58 - ], - [ - -54, - -1 - ], - [ - -60, - -46 - ], - [ - -18, - -78 - ], - [ - -40, - -121 - ], - [ - -55, - -54 - ], - [ - -35, - -34 - ], - [ - -64, - 2 - ], - [ - -45, - 40 - ], - [ - -76, - 8 - ], - [ - -11, - 44 - ], - [ - 37, - 89 - ], - [ - 88, - 119 - ], - [ - 45, - 22 - ], - [ - 50, - 46 - ], - [ - 60, - 63 - ], - [ - 41, - 62 - ], - [ - 31, - 89 - ], - [ - 27, - 31 - ], - [ - 10, - 67 - ], - [ - 49, - 55 - ], - [ - 15, - -51 - ] - ], - [ - [ - 24719, - 6460 - ], - [ - 51, - -127 - ], - [ - 1, - 82 - ], - [ - 32, - -33 - ], - [ - 10, - -90 - ], - [ - 56, - -39 - ], - [ - 47, - -10 - ], - [ - 40, - 46 - ], - [ - 36, - -14 - ], - [ - -17, - -107 - ], - [ - -21, - -70 - ], - [ - -54, - 3 - ], - [ - -18, - -37 - ], - [ - 6, - -51 - ], - [ - -10, - -22 - ], - [ - -26, - -65 - ], - [ - -35, - -82 - ], - [ - -54, - -48 - ], - [ - -12, - 31 - ], - [ - -29, - 18 - ], - [ - 40, - 98 - ], - [ - -23, - 66 - ], - [ - -75, - 48 - ], - [ - 2, - 44 - ], - [ - 51, - 42 - ], - [ - 12, - 92 - ], - [ - -4, - 78 - ], - [ - -28, - 80 - ], - [ - 2, - 21 - ], - [ - -33, - 50 - ], - [ - -55, - 106 - ], - [ - -29, - 85 - ], - [ - 26, - 9 - ], - [ - 37, - -66 - ], - [ - 55, - -32 - ], - [ - 19, - -106 - ] - ], - [ - [ - 16456, - 13923 - ], - [ - 20, - 41 - ], - [ - 9, - -11 - ], - [ - -7, - -49 - ], - [ - -9, - -22 - ] - ], - [ - [ - 16479, - 13787 - ], - [ - 31, - -82 - ], - [ - 39, - -43 - ], - [ - 51, - -16 - ], - [ - 41, - -22 - ], - [ - 32, - -68 - ], - [ - 19, - -40 - ], - [ - 25, - -15 - ], - [ - -1, - -27 - ], - [ - -25, - -72 - ], - [ - -11, - -33 - ], - [ - -29, - -39 - ], - [ - -26, - -82 - ], - [ - -32, - 6 - ], - [ - -15, - -28 - ], - [ - -11, - -61 - ], - [ - 9, - -80 - ], - [ - -7, - -15 - ], - [ - -32, - 0 - ], - [ - -43, - -44 - ], - [ - -7, - -59 - ], - [ - -16, - -25 - ], - [ - -43, - 1 - ], - [ - -28, - -30 - ], - [ - 1, - -49 - ], - [ - -34, - -33 - ], - [ - -39, - 11 - ], - [ - -46, - -40 - ], - [ - -32, - -7 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -23, - 84 - ], - [ - -55, - 198 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - 209, - 120 - ], - [ - 47, - 240 - ], - [ - -32, - 84 - ] - ], - [ - [ - 17300, - 13639 - ], - [ - -51, - 31 - ], - [ - -21, - 86 - ], - [ - -54, - 91 - ], - [ - -128, - -22 - ], - [ - -113, - -2 - ], - [ - -99, - -17 - ] - ], - [ - [ - 7119, - 11664 - ], - [ - -24, - 34 - ], - [ - -15, - 65 - ], - [ - 18, - 32 - ], - [ - -18, - 8 - ], - [ - -13, - 40 - ], - [ - -35, - 33 - ], - [ - -30, - -7 - ], - [ - -14, - -42 - ], - [ - -29, - -30 - ], - [ - -15, - -4 - ], - [ - -7, - -25 - ], - [ - 34, - -65 - ], - [ - -19, - -16 - ], - [ - -11, - -17 - ], - [ - -32, - -7 - ], - [ - -12, - 72 - ], - [ - -9, - -20 - ], - [ - -23, - 7 - ], - [ - -14, - 48 - ], - [ - -29, - 8 - ], - [ - -18, - 14 - ], - [ - -30, - 0 - ], - [ - -2, - -26 - ], - [ - -8, - 18 - ] - ], - [ - [ - 6793, - 11945 - ], - [ - 25, - -43 - ], - [ - -1, - -26 - ], - [ - 28, - -5 - ], - [ - 6, - 10 - ], - [ - 20, - -30 - ], - [ - 34, - 9 - ], - [ - 29, - 30 - ], - [ - 43, - 24 - ], - [ - 24, - 36 - ], - [ - 38, - -7 - ], - [ - -3, - -12 - ], - [ - 39, - -4 - ], - [ - 31, - -20 - ], - [ - 23, - -36 - ], - [ - 26, - -34 - ] - ], - [ - [ - 7642, - 8596 - ], - [ - -70, - 69 - ], - [ - -6, - 49 - ], - [ - -138, - 121 - ], - [ - -125, - 131 - ], - [ - -54, - 74 - ], - [ - -29, - 99 - ], - [ - 12, - 34 - ], - [ - -59, - 158 - ], - [ - -69, - 221 - ], - [ - -66, - 239 - ], - [ - -29, - 55 - ], - [ - -21, - 88 - ], - [ - -55, - 78 - ], - [ - -49, - 49 - ], - [ - 22, - 54 - ], - [ - -34, - 114 - ], - [ - 22, - 84 - ], - [ - 56, - 76 - ] - ], - [ - [ - 21357, - 11807 - ], - [ - 7, - -80 - ], - [ - 4, - -67 - ], - [ - -24, - -110 - ], - [ - -25, - 122 - ], - [ - -33, - -61 - ], - [ - 23, - -88 - ], - [ - -20, - -56 - ], - [ - -82, - 69 - ], - [ - -20, - 87 - ], - [ - 21, - 57 - ], - [ - -44, - 57 - ], - [ - -22, - -50 - ], - [ - -33, - 5 - ], - [ - -51, - -67 - ], - [ - -12, - 35 - ], - [ - 28, - 101 - ], - [ - 44, - 34 - ], - [ - 38, - 45 - ], - [ - 24, - -54 - ], - [ - 53, - 33 - ], - [ - 12, - 53 - ], - [ - 49, - 3 - ], - [ - -4, - 93 - ], - [ - 56, - -57 - ], - [ - 6, - -60 - ], - [ - 5, - -44 - ] - ], - [ - [ - 21190, - 12030 - ], - [ - -25, - -39 - ], - [ - -22, - -76 - ], - [ - -22, - -35 - ], - [ - -43, - 82 - ], - [ - 15, - 33 - ], - [ - 17, - 33 - ], - [ - 8, - 75 - ], - [ - 38, - 7 - ], - [ - -11, - -81 - ], - [ - 52, - 116 - ], - [ - -7, - -115 - ] - ], - [ - [ - 20808, - 11915 - ], - [ - -92, - -114 - ], - [ - 34, - 84 - ], - [ - 50, - 74 - ], - [ - 42, - 83 - ], - [ - 36, - 119 - ], - [ - 13, - -98 - ], - [ - -46, - -66 - ], - [ - -37, - -82 - ] - ], - [ - [ - 21044, - 12224 - ], - [ - 42, - -37 - ], - [ - 44, - 0 - ], - [ - -1, - -50 - ], - [ - -33, - -51 - ], - [ - -44, - -36 - ], - [ - -2, - 56 - ], - [ - 5, - 61 - ], - [ - -11, - 57 - ] - ], - [ - [ - 21296, - 12256 - ], - [ - 20, - -134 - ], - [ - -54, - 32 - ], - [ - 1, - -40 - ], - [ - 17, - -74 - ], - [ - -33, - -27 - ], - [ - -3, - 84 - ], - [ - -21, - 7 - ], - [ - -11, - 72 - ], - [ - 41, - -9 - ], - [ - 0, - 45 - ], - [ - -43, - 92 - ], - [ - 67, - -3 - ], - [ - 19, - -45 - ] - ], - [ - [ - 21019, - 12365 - ], - [ - -19, - -104 - ], - [ - -29, - 60 - ], - [ - -36, - 92 - ], - [ - 60, - -5 - ], - [ - 24, - -43 - ] - ], - [ - [ - 21005, - 13017 - ], - [ - 43, - -34 - ], - [ - 21, - 31 - ], - [ - 6, - -30 - ], - [ - -11, - -50 - ], - [ - 24, - -86 - ], - [ - -18, - -100 - ], - [ - -42, - -40 - ], - [ - -11, - -96 - ], - [ - 16, - -96 - ], - [ - 37, - -13 - ], - [ - 31, - 14 - ], - [ - 87, - -66 - ], - [ - -7, - -66 - ], - [ - 23, - -29 - ], - [ - -7, - -55 - ], - [ - -55, - 59 - ], - [ - -25, - 63 - ], - [ - -18, - -44 - ], - [ - -45, - 72 - ], - [ - -63, - -18 - ], - [ - -35, - 27 - ], - [ - 4, - 49 - ], - [ - 22, - 31 - ], - [ - -21, - 28 - ], - [ - -9, - -44 - ], - [ - -35, - 69 - ], - [ - -10, - 52 - ], - [ - -3, - 115 - ], - [ - 28, - -39 - ], - [ - 8, - 188 - ], - [ - 22, - 108 - ], - [ - 43, - 0 - ] - ], - [ - [ - 22376, - 10485 - ], - [ - 121, - -82 - ], - [ - 129, - -69 - ], - [ - 48, - -62 - ], - [ - 39, - -60 - ], - [ - 11, - -71 - ], - [ - 116, - -74 - ], - [ - 17, - -63 - ], - [ - -64, - -13 - ], - [ - 15, - -80 - ], - [ - 62, - -79 - ], - [ - 46, - -127 - ], - [ - 39, - 4 - ], - [ - -2, - -53 - ], - [ - 53, - -21 - ], - [ - -20, - -22 - ], - [ - 74, - -51 - ], - [ - -8, - -34 - ], - [ - -46, - -9 - ], - [ - -17, - 31 - ], - [ - -60, - 14 - ], - [ - -71, - 18 - ], - [ - -54, - 76 - ], - [ - -39, - 66 - ], - [ - -37, - 105 - ], - [ - -91, - 53 - ], - [ - -59, - -34 - ], - [ - -42, - -40 - ], - [ - 9, - -88 - ], - [ - -55, - -42 - ], - [ - -39, - 20 - ], - [ - -72, - 5 - ] - ], - [ - [ - 23414, - 9979 - ], - [ - -20, - -12 - ], - [ - -30, - 46 - ], - [ - -31, - 76 - ], - [ - -15, - 92 - ], - [ - 10, - 11 - ], - [ - 8, - -35 - ], - [ - 21, - -28 - ], - [ - 33, - -76 - ], - [ - 33, - -40 - ], - [ - -9, - -34 - ] - ], - [ - [ - 23142, - 10140 - ], - [ - -37, - -10 - ], - [ - -11, - -34 - ], - [ - -38, - -29 - ], - [ - -35, - -28 - ], - [ - -37, - 0 - ], - [ - -58, - 35 - ], - [ - -39, - 34 - ], - [ - 5, - 37 - ], - [ - 63, - -18 - ], - [ - 38, - 10 - ], - [ - 10, - 57 - ], - [ - 10, - 3 - ], - [ - 7, - -64 - ], - [ - 40, - 10 - ], - [ - 20, - 41 - ], - [ - 39, - 42 - ], - [ - -8, - 71 - ], - [ - 42, - 2 - ], - [ - 14, - -19 - ], - [ - -2, - -67 - ], - [ - -23, - -73 - ] - ], - [ - [ - 23223, - 10257 - ], - [ - -22, - -32 - ], - [ - -13, - 71 - ], - [ - -17, - 47 - ], - [ - -31, - 39 - ], - [ - -40, - 51 - ], - [ - -50, - 35 - ], - [ - 19, - 29 - ], - [ - 38, - -33 - ], - [ - 24, - -27 - ], - [ - 29, - -29 - ], - [ - 28, - -50 - ], - [ - 26, - -38 - ], - [ - 9, - -63 - ] - ], - [ - [ - 14188, - 16985 - ], - [ - 35, - -105 - ], - [ - -8, - -33 - ], - [ - -34, - -14 - ], - [ - -64, - -100 - ], - [ - 18, - -54 - ], - [ - -15, - 7 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -66, - 46 - ], - [ - -50, - -17 - ], - [ - -33, - 12 - ], - [ - -42, - -25 - ], - [ - -35, - 42 - ], - [ - -28, - -16 - ], - [ - -4, - 7 - ] - ], - [ - [ - 13532, - 17246 - ], - [ - 47, - 36 - ], - [ - 109, - 55 - ], - [ - 88, - 41 - ], - [ - 70, - -21 - ], - [ - 5, - -29 - ], - [ - 67, - -1 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 86, - -14 - ], - [ - 128, - 2 - ] - ], - [ - [ - 7927, - 13018 - ], - [ - 36, - -10 - ], - [ - 12, - -24 - ], - [ - -18, - -30 - ], - [ - -52, - 0 - ], - [ - -41, - -4 - ], - [ - -4, - 52 - ], - [ - 10, - 17 - ], - [ - 57, - -1 - ] - ], - [ - [ - 21654, - 15883 - ], - [ - 10, - -21 - ] - ], - [ - [ - 21664, - 15862 - ], - [ - -27, - 7 - ], - [ - -30, - -40 - ], - [ - -21, - -41 - ], - [ - 3, - -86 - ], - [ - -36, - -27 - ], - [ - -12, - -21 - ], - [ - -27, - -35 - ], - [ - -46, - -20 - ], - [ - -30, - -32 - ], - [ - -3, - -52 - ], - [ - -8, - -13 - ], - [ - 28, - -20 - ], - [ - 40, - -53 - ] - ], - [ - [ - 21343, - 15326 - ], - [ - -34, - 23 - ], - [ - -8, - -23 - ], - [ - -21, - -10 - ], - [ - -2, - 23 - ], - [ - -18, - 11 - ], - [ - -19, - 19 - ], - [ - 19, - 53 - ], - [ - 17, - 14 - ], - [ - -7, - 22 - ], - [ - 18, - 65 - ], - [ - -5, - 19 - ], - [ - -40, - 13 - ], - [ - -33, - 32 - ] - ], - [ - [ - 12028, - 15248 - ], - [ - -28, - -31 - ], - [ - -37, - 17 - ], - [ - -36, - -14 - ], - [ - 11, - 94 - ], - [ - -7, - 74 - ], - [ - -31, - 11 - ], - [ - -17, - 45 - ], - [ - 6, - 79 - ], - [ - 28, - 44 - ], - [ - 5, - 48 - ], - [ - 14, - 72 - ], - [ - -1, - 51 - ], - [ - -14, - 43 - ], - [ - -3, - 41 - ] - ], - [ - [ - 16089, - 13767 - ], - [ - -4, - 87 - ], - [ - 19, - 63 - ], - [ - 19, - 13 - ], - [ - 21, - -37 - ], - [ - 1, - -71 - ], - [ - -15, - -70 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - -20, - -9 - ], - [ - -21, - 24 - ] - ], - [ - [ - 14127, - 16104 - ], - [ - -13, - 21 - ], - [ - 16, - 20 - ], - [ - -17, - 15 - ], - [ - -22, - -27 - ], - [ - -40, - 35 - ], - [ - -6, - 50 - ], - [ - -42, - 28 - ], - [ - -8, - 38 - ], - [ - -38, - 47 - ] - ], - [ - [ - 14131, - 16542 - ], - [ - 30, - 25 - ], - [ - 43, - -13 - ], - [ - 45, - 0 - ], - [ - 32, - -30 - ], - [ - 24, - 19 - ], - [ - 51, - 11 - ], - [ - 18, - 28 - ], - [ - 29, - 0 - ] - ], - [ - [ - 14516, - 16254 - ], - [ - 31, - -22 - ], - [ - 32, - 20 - ], - [ - 32, - -21 - ] - ], - [ - [ - 14611, - 16231 - ], - [ - 2, - -31 - ], - [ - -34, - -26 - ], - [ - -21, - 11 - ], - [ - -20, - -144 - ] - ], - [ - [ - 15333, - 16008 - ], - [ - -89, - 101 - ], - [ - -80, - 46 - ], - [ - -60, - 70 - ], - [ - 51, - 19 - ], - [ - 58, - 101 - ], - [ - -39, - 47 - ], - [ - 102, - 49 - ], - [ - -1, - 26 - ], - [ - -63, - -19 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - 2, - 53 - ], - [ - 36, - 34 - ], - [ - 68, - 9 - ], - [ - 11, - 40 - ], - [ - -16, - 66 - ], - [ - 28, - 63 - ], - [ - 0, - 35 - ], - [ - -103, - 39 - ], - [ - -41, - -1 - ], - [ - -43, - 56 - ], - [ - -53, - -19 - ], - [ - -89, - 42 - ], - [ - 2, - 23 - ], - [ - -25, - 53 - ], - [ - -56, - 5 - ], - [ - -6, - 38 - ], - [ - 18, - 24 - ], - [ - -45, - 68 - ], - [ - -72, - -12 - ], - [ - -21, - 6 - ], - [ - -18, - -27 - ], - [ - -26, - 5 - ] - ], - [ - [ - 14498, - 17932 - ], - [ - 79, - 67 - ], - [ - -73, - 57 - ] - ], - [ - [ - 14716, - 19142 - ], - [ - 71, - 42 - ], - [ - 115, - -73 - ], - [ - 191, - -28 - ], - [ - 263, - -136 - ], - [ - 54, - -57 - ], - [ - 4, - -80 - ], - [ - -77, - -63 - ], - [ - -114, - -32 - ], - [ - -311, - 91 - ], - [ - -51, - -15 - ], - [ - 113, - -88 - ], - [ - 5, - -56 - ], - [ - 4, - -122 - ], - [ - 90, - -37 - ], - [ - 55, - -31 - ], - [ - 9, - 58 - ], - [ - -42, - 52 - ], - [ - 44, - 45 - ], - [ - 168, - -74 - ], - [ - 59, - 29 - ], - [ - -47, - 88 - ], - [ - 163, - 117 - ], - [ - 64, - -7 - ], - [ - 65, - -42 - ], - [ - 41, - 83 - ], - [ - -58, - 71 - ], - [ - 34, - 72 - ], - [ - -51, - 75 - ], - [ - 195, - -39 - ], - [ - 39, - -67 - ], - [ - -88, - -15 - ], - [ - 1, - -67 - ], - [ - 54, - -41 - ], - [ - 108, - 26 - ], - [ - 17, - 77 - ], - [ - 146, - 57 - ], - [ - 243, - 103 - ], - [ - 53, - -6 - ], - [ - -69, - -73 - ], - [ - 86, - -12 - ], - [ - 50, - 41 - ], - [ - 131, - 3 - ], - [ - 103, - 50 - ], - [ - 80, - -73 - ], - [ - 79, - 80 - ], - [ - -73, - 69 - ], - [ - 36, - 40 - ], - [ - 206, - -36 - ], - [ - 97, - -38 - ], - [ - 252, - -137 - ], - [ - 47, - 63 - ], - [ - -71, - 63 - ], - [ - -2, - 26 - ], - [ - -84, - 12 - ], - [ - 23, - 56 - ], - [ - -37, - 94 - ], - [ - -2, - 38 - ], - [ - 128, - 109 - ], - [ - 46, - 109 - ], - [ - 52, - 24 - ], - [ - 184, - -32 - ], - [ - 15, - -67 - ], - [ - -66, - -97 - ], - [ - 43, - -38 - ], - [ - 23, - -84 - ], - [ - -16, - -164 - ], - [ - 77, - -74 - ], - [ - -30, - -80 - ], - [ - -137, - -170 - ], - [ - 80, - -18 - ], - [ - 28, - 43 - ], - [ - 76, - 31 - ], - [ - 19, - 59 - ], - [ - 60, - 57 - ], - [ - -40, - 69 - ], - [ - 32, - 79 - ], - [ - -76, - 10 - ], - [ - -17, - 66 - ], - [ - 56, - 121 - ], - [ - -91, - 98 - ], - [ - 125, - 80 - ], - [ - -16, - 86 - ], - [ - 35, - 3 - ], - [ - 36, - -67 - ], - [ - -27, - -116 - ], - [ - 74, - -22 - ], - [ - -31, - 87 - ], - [ - 116, - 47 - ], - [ - 145, - 6 - ], - [ - 129, - -68 - ], - [ - -62, - 100 - ], - [ - -7, - 128 - ], - [ - 121, - 24 - ], - [ - 168, - -5 - ], - [ - 151, - 15 - ], - [ - -57, - 63 - ], - [ - 81, - 79 - ], - [ - 80, - 3 - ], - [ - 135, - 60 - ], - [ - 184, - 16 - ], - [ - 24, - 32 - ], - [ - 183, - 12 - ], - [ - 57, - -27 - ], - [ - 156, - 63 - ], - [ - 128, - -2 - ], - [ - 20, - 52 - ], - [ - 66, - 51 - ], - [ - 165, - 50 - ], - [ - 119, - -39 - ], - [ - -95, - -30 - ], - [ - 158, - -18 - ], - [ - 19, - -60 - ], - [ - 64, - 30 - ], - [ - 204, - -2 - ], - [ - 157, - -59 - ], - [ - 56, - -44 - ], - [ - -18, - -63 - ], - [ - -77, - -35 - ], - [ - -183, - -67 - ], - [ - -52, - -36 - ], - [ - 86, - -16 - ], - [ - 103, - -31 - ], - [ - 63, - 23 - ], - [ - 35, - -77 - ], - [ - 31, - 31 - ], - [ - 112, - 19 - ], - [ - 223, - -20 - ], - [ - 17, - -56 - ], - [ - 292, - -18 - ], - [ - 4, - 92 - ], - [ - 148, - -21 - ], - [ - 111, - 1 - ], - [ - 112, - -63 - ], - [ - 32, - -77 - ], - [ - -41, - -50 - ], - [ - 88, - -95 - ], - [ - 109, - -49 - ], - [ - 68, - 126 - ], - [ - 111, - -54 - ], - [ - 119, - 33 - ], - [ - 135, - -37 - ], - [ - 52, - 33 - ], - [ - 114, - -16 - ], - [ - -51, - 111 - ], - [ - 92, - 52 - ], - [ - 630, - -78 - ], - [ - 59, - -71 - ], - [ - 183, - -92 - ], - [ - 281, - 23 - ], - [ - 139, - -20 - ], - [ - 58, - -50 - ], - [ - -8, - -87 - ], - [ - 85, - -34 - ], - [ - 94, - 24 - ], - [ - 123, - 3 - ], - [ - 132, - -23 - ], - [ - 132, - 13 - ], - [ - 121, - -107 - ], - [ - 87, - 39 - ], - [ - -57, - 76 - ], - [ - 32, - 54 - ], - [ - 222, - -34 - ], - [ - 145, - 7 - ], - [ - 200, - -57 - ], - [ - 98, - -52 - ], - [ - 0, - -478 - ], - [ - -1, - -1 - ], - [ - -89, - -53 - ], - [ - -90, - 9 - ], - [ - 62, - -64 - ], - [ - 42, - -99 - ], - [ - 32, - -32 - ], - [ - 8, - -49 - ], - [ - -18, - -32 - ], - [ - -130, - 26 - ], - [ - -195, - -90 - ], - [ - -62, - -14 - ], - [ - -106, - -85 - ], - [ - -101, - -73 - ], - [ - -26, - -55 - ], - [ - -100, - 83 - ], - [ - -181, - -94 - ], - [ - -32, - 45 - ], - [ - -67, - -52 - ], - [ - -93, - 17 - ], - [ - -23, - -79 - ], - [ - -84, - -116 - ], - [ - 3, - -49 - ], - [ - 79, - -27 - ], - [ - -9, - -174 - ], - [ - -65, - -5 - ], - [ - -30, - -100 - ], - [ - 29, - -52 - ], - [ - -121, - -61 - ], - [ - -25, - -137 - ], - [ - -104, - -29 - ], - [ - -20, - -122 - ], - [ - -101, - -112 - ], - [ - -26, - 83 - ], - [ - -30, - 175 - ], - [ - -38, - 266 - ], - [ - 33, - 167 - ], - [ - 59, - 71 - ], - [ - 3, - 56 - ], - [ - 109, - 27 - ], - [ - 124, - 151 - ], - [ - 120, - 123 - ], - [ - 126, - 96 - ], - [ - 56, - 169 - ], - [ - -85, - -10 - ], - [ - -42, - -99 - ], - [ - -177, - -131 - ], - [ - -57, - 147 - ], - [ - -180, - -41 - ], - [ - -174, - -201 - ], - [ - 57, - -73 - ], - [ - -155, - -32 - ], - [ - -108, - -12 - ], - [ - 5, - 87 - ], - [ - -108, - 18 - ], - [ - -87, - -59 - ], - [ - -213, - 21 - ], - [ - -229, - -36 - ], - [ - -226, - -234 - ], - [ - -267, - -283 - ], - [ - 110, - -15 - ], - [ - 34, - -75 - ], - [ - 68, - -27 - ], - [ - 44, - 60 - ], - [ - 77, - -8 - ], - [ - 100, - -132 - ], - [ - 3, - -102 - ], - [ - -55, - -120 - ], - [ - -6, - -143 - ], - [ - -31, - -192 - ], - [ - -105, - -173 - ], - [ - -23, - -83 - ], - [ - -95, - -140 - ], - [ - -94, - -138 - ], - [ - -45, - -71 - ], - [ - -93, - -71 - ], - [ - -44, - -1 - ], - [ - -44, - 58 - ], - [ - -93, - -88 - ], - [ - -11, - -40 - ] - ], - [ - [ - 15970, - 16364 - ], - [ - -32, - -71 - ], - [ - -67, - -20 - ], - [ - -69, - -124 - ], - [ - 63, - -114 - ], - [ - -7, - -81 - ], - [ - 76, - -141 - ] - ], - [ - [ - 13918, - 17327 - ], - [ - 16, - 52 - ], - [ - 96, - 39 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - 19, - 30 - ], - [ - 49, - -26 - ], - [ - 23, - -4 - ], - [ - 9, - -24 - ], - [ - 10, - -4 - ] - ], - [ - [ - 14988, - 16284 - ], - [ - 1, - -10 - ], - [ - 34, - -29 - ], - [ - 71, - 7 - ], - [ - -14, - -43 - ], - [ - -76, - -20 - ], - [ - -95, - -70 - ], - [ - -38, - 25 - ], - [ - 15, - 56 - ], - [ - -76, - 35 - ], - [ - 12, - 23 - ], - [ - 67, - 40 - ], - [ - -11, - 14 - ] - ], - [ - [ - 22561, - 16885 - ], - [ - 70, - -212 - ], - [ - -103, - 39 - ], - [ - -43, - -173 - ], - [ - 68, - -123 - ], - [ - -2, - -84 - ], - [ - -53, - 73 - ], - [ - -46, - -93 - ], - [ - -12, - 100 - ], - [ - 7, - 117 - ], - [ - -8, - 130 - ], - [ - 17, - 90 - ], - [ - 3, - 161 - ], - [ - -41, - 118 - ], - [ - 6, - 164 - ], - [ - 64, - 55 - ], - [ - -27, - 56 - ], - [ - 31, - 16 - ], - [ - 18, - -79 - ], - [ - 24, - -116 - ], - [ - -2, - -118 - ], - [ - 29, - -121 - ] - ], - [ - [ - 348, - 18785 - ], - [ - 47, - -30 - ], - [ - -17, - 88 - ], - [ - 190, - -18 - ], - [ - 136, - -113 - ], - [ - -69, - -52 - ], - [ - -114, - -12 - ], - [ - -2, - -118 - ], - [ - -28, - -24 - ], - [ - -65, - 3 - ], - [ - -53, - 42 - ], - [ - -93, - 35 - ], - [ - -16, - 52 - ], - [ - -70, - 20 - ], - [ - -80, - -16 - ], - [ - -38, - 42 - ], - [ - 16, - 45 - ], - [ - -84, - -29 - ], - [ - 32, - -56 - ], - [ - -40, - -51 - ], - [ - 0, - 478 - ], - [ - 171, - -92 - ], - [ - 183, - -119 - ], - [ - -6, - -75 - ] - ], - [ - [ - 25095, - 19295 - ], - [ - -76, - -6 - ], - [ - -13, - 38 - ], - [ - 89, - 50 - ], - [ - 0, - -82 - ] - ], - [ - [ - 91, - 19302 - ], - [ - -91, - -7 - ], - [ - 0, - 82 - ], - [ - 9, - 5 - ], - [ - 59, - 0 - ], - [ - 101, - -35 - ], - [ - -6, - -16 - ], - [ - -72, - -29 - ] - ], - [ - [ - 22558, - 19580 - ], - [ - -106, - 0 - ], - [ - -143, - 13 - ], - [ - -12, - 6 - ], - [ - 66, - 48 - ], - [ - 87, - 11 - ], - [ - 99, - -46 - ], - [ - 9, - -32 - ] - ], - [ - [ - 23055, - 19805 - ], - [ - -81, - -47 - ], - [ - -111, - 10 - ], - [ - -130, - 48 - ], - [ - 17, - 38 - ], - [ - 130, - -18 - ], - [ - 175, - -31 - ] - ], - [ - [ - 22661, - 19862 - ], - [ - -55, - -89 - ], - [ - -257, - 4 - ], - [ - -115, - -29 - ], - [ - -138, - 78 - ], - [ - 37, - 83 - ], - [ - 92, - 22 - ], - [ - 184, - -5 - ], - [ - 252, - -64 - ] - ], - [ - [ - 16558, - 19281 - ], - [ - -41, - -10 - ], - [ - -228, - 16 - ], - [ - -18, - 53 - ], - [ - -126, - 32 - ], - [ - -11, - 65 - ], - [ - 72, - 25 - ], - [ - -3, - 66 - ], - [ - 139, - 102 - ], - [ - -65, - 15 - ], - [ - 167, - 105 - ], - [ - -18, - 55 - ], - [ - 155, - 63 - ], - [ - 231, - 77 - ], - [ - 232, - 22 - ], - [ - 119, - 45 - ], - [ - 136, - 16 - ], - [ - 48, - -48 - ], - [ - -47, - -37 - ], - [ - -247, - -60 - ], - [ - -213, - -57 - ], - [ - -216, - -114 - ], - [ - -104, - -117 - ], - [ - -109, - -116 - ], - [ - 14, - -99 - ], - [ - 133, - -99 - ] - ], - [ - [ - 19872, - 20192 - ], - [ - -393, - -47 - ], - [ - 128, - 158 - ], - [ - 57, - 13 - ], - [ - 52, - -8 - ], - [ - 177, - -68 - ], - [ - -21, - -48 - ] - ], - [ - [ - 16112, - 20460 - ], - [ - -93, - -15 - ], - [ - -63, - -10 - ], - [ - -10, - -19 - ], - [ - -81, - -20 - ], - [ - -76, - 28 - ], - [ - 40, - 38 - ], - [ - -155, - 3 - ], - [ - 136, - 22 - ], - [ - 106, - 2 - ], - [ - 14, - -33 - ], - [ - 40, - 29 - ], - [ - 66, - 20 - ], - [ - 103, - -26 - ], - [ - -27, - -19 - ] - ], - [ - [ - 19514, - 20260 - ], - [ - -152, - -15 - ], - [ - -194, - 35 - ], - [ - -116, - 46 - ], - [ - -53, - 86 - ], - [ - -95, - 24 - ], - [ - 181, - 82 - ], - [ - 150, - 27 - ], - [ - 136, - -61 - ], - [ - 160, - -116 - ], - [ - -17, - -108 - ] - ], - [ - [ - 14609, - 10636 - ], - [ - 17, - -12 - ], - [ - 42, - 37 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 28, - -68 - ], - [ - -4, - -70 - ], - [ - -21, - -15 - ] - ], - [ - [ - 11358, - 13317 - ], - [ - 3, - 50 - ] - ], - [ - [ - 16172, - 13077 - ], - [ - -201, - -46 - ], - [ - -65, - -54 - ], - [ - -50, - -126 - ], - [ - -32, - -20 - ], - [ - -18, - 40 - ], - [ - -26, - -6 - ], - [ - -68, - 12 - ], - [ - -13, - 12 - ], - [ - -80, - -3 - ], - [ - -19, - -11 - ], - [ - -28, - 31 - ], - [ - -19, - -59 - ], - [ - 7, - -50 - ], - [ - -30, - -39 - ] - ], - [ - [ - 15530, - 12758 - ], - [ - -9, - 52 - ], - [ - -21, - 36 - ], - [ - -6, - 48 - ], - [ - -36, - 43 - ], - [ - -37, - 100 - ], - [ - -20, - 98 - ], - [ - -48, - 83 - ], - [ - -31, - 19 - ], - [ - -46, - 115 - ], - [ - -8, - 83 - ], - [ - 3, - 71 - ], - [ - -40, - 133 - ], - [ - -33, - 47 - ], - [ - -38, - 25 - ], - [ - -22, - 68 - ], - [ - 3, - 28 - ], - [ - -19, - 62 - ], - [ - -20, - 27 - ], - [ - -28, - 89 - ], - [ - -42, - 97 - ], - [ - -36, - 82 - ], - [ - -34, - -1 - ], - [ - 10, - 66 - ], - [ - 4, - 42 - ], - [ - 8, - 48 - ] - ], - [ - [ - 15923, - 14223 - ], - [ - 27, - -104 - ], - [ - 34, - -27 - ], - [ - 12, - -42 - ], - [ - 48, - -51 - ], - [ - 4, - -49 - ], - [ - -7, - -40 - ], - [ - 9, - -41 - ], - [ - 20, - -33 - ], - [ - 9, - -40 - ], - [ - 10, - -29 - ] - ], - [ - [ - 16130, - 13752 - ], - [ - 13, - -46 - ] - ], - [ - [ - 14141, - 12134 - ], - [ - 1, - 29 - ], - [ - -25, - 35 - ], - [ - -1, - 70 - ], - [ - -15, - 46 - ], - [ - -24, - -7 - ], - [ - 7, - 44 - ], - [ - 18, - 50 - ], - [ - -8, - 50 - ], - [ - 23, - 37 - ], - [ - -15, - 28 - ], - [ - 19, - 74 - ], - [ - 32, - 88 - ], - [ - 60, - -8 - ], - [ - -4, - 476 - ] - ], - [ - [ - 15117, - 13437 - ], - [ - 23, - -118 - ], - [ - -15, - -22 - ], - [ - 10, - -123 - ], - [ - 25, - -144 - ], - [ - 27, - -29 - ], - [ - 38, - -45 - ] - ], - [ - [ - 14916, - 11839 - ], - [ - -1, - 94 - ], - [ - -10, - 2 - ], - [ - 2, - 60 - ], - [ - -9, - 41 - ], - [ - -36, - 47 - ], - [ - -8, - 87 - ], - [ - 8, - 88 - ], - [ - -32, - 9 - ], - [ - -5, - -27 - ], - [ - -42, - -6 - ], - [ - 17, - -35 - ], - [ - 6, - -72 - ], - [ - -38, - -66 - ], - [ - -35, - -87 - ], - [ - -36, - -12 - ], - [ - -58, - 70 - ], - [ - -27, - -25 - ], - [ - -7, - -35 - ], - [ - -36, - -23 - ], - [ - -2, - -24 - ], - [ - -70, - 0 - ], - [ - -9, - 24 - ], - [ - -51, - 5 - ], - [ - -25, - -21 - ], - [ - -19, - 10 - ], - [ - -36, - 70 - ], - [ - -12, - 33 - ], - [ - -50, - -16 - ], - [ - -19, - -56 - ], - [ - -18, - -107 - ], - [ - -24, - -23 - ], - [ - -21, - -13 - ], - [ - 47, - -47 - ] - ], - [ - [ - 14918, - 11307 - ], - [ - -43, - -55 - ], - [ - -49, - 0 - ], - [ - -56, - -28 - ], - [ - -44, - 27 - ], - [ - -29, - -33 - ] - ], - [ - [ - 11385, - 12283 - ], - [ - -11, - 92 - ] - ], - [ - [ - 11382, - 12428 - ], - [ - -28, - 94 - ], - [ - -35, - 42 - ], - [ - 31, - 23 - ], - [ - 33, - 84 - ], - [ - 17, - 62 - ] - ], - [ - [ - 23849, - 9540 - ], - [ - 19, - -42 - ], - [ - -49, - 1 - ], - [ - -26, - 74 - ], - [ - 41, - -29 - ], - [ - 15, - -4 - ] - ], - [ - [ - 23760, - 9613 - ], - [ - -27, - -3 - ], - [ - -43, - 12 - ], - [ - -14, - 19 - ], - [ - 4, - 47 - ], - [ - 46, - -19 - ], - [ - 23, - -25 - ], - [ - 11, - -31 - ] - ], - [ - [ - 23818, - 9645 - ], - [ - -11, - -22 - ], - [ - -51, - 104 - ], - [ - -15, - 72 - ], - [ - 24, - 0 - ], - [ - 25, - -96 - ], - [ - 28, - -58 - ] - ], - [ - [ - 23692, - 9797 - ], - [ - 3, - -24 - ], - [ - -55, - 51 - ], - [ - -38, - 43 - ], - [ - -26, - 40 - ], - [ - 11, - 12 - ], - [ - 32, - -29 - ], - [ - 57, - -55 - ], - [ - 16, - -38 - ] - ], - [ - [ - 23529, - 9916 - ], - [ - -14, - -7 - ], - [ - -30, - 27 - ], - [ - -29, - 49 - ], - [ - 4, - 20 - ], - [ - 41, - -50 - ], - [ - 28, - -39 - ] - ], - [ - [ - 11750, - 11611 - ], - [ - -19, - 9 - ], - [ - -50, - 49 - ], - [ - -36, - 64 - ], - [ - -12, - 44 - ], - [ - -9, - 88 - ] - ], - [ - [ - 6428, - 12403 - ], - [ - -8, - -28 - ], - [ - -41, - 1 - ], - [ - -25, - 12 - ], - [ - -28, - 24 - ], - [ - -39, - 7 - ], - [ - -20, - 26 - ] - ], - [ - [ - 15555, - 12172 - ], - [ - 23, - -22 - ], - [ - 13, - -49 - ], - [ - 32, - -51 - ], - [ - 34, - 0 - ], - [ - 66, - 31 - ], - [ - 76, - 14 - ], - [ - 61, - 37 - ], - [ - 35, - 8 - ], - [ - 25, - 22 - ], - [ - 40, - 4 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - -1, - -2 - ], - [ - 0, - -49 - ], - [ - 0, - -121 - ], - [ - 0, - -63 - ], - [ - -32, - -74 - ], - [ - -48, - -100 - ] - ], - [ - [ - 15960, - 12166 - ], - [ - 22, - 2 - ], - [ - 32, - 18 - ], - [ - 37, - 12 - ], - [ - 33, - 41 - ], - [ - 26, - 1 - ], - [ - 2, - -33 - ], - [ - -6, - -70 - ], - [ - 0, - -63 - ], - [ - -15, - -44 - ], - [ - -20, - -129 - ], - [ - -33, - -134 - ], - [ - -43, - -153 - ], - [ - -60, - -176 - ], - [ - -60, - -135 - ], - [ - -82, - -163 - ], - [ - -69, - -97 - ], - [ - -105, - -120 - ], - [ - -65, - -91 - ], - [ - -76, - -145 - ], - [ - -16, - -63 - ], - [ - -16, - -29 - ] - ], - [ - [ - 8564, - 11514 - ], - [ - 83, - -24 - ], - [ - 8, - 21 - ], - [ - 56, - 9 - ], - [ - 75, - -32 - ] - ], - [ - [ - 14120, - 16686 - ], - [ - -19, - -31 - ], - [ - -14, - -49 - ] - ], - [ - [ - 13504, - 16256 - ], - [ - 15, - 11 - ] - ], - [ - [ - 14214, - 18716 - ], - [ - -120, - -34 - ], - [ - -68, - -84 - ], - [ - 11, - -73 - ], - [ - -111, - -97 - ], - [ - -134, - -103 - ], - [ - -51, - -169 - ], - [ - 49, - -84 - ], - [ - 67, - -67 - ], - [ - -64, - -135 - ], - [ - -72, - -28 - ], - [ - -27, - -202 - ], - [ - -40, - -112 - ], - [ - -84, - 12 - ], - [ - -40, - -96 - ], - [ - -80, - -5 - ], - [ - -22, - 113 - ], - [ - -59, - 136 - ], - [ - -53, - 170 - ] - ], - [ - [ - 14783, - 7590 - ], - [ - -14, - -53 - ], - [ - -41, - -13 - ], - [ - -41, - 65 - ], - [ - -1, - 41 - ], - [ - 19, - 45 - ], - [ - 7, - 35 - ], - [ - 20, - 9 - ], - [ - 35, - -22 - ] - ], - [ - [ - 15057, - 14954 - ], - [ - -7, - 91 - ], - [ - 17, - 50 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - 19, - 26 - ], - [ - 19, - 26 - ], - [ - 4, - 67 - ], - [ - 22, - -23 - ], - [ - 77, - 33 - ], - [ - 37, - -22 - ], - [ - 58, - 0 - ], - [ - 80, - 45 - ], - [ - 37, - -2 - ], - [ - 80, - 19 - ] - ], - [ - [ - 12678, - 11534 - ], - [ - -57, - -26 - ] - ], - [ - [ - 19699, - 12259 - ], - [ - -63, - 55 - ], - [ - -60, - -2 - ], - [ - 11, - 94 - ], - [ - -62, - 0 - ], - [ - -5, - -132 - ], - [ - -38, - -176 - ], - [ - -23, - -106 - ], - [ - 5, - -86 - ], - [ - 46, - -4 - ], - [ - 28, - -110 - ], - [ - 12, - -103 - ], - [ - 39, - -69 - ], - [ - 42, - -14 - ], - [ - 37, - -62 - ] - ], - [ - [ - 19524, - 11573 - ], - [ - -27, - 46 - ], - [ - -12, - 59 - ], - [ - -37, - 68 - ], - [ - -34, - 57 - ], - [ - -11, - -71 - ], - [ - -14, - 67 - ], - [ - 8, - 75 - ], - [ - 21, - 115 - ] - ], - [ - [ - 17276, - 15253 - ], - [ - 39, - 122 - ], - [ - -15, - 89 - ], - [ - -51, - 29 - ], - [ - 18, - 53 - ], - [ - 58, - -6 - ], - [ - 33, - 66 - ], - [ - 22, - 77 - ], - [ - 94, - 28 - ], - [ - -15, - -55 - ], - [ - 10, - -34 - ], - [ - 29, - 3 - ] - ], - [ - [ - 16306, - 15260 - ], - [ - -13, - 85 - ], - [ - 10, - 125 - ], - [ - -54, - 41 - ], - [ - 18, - 82 - ], - [ - -46, - 7 - ], - [ - 15, - 101 - ], - [ - 66, - -29 - ], - [ - 61, - 38 - ], - [ - -51, - 72 - ], - [ - -20, - 69 - ], - [ - -56, - -31 - ], - [ - -7, - -88 - ], - [ - -22, - 78 - ] - ], - [ - [ - 16449, - 15753 - ], - [ - 79, - 2 - ], - [ - -12, - 60 - ], - [ - 60, - 41 - ], - [ - 58, - 70 - ], - [ - 94, - -63 - ], - [ - 8, - -96 - ], - [ - 26, - -25 - ], - [ - 76, - 6 - ], - [ - 23, - -22 - ], - [ - 35, - -124 - ], - [ - 79, - -82 - ], - [ - 46, - -57 - ], - [ - 73, - -59 - ], - [ - 92, - -51 - ], - [ - -2, - -73 - ] - ], - [ - [ - 21259, - 9730 - ], - [ - 8, - 29 - ], - [ - 60, - 27 - ], - [ - 49, - 4 - ], - [ - 21, - 15 - ], - [ - 27, - -15 - ], - [ - -26, - -33 - ], - [ - -72, - -52 - ], - [ - -59, - -35 - ] - ], - [ - [ - 8248, - 12088 - ], - [ - 40, - 16 - ], - [ - 15, - -5 - ], - [ - -3, - -89 - ], - [ - -58, - -13 - ], - [ - -13, - 11 - ], - [ - 20, - 33 - ], - [ - -1, - 47 - ] - ], - [ - [ - 13135, - 15230 - ], - [ - 75, - 48 - ], - [ - 49, - -14 - ], - [ - -2, - -61 - ], - [ - 59, - 44 - ], - [ - 5, - -23 - ], - [ - -35, - -59 - ], - [ - 0, - -55 - ], - [ - 24, - -30 - ], - [ - -9, - -104 - ], - [ - -46, - -60 - ], - [ - 13, - -66 - ], - [ - 36, - -2 - ], - [ - 18, - -57 - ], - [ - 26, - -18 - ] - ], - [ - [ - 15067, - 15095 - ], - [ - -25, - 54 - ], - [ - 26, - 45 - ], - [ - -42, - -10 - ], - [ - -59, - 28 - ], - [ - -48, - -70 - ], - [ - -105, - -13 - ], - [ - -57, - 64 - ], - [ - -75, - 4 - ], - [ - -16, - -49 - ], - [ - -48, - -15 - ], - [ - -68, - 64 - ], - [ - -76, - -2 - ], - [ - -41, - 119 - ], - [ - -51, - 67 - ], - [ - 34, - 93 - ], - [ - -44, - 58 - ], - [ - 77, - 114 - ], - [ - 107, - 5 - ], - [ - 30, - 91 - ], - [ - 133, - -16 - ], - [ - 83, - 78 - ], - [ - 82, - 34 - ], - [ - 115, - 3 - ], - [ - 122, - -85 - ], - [ - 100, - -46 - ], - [ - 81, - 18 - ], - [ - 60, - -10 - ], - [ - 82, - 62 - ] - ], - [ - [ - 14499, - 15837 - ], - [ - 8, - -46 - ], - [ - 61, - -39 - ], - [ - -12, - -29 - ], - [ - -83, - -7 - ], - [ - -30, - -37 - ], - [ - -58, - -65 - ], - [ - -22, - 56 - ], - [ - 1, - 25 - ] - ], - [ - [ - 21036, - 13724 - ], - [ - -42, - -193 - ], - [ - -29, - -98 - ], - [ - -37, - 101 - ], - [ - -8, - 89 - ], - [ - 41, - 118 - ], - [ - 56, - 91 - ], - [ - 32, - -36 - ], - [ - -13, - -72 - ] - ], - [ - [ - 14668, - 10661 - ], - [ - 24, - 14 - ], - [ - 77, - -1 - ], - [ - 142, - 9 - ] - ], - [ - [ - 15280, - 10236 - ], - [ - -32, - -148 - ], - [ - 4, - -68 - ], - [ - 45, - -43 - ], - [ - 2, - -32 - ], - [ - -19, - -72 - ], - [ - 4, - -36 - ], - [ - -5, - -58 - ], - [ - 24, - -75 - ], - [ - 29, - -118 - ], - [ - 26, - -27 - ] - ], - [ - [ - 14831, - 9690 - ], - [ - -39, - 36 - ], - [ - -45, - 20 - ], - [ - -28, - 20 - ], - [ - -29, - 31 - ] - ], - [ - [ - 15212, - 16448 - ], - [ - -56, - -10 - ], - [ - -46, - -38 - ], - [ - -65, - -7 - ], - [ - -60, - -44 - ], - [ - 3, - -65 - ] - ], - [ - [ - 14878, - 16312 - ], - [ - -9, - 13 - ], - [ - -109, - 31 - ], - [ - -4, - 44 - ], - [ - -65, - -14 - ], - [ - -26, - -66 - ], - [ - -54, - -89 - ] - ], - [ - [ - 8827, - 6746 - ], - [ - -30, - -75 - ], - [ - -79, - -67 - ], - [ - -51, - 24 - ], - [ - -38, - -13 - ], - [ - -65, - 52 - ], - [ - -47, - -4 - ], - [ - -42, - 66 - ] - ], - [ - [ - 7867, - 16212 - ], - [ - 13, - -39 - ], - [ - -75, - -58 - ], - [ - -72, - -42 - ], - [ - -73, - -35 - ], - [ - -37, - -71 - ], - [ - -12, - -27 - ], - [ - -1, - -64 - ], - [ - 23, - -64 - ], - [ - 29, - -3 - ], - [ - -7, - 44 - ], - [ - 21, - -26 - ], - [ - -6, - -35 - ], - [ - -47, - -19 - ], - [ - -33, - 2 - ], - [ - -52, - -21 - ], - [ - -30, - -6 - ], - [ - -41, - -6 - ], - [ - -58, - -34 - ], - [ - 103, - 22 - ], - [ - 20, - -22 - ], - [ - -97, - -36 - ], - [ - -45, - -1 - ], - [ - 2, - 15 - ], - [ - -21, - -33 - ], - [ - 21, - -6 - ], - [ - -15, - -86 - ], - [ - -51, - -92 - ], - [ - -5, - 31 - ], - [ - -16, - 6 - ], - [ - -22, - 30 - ], - [ - 14, - -65 - ], - [ - 17, - -21 - ], - [ - 1, - -46 - ], - [ - -22, - -46 - ], - [ - -39, - -96 - ], - [ - -7, - 5 - ], - [ - 22, - 81 - ], - [ - -36, - 46 - ], - [ - -8, - 100 - ], - [ - -13, - -52 - ], - [ - 15, - -76 - ], - [ - -46, - 19 - ], - [ - 48, - -39 - ], - [ - 3, - -114 - ], - [ - 20, - -8 - ], - [ - 7, - -42 - ], - [ - 10, - -120 - ], - [ - -45, - -89 - ], - [ - -72, - -35 - ], - [ - -46, - -71 - ], - [ - -34, - -8 - ], - [ - -36, - -44 - ], - [ - -10, - -40 - ], - [ - -76, - -78 - ], - [ - -39, - -57 - ], - [ - -33, - -71 - ], - [ - -11, - -85 - ], - [ - 12, - -83 - ], - [ - 24, - -103 - ], - [ - 30, - -85 - ], - [ - 1, - -52 - ], - [ - 33, - -139 - ], - [ - -2, - -81 - ], - [ - -3, - -47 - ], - [ - -18, - -73 - ], - [ - -21, - -15 - ], - [ - -34, - 15 - ], - [ - -11, - 52 - ], - [ - -26, - 28 - ], - [ - -37, - 103 - ], - [ - -33, - 92 - ], - [ - -10, - 47 - ], - [ - 14, - 79 - ], - [ - -19, - 66 - ], - [ - -55, - 101 - ], - [ - -27, - 18 - ], - [ - -70, - -54 - ], - [ - -13, - 6 - ], - [ - -34, - 56 - ], - [ - -43, - 29 - ], - [ - -79, - -15 - ], - [ - -62, - 13 - ], - [ - -53, - -8 - ], - [ - -29, - -19 - ], - [ - 13, - -31 - ], - [ - -2, - -49 - ], - [ - 15, - -24 - ], - [ - -13, - -16 - ], - [ - -26, - 18 - ], - [ - -26, - -23 - ], - [ - -51, - 4 - ], - [ - -52, - 64 - ], - [ - -60, - -15 - ], - [ - -51, - 27 - ], - [ - -44, - -8 - ], - [ - -58, - -28 - ], - [ - -64, - -89 - ], - [ - -69, - -52 - ], - [ - -38, - -57 - ], - [ - -16, - -54 - ], - [ - -1, - -83 - ], - [ - 4, - -57 - ], - [ - 13, - -41 - ] - ], - [ - [ - 4383, - 14700 - ], - [ - -12, - 62 - ], - [ - -45, - 69 - ], - [ - -33, - 14 - ], - [ - -7, - 34 - ], - [ - -39, - 6 - ], - [ - -25, - 33 - ], - [ - -65, - 12 - ], - [ - -18, - 19 - ], - [ - -8, - 66 - ], - [ - -68, - 120 - ], - [ - -58, - 167 - ], - [ - 2, - 28 - ], - [ - -30, - 40 - ], - [ - -54, - 100 - ], - [ - -10, - 98 - ], - [ - -37, - 66 - ], - [ - 15, - 99 - ], - [ - -2, - 103 - ], - [ - -22, - 92 - ], - [ - 27, - 113 - ], - [ - 8, - 109 - ], - [ - 9, - 109 - ], - [ - -13, - 161 - ], - [ - -22, - 102 - ], - [ - -20, - 56 - ], - [ - 8, - 23 - ], - [ - 101, - -41 - ], - [ - 37, - -113 - ], - [ - 17, - 32 - ], - [ - -11, - 98 - ], - [ - -23, - 99 - ] - ], - [ - [ - 3448, - 17372 - ], - [ - -38, - 45 - ], - [ - -62, - 38 - ], - [ - -19, - 105 - ], - [ - -90, - 97 - ], - [ - -38, - 113 - ], - [ - -67, - 8 - ], - [ - -111, - 3 - ], - [ - -81, - 34 - ], - [ - -144, - 125 - ], - [ - -67, - 23 - ], - [ - -122, - 42 - ], - [ - -97, - -10 - ], - [ - -137, - 55 - ], - [ - -83, - 51 - ], - [ - -77, - -25 - ], - [ - 14, - -83 - ], - [ - -38, - -8 - ], - [ - -81, - -25 - ], - [ - -61, - -40 - ], - [ - -77, - -26 - ], - [ - -10, - 71 - ], - [ - 31, - 117 - ], - [ - 74, - 37 - ], - [ - -19, - 30 - ], - [ - -89, - -66 - ], - [ - -47, - -80 - ], - [ - -101, - -86 - ], - [ - 51, - -58 - ], - [ - -66, - -86 - ], - [ - -75, - -50 - ], - [ - -69, - -37 - ], - [ - -18, - -53 - ], - [ - -109, - -62 - ], - [ - -22, - -56 - ], - [ - -81, - -52 - ], - [ - -48, - 10 - ], - [ - -65, - -34 - ], - [ - -71, - -41 - ], - [ - -58, - -40 - ], - [ - -119, - -34 - ], - [ - -11, - 20 - ], - [ - 76, - 56 - ], - [ - 68, - 37 - ], - [ - 74, - 66 - ], - [ - 87, - 13 - ], - [ - 34, - 50 - ], - [ - 97, - 71 - ], - [ - 15, - 24 - ], - [ - 52, - 43 - ], - [ - 12, - 91 - ], - [ - 35, - 71 - ], - [ - -80, - -37 - ], - [ - -22, - 21 - ], - [ - -38, - -44 - ], - [ - -46, - 61 - ], - [ - -19, - -43 - ], - [ - -26, - 60 - ], - [ - -69, - -48 - ], - [ - -43, - 0 - ], - [ - -6, - 71 - ], - [ - 13, - 44 - ], - [ - -45, - 43 - ], - [ - -91, - -23 - ], - [ - -59, - 56 - ], - [ - -48, - 29 - ], - [ - 0, - 68 - ], - [ - -54, - 51 - ], - [ - 27, - 69 - ], - [ - 57, - 67 - ], - [ - 25, - 62 - ], - [ - 57, - 9 - ], - [ - 47, - -20 - ], - [ - 57, - 58 - ], - [ - 50, - -10 - ], - [ - 53, - 37 - ], - [ - -13, - 55 - ], - [ - -39, - 22 - ], - [ - 52, - 46 - ], - [ - -43, - -2 - ], - [ - -74, - -26 - ], - [ - -21, - -26 - ], - [ - -55, - 26 - ], - [ - -99, - -13 - ], - [ - -102, - 29 - ], - [ - -29, - 48 - ], - [ - -88, - 70 - ], - [ - 98, - 50 - ], - [ - 155, - 58 - ], - [ - 58, - 0 - ], - [ - -10, - -60 - ], - [ - 147, - 5 - ], - [ - -56, - 74 - ], - [ - -86, - 46 - ], - [ - -50, - 60 - ], - [ - -67, - 51 - ], - [ - -95, - 38 - ], - [ - 39, - 63 - ], - [ - 123, - 4 - ], - [ - 88, - 55 - ], - [ - 17, - 58 - ], - [ - 71, - 57 - ], - [ - 68, - 14 - ], - [ - 132, - 53 - ], - [ - 64, - -8 - ], - [ - 108, - 64 - ], - [ - 105, - -25 - ], - [ - 50, - -54 - ], - [ - 31, - 23 - ], - [ - 118, - -7 - ], - [ - -4, - -28 - ], - [ - 107, - -20 - ], - [ - 71, - 12 - ], - [ - 147, - -38 - ], - [ - 134, - -12 - ], - [ - 53, - -15 - ], - [ - 93, - 19 - ], - [ - 106, - -36 - ], - [ - 76, - -17 - ] - ], - [ - [ - 1705, - 13087 - ], - [ - -10, - -20 - ], - [ - -18, - 17 - ], - [ - 2, - 33 - ], - [ - -11, - 44 - ], - [ - 3, - 13 - ], - [ - 12, - 20 - ], - [ - -4, - 23 - ], - [ - 4, - 12 - ], - [ - 5, - -3 - ], - [ - 27, - -20 - ], - [ - 12, - -10 - ], - [ - 11, - -16 - ], - [ - 18, - -42 - ], - [ - -2, - -7 - ], - [ - -27, - -26 - ], - [ - -22, - -18 - ] - ], - [ - [ - 1667, - 13274 - ], - [ - -23, - -9 - ], - [ - -12, - 26 - ], - [ - -8, - 9 - ], - [ - -1, - 8 - ], - [ - 7, - 10 - ], - [ - 25, - -11 - ], - [ - 18, - -19 - ], - [ - -6, - -14 - ] - ], - [ - [ - 1620, - 13338 - ], - [ - -2, - -13 - ], - [ - -37, - 3 - ], - [ - 5, - 15 - ], - [ - 34, - -5 - ] - ], - [ - [ - 1558, - 13355 - ], - [ - -4, - -7 - ], - [ - -5, - 2 - ], - [ - -24, - 4 - ], - [ - -9, - 27 - ], - [ - -3, - 5 - ], - [ - 19, - 17 - ], - [ - 6, - -8 - ], - [ - 20, - -40 - ] - ], - [ - [ - 1440, - 13434 - ], - [ - -8, - -12 - ], - [ - -24, - 22 - ], - [ - 4, - 9 - ], - [ - 10, - 12 - ], - [ - 16, - -3 - ], - [ - 2, - -28 - ] - ], - [ - [ - 1882, - 17649 - ], - [ - -70, - -45 - ], - [ - -36, - 31 - ], - [ - -10, - 56 - ], - [ - 63, - 42 - ], - [ - 37, - 19 - ], - [ - 46, - -8 - ], - [ - 30, - -38 - ], - [ - -60, - -57 - ] - ], - [ - [ - 1005, - 17985 - ], - [ - -43, - -19 - ], - [ - -45, - 22 - ], - [ - -43, - 33 - ], - [ - 69, - 20 - ], - [ - 56, - -10 - ], - [ - 6, - -46 - ] - ], - [ - [ - 576, - 18449 - ], - [ - 43, - -23 - ], - [ - 44, - 13 - ], - [ - 56, - -32 - ], - [ - 69, - -16 - ], - [ - -5, - -13 - ], - [ - -53, - -26 - ], - [ - -53, - 27 - ], - [ - -27, - 21 - ], - [ - -61, - -7 - ], - [ - -17, - 11 - ], - [ - 4, - 45 - ] - ], - [ - [ - 7575, - 12210 - ], - [ - -2, - -28 - ], - [ - -41, - -14 - ], - [ - 23, - -55 - ], - [ - -1, - -63 - ], - [ - -31, - -69 - ], - [ - 27, - -95 - ], - [ - 30, - 7 - ], - [ - 15, - 87 - ], - [ - -21, - 42 - ], - [ - -4, - 91 - ], - [ - 87, - 49 - ], - [ - -10, - 56 - ], - [ - 25, - 38 - ], - [ - 25, - -84 - ], - [ - 49, - -2 - ], - [ - 45, - -67 - ], - [ - 3, - -40 - ], - [ - 62, - -1 - ], - [ - 75, - 13 - ], - [ - 40, - -54 - ], - [ - 53, - -15 - ], - [ - 39, - 38 - ], - [ - 1, - 30 - ], - [ - 86, - 7 - ], - [ - 84, - 2 - ], - [ - -59, - -36 - ], - [ - 24, - -56 - ], - [ - 55, - -9 - ], - [ - 53, - -59 - ], - [ - 11, - -96 - ], - [ - 37, - 2 - ], - [ - 27, - -28 - ] - ], - [ - [ - 20079, - 13383 - ], - [ - -93, - -103 - ], - [ - -58, - -113 - ], - [ - -15, - -83 - ], - [ - 53, - -127 - ], - [ - 66, - -157 - ], - [ - 63, - -74 - ], - [ - 42, - -96 - ], - [ - 32, - -222 - ], - [ - -9, - -211 - ], - [ - -58, - -79 - ], - [ - -80, - -77 - ], - [ - -57, - -100 - ], - [ - -87, - -112 - ], - [ - -25, - 77 - ], - [ - 19, - 81 - ], - [ - -52, - 68 - ] - ], - [ - [ - 24248, - 8822 - ], - [ - -23, - -16 - ], - [ - -24, - 52 - ], - [ - 3, - 33 - ], - [ - 44, - -69 - ] - ], - [ - [ - 24196, - 9006 - ], - [ - 12, - -97 - ], - [ - -19, - 15 - ], - [ - -15, - -7 - ], - [ - -10, - 34 - ], - [ - -1, - 91 - ], - [ - 33, - -36 - ] - ], - [ - [ - 16250, - 12795 - ], - [ - -51, - -32 - ], - [ - -13, - -54 - ], - [ - -2, - -41 - ], - [ - -69, - -50 - ], - [ - -112, - -56 - ], - [ - -62, - -85 - ], - [ - -31, - -6 - ], - [ - -21, - 7 - ], - [ - -40, - -50 - ], - [ - -45, - -23 - ], - [ - -58, - -6 - ], - [ - -18, - -7 - ], - [ - -15, - -32 - ], - [ - -19, - -9 - ], - [ - -10, - -30 - ], - [ - -35, - 2 - ], - [ - -22, - -16 - ], - [ - -48, - 6 - ], - [ - -19, - 70 - ], - [ - 2, - 66 - ], - [ - -11, - 35 - ], - [ - -14, - 89 - ], - [ - -20, - 49 - ], - [ - 14, - 6 - ], - [ - -7, - 55 - ], - [ - 9, - 23 - ], - [ - -3, - 52 - ] - ], - [ - [ - 14599, - 8147 - ], - [ - 29, - -1 - ], - [ - 33, - -21 - ], - [ - 24, - 15 - ], - [ - 37, - -12 - ] - ], - [ - [ - 14836, - 7589 - ], - [ - -17, - -87 - ], - [ - -9, - -100 - ], - [ - -18, - -54 - ], - [ - -47, - -61 - ], - [ - -14, - -17 - ], - [ - -29, - -61 - ], - [ - -20, - -62 - ], - [ - -39, - -86 - ], - [ - -79, - -123 - ], - [ - -49, - -72 - ], - [ - -53, - -55 - ], - [ - -73, - -47 - ], - [ - -35, - -6 - ], - [ - -9, - -33 - ], - [ - -43, - 18 - ], - [ - -34, - -23 - ], - [ - -76, - 23 - ], - [ - -42, - -15 - ], - [ - -29, - 7 - ], - [ - -72, - -48 - ], - [ - -59, - -19 - ], - [ - -43, - -45 - ], - [ - -32, - -3 - ], - [ - -30, - 43 - ], - [ - -23, - 2 - ], - [ - -30, - 54 - ], - [ - -3, - -17 - ], - [ - -10, - 32 - ], - [ - 1, - 70 - ], - [ - -23, - 81 - ], - [ - 23, - 22 - ], - [ - -2, - 92 - ], - [ - -46, - 112 - ], - [ - -35, - 102 - ], - [ - 0, - 0 - ], - [ - -50, - 156 - ] - ], - [ - [ - 14658, - 8937 - ], - [ - -53, - -17 - ], - [ - -40, - -47 - ], - [ - -8, - -42 - ], - [ - -25, - -10 - ], - [ - -61, - -98 - ], - [ - -38, - -78 - ], - [ - -24, - -3 - ], - [ - -22, - 14 - ], - [ - -78, - 13 - ] - ] - ], - "transform": { - "scale": [ - 0.01434548714883443, - 0.008335499711981569 - ], - "translate": [ - -180, - -90 - ] - }, - "objects": { - "ne_110m_admin_0_countries": { - "type": "GeometryCollection", - "geometries": [ - { - "arcs": [ - [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Afghanistan", - "NAME_LONG": "Afghanistan", - "ABBREV": "Afg.", - "FORMAL_EN": "Islamic State of Afghanistan", - "POP_EST": 34124811, - "POP_RANK": 15, - "GDP_MD_EST": 64080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AF", - "ISO_A3": "AFG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 6, - 7, - 8, - 9 - ] - ], - [ - [ - 10, - 11, - 12 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Angola", - "NAME_LONG": "Angola", - "ABBREV": "Ang.", - "FORMAL_EN": "People's Republic of Angola", - "POP_EST": 29310273, - "POP_RANK": 15, - "GDP_MD_EST": 189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AO", - "ISO_A3": "AGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - 13, - 14, - 15, - 16, - 17 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Albania", - "NAME_LONG": "Albania", - "ABBREV": "Alb.", - "FORMAL_EN": "Republic of Albania", - "POP_EST": 3047987, - "POP_RANK": 12, - "GDP_MD_EST": 33900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AL", - "ISO_A3": "ALB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 18, - 19, - 20, - 21, - 22 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "United Arab Emirates", - "NAME_LONG": "United Arab Emirates", - "ABBREV": "U.A.E.", - "FORMAL_EN": "United Arab Emirates", - "POP_EST": 6072475, - "POP_RANK": 13, - "GDP_MD_EST": 667200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AE", - "ISO_A3": "ARE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 23, - 24 - ] - ], - [ - [ - 25, - 26, - 27, - 28, - 29, - 30 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Argentina", - "NAME_LONG": "Argentina", - "ABBREV": "Arg.", - "FORMAL_EN": "Argentine Republic", - "POP_EST": 44293293, - "POP_RANK": 15, - "GDP_MD_EST": 879400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AR", - "ISO_A3": "ARG", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 31, - 32, - 33, - 34, - 35 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Armenia", - "NAME_LONG": "Armenia", - "ABBREV": "Arm.", - "FORMAL_EN": "Republic of Armenia", - "POP_EST": 3045191, - "POP_RANK": 12, - "GDP_MD_EST": 26300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AM", - "ISO_A3": "ARM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 36 - ] - ], - [ - [ - 37 - ] - ], - [ - [ - 38 - ] - ], - [ - [ - 39 - ] - ], - [ - [ - 40 - ] - ], - [ - [ - 41 - ] - ], - [ - [ - 42 - ] - ], - [ - [ - 43 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Antarctica", - "NAME_LONG": "Antarctica", - "ABBREV": "Ant.", - "FORMAL_EN": "", - "POP_EST": 4050, - "POP_RANK": 4, - "GDP_MD_EST": 810, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "AQ", - "ISO_A3": "ATA", - "CONTINENT": "Antarctica", - "REGION_UN": "Antarctica", - "SUBREGION": "Antarctica" - } - }, - { - "arcs": [ - [ - 44 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Fr. S. Antarctic Lands", - "NAME_LONG": "French Southern and Antarctic Lands", - "ABBREV": "Fr. S.A.L.", - "FORMAL_EN": "Territory of the French Southern and Antarctic Lands", - "POP_EST": 140, - "POP_RANK": 1, - "GDP_MD_EST": 16, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TF", - "ISO_A3": "ATF", - "CONTINENT": "Seven seas (open ocean)", - "REGION_UN": "Seven seas (open ocean)", - "SUBREGION": "Seven seas (open ocean)" - } - }, - { - "arcs": [ - [ - [ - 45 - ] - ], - [ - [ - 46 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Australia", - "NAME_LONG": "Australia", - "ABBREV": "Auz.", - "FORMAL_EN": "Commonwealth of Australia", - "POP_EST": 23232413, - "POP_RANK": 15, - "GDP_MD_EST": 1189000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AU", - "ISO_A3": "AUS", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - 47, - 48, - 49, - 50, - 51, - 52, - 53 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Austria", - "NAME_LONG": "Austria", - "ABBREV": "Aust.", - "FORMAL_EN": "Republic of Austria", - "POP_EST": 8754413, - "POP_RANK": 13, - "GDP_MD_EST": 416600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AT", - "ISO_A3": "AUT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -33, - 54, - 55, - 56, - 57 - ] - ], - [ - [ - -35, - 58 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Azerbaijan", - "NAME_LONG": "Azerbaijan", - "ABBREV": "Aze.", - "FORMAL_EN": "Republic of Azerbaijan", - "POP_EST": 9961396, - "POP_RANK": 13, - "GDP_MD_EST": 167900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "AZ", - "ISO_A3": "AZE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 59, - 60, - 61 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burundi", - "NAME_LONG": "Burundi", - "ABBREV": "Bur.", - "FORMAL_EN": "Republic of Burundi", - "POP_EST": 11466756, - "POP_RANK": 14, - "GDP_MD_EST": 7892, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BI", - "ISO_A3": "BDI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 62, - 63, - 64, - 65, - 66 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belgium", - "NAME_LONG": "Belgium", - "ABBREV": "Belg.", - "FORMAL_EN": "Kingdom of Belgium", - "POP_EST": 11491346, - "POP_RANK": 14, - "GDP_MD_EST": 508600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BE", - "ISO_A3": "BEL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 67, - 68, - 69, - 70, - 71 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Benin", - "NAME_LONG": "Benin", - "ABBREV": "Benin", - "FORMAL_EN": "Republic of Benin", - "POP_EST": 11038805, - "POP_RANK": 14, - "GDP_MD_EST": 24310, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BJ", - "ISO_A3": "BEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -70, - 72, - 73, - 74, - 75, - 76 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Burkina Faso", - "NAME_LONG": "Burkina Faso", - "ABBREV": "B.F.", - "FORMAL_EN": "Burkina Faso", - "POP_EST": 20107509, - "POP_RANK": 15, - "GDP_MD_EST": 32990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BF", - "ISO_A3": "BFA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 77, - 78, - 79 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bangladesh", - "NAME_LONG": "Bangladesh", - "ABBREV": "Bang.", - "FORMAL_EN": "People's Republic of Bangladesh", - "POP_EST": 157826578, - "POP_RANK": 17, - "GDP_MD_EST": 628400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BD", - "ISO_A3": "BGD", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 80, - 81, - 82, - 83, - 84, - 85 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bulgaria", - "NAME_LONG": "Bulgaria", - "ABBREV": "Bulg.", - "FORMAL_EN": "Republic of Bulgaria", - "POP_EST": 7101510, - "POP_RANK": 13, - "GDP_MD_EST": 143100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BG", - "ISO_A3": "BGR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 86 - ] - ], - [ - [ - 87 - ] - ], - [ - [ - 88 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Bahamas", - "NAME_LONG": "Bahamas", - "ABBREV": "Bhs.", - "FORMAL_EN": "Commonwealth of the Bahamas", - "POP_EST": 329988, - "POP_RANK": 10, - "GDP_MD_EST": 9066, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BS", - "ISO_A3": "BHS", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 89, - 90, - 91 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bosnia and Herz.", - "NAME_LONG": "Bosnia and Herzegovina", - "ABBREV": "B.H.", - "FORMAL_EN": "Bosnia and Herzegovina", - "POP_EST": 3856181, - "POP_RANK": 12, - "GDP_MD_EST": 42530, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BA", - "ISO_A3": "BIH", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 92, - 93, - 94, - 95, - 96 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belarus", - "NAME_LONG": "Belarus", - "ABBREV": "Bela.", - "FORMAL_EN": "Republic of Belarus", - "POP_EST": 9549747, - "POP_RANK": 13, - "GDP_MD_EST": 165400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BY", - "ISO_A3": "BLR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 97, - 98, - 99 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Belize", - "NAME_LONG": "Belize", - "ABBREV": "Belize", - "FORMAL_EN": "Belize", - "POP_EST": 360346, - "POP_RANK": 10, - "GDP_MD_EST": 3088, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BZ", - "ISO_A3": "BLZ", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -27, - 100, - 101, - 102, - 103 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bolivia", - "NAME_LONG": "Bolivia", - "ABBREV": "Bolivia", - "FORMAL_EN": "Plurinational State of Bolivia", - "POP_EST": 11138234, - "POP_RANK": 14, - "GDP_MD_EST": 78350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BO", - "ISO_A3": "BOL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -29, - 104, - -103, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brazil", - "NAME_LONG": "Brazil", - "ABBREV": "Brazil", - "FORMAL_EN": "Federative Republic of Brazil", - "POP_EST": 207353391, - "POP_RANK": 17, - "GDP_MD_EST": 3081000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BR", - "ISO_A3": "BRA", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 113, - 114 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Brunei", - "NAME_LONG": "Brunei Darussalam", - "ABBREV": "Brunei", - "FORMAL_EN": "Negara Brunei Darussalam", - "POP_EST": 443593, - "POP_RANK": 10, - "GDP_MD_EST": 33730, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BN", - "ISO_A3": "BRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 115, - 116 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Bhutan", - "NAME_LONG": "Bhutan", - "ABBREV": "Bhutan", - "FORMAL_EN": "Kingdom of Bhutan", - "POP_EST": 758288, - "POP_RANK": 11, - "GDP_MD_EST": 6432, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BT", - "ISO_A3": "BTN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 117, - 118, - 119, - 120 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Botswana", - "NAME_LONG": "Botswana", - "ABBREV": "Bwa.", - "FORMAL_EN": "Republic of Botswana", - "POP_EST": 2214858, - "POP_RANK": 12, - "GDP_MD_EST": 35900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "BW", - "ISO_A3": "BWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 121, - 122, - 123, - 124, - 125, - 126 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Central African Rep.", - "NAME_LONG": "Central African Republic", - "ABBREV": "C.A.R.", - "FORMAL_EN": "Central African Republic", - "POP_EST": 5625118, - "POP_RANK": 13, - "GDP_MD_EST": 3206, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CF", - "ISO_A3": "CAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 127 - ] - ], - [ - [ - 128 - ] - ], - [ - [ - 129 - ] - ], - [ - [ - 130 - ] - ], - [ - [ - 131 - ] - ], - [ - [ - 132 - ] - ], - [ - [ - 133 - ] - ], - [ - [ - 134 - ] - ], - [ - [ - 135 - ] - ], - [ - [ - 136 - ] - ], - [ - [ - 137, - 138, - 139, - 140 - ] - ], - [ - [ - 141 - ] - ], - [ - [ - 142 - ] - ], - [ - [ - 143 - ] - ], - [ - [ - 144 - ] - ], - [ - [ - 145 - ] - ], - [ - [ - 146 - ] - ], - [ - [ - 147 - ] - ], - [ - [ - 148 - ] - ], - [ - [ - 149 - ] - ], - [ - [ - 150 - ] - ], - [ - [ - 151 - ] - ], - [ - [ - 152 - ] - ], - [ - [ - 153 - ] - ], - [ - [ - 154 - ] - ], - [ - [ - 155 - ] - ], - [ - [ - 156 - ] - ], - [ - [ - 157 - ] - ], - [ - [ - 158 - ] - ], - [ - [ - 159 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Canada", - "NAME_LONG": "Canada", - "ABBREV": "Can.", - "FORMAL_EN": "Canada", - "POP_EST": 35623680, - "POP_RANK": 15, - "GDP_MD_EST": 1674000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CA", - "ISO_A3": "CAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -51, - 160, - 161, - 162 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Switzerland", - "NAME_LONG": "Switzerland", - "ABBREV": "Switz.", - "FORMAL_EN": "Swiss Confederation", - "POP_EST": 8236303, - "POP_RANK": 13, - "GDP_MD_EST": 496300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CH", - "ISO_A3": "CHE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -24, - 163 - ] - ], - [ - [ - -26, - 164, - 165, - -101 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Chile", - "NAME_LONG": "Chile", - "ABBREV": "Chile", - "FORMAL_EN": "Republic of Chile", - "POP_EST": 17789267, - "POP_RANK": 14, - "GDP_MD_EST": 436100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CL", - "ISO_A3": "CHL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -4, - 166, - 167, - 168, - 169, - 170, - 171, - 172, - 173, - 174, - 175, - 176, - 177, - -117, - 178, - 179, - 180, - 181 - ] - ], - [ - [ - 182 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "China", - "NAME_LONG": "China", - "ABBREV": "China", - "FORMAL_EN": "People's Republic of China", - "POP_EST": 1379302771, - "POP_RANK": 18, - "GDP_MD_EST": 21140000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CN", - "ISO_A3": "CHN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -75, - 183, - 184, - 185, - 186, - 187 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Côte d'Ivoire", - "NAME_LONG": "Côte d'Ivoire", - "ABBREV": "I.C.", - "FORMAL_EN": "Republic of Ivory Coast", - "POP_EST": 24184810, - "POP_RANK": 15, - "GDP_MD_EST": 87120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CI", - "ISO_A3": "CIV", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -127, - 188, - 189, - 190, - 191, - 192, - 193, - 194 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cameroon", - "NAME_LONG": "Cameroon", - "ABBREV": "Cam.", - "FORMAL_EN": "Republic of Cameroon", - "POP_EST": 24994885, - "POP_RANK": 15, - "GDP_MD_EST": 77240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CM", - "ISO_A3": "CMR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -9, - 195, - -13, - 196, - -125, - 197, - 198, - 199, - -60, - 200, - 201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dem. Rep. Congo", - "NAME_LONG": "Democratic Republic of the Congo", - "ABBREV": "D.R.C.", - "FORMAL_EN": "Democratic Republic of the Congo", - "POP_EST": 83301151, - "POP_RANK": 16, - "GDP_MD_EST": 66010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CD", - "ISO_A3": "COD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -12, - 202, - 203, - -189, - -126, - -197 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Congo", - "NAME_LONG": "Republic of the Congo", - "ABBREV": "Rep. Congo", - "FORMAL_EN": "Republic of the Congo", - "POP_EST": 4954674, - "POP_RANK": 12, - "GDP_MD_EST": 30270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CG", - "ISO_A3": "COG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -107, - 204, - 205, - 206, - 207, - 208, - 209 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Colombia", - "NAME_LONG": "Colombia", - "ABBREV": "Col.", - "FORMAL_EN": "Republic of Colombia", - "POP_EST": 47698524, - "POP_RANK": 15, - "GDP_MD_EST": 688000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CO", - "ISO_A3": "COL", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 210, - 211, - 212, - 213 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Costa Rica", - "NAME_LONG": "Costa Rica", - "ABBREV": "C.R.", - "FORMAL_EN": "Republic of Costa Rica", - "POP_EST": 4930258, - "POP_RANK": 12, - "GDP_MD_EST": 79260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CR", - "ISO_A3": "CRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - 214 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cuba", - "NAME_LONG": "Cuba", - "ABBREV": "Cuba", - "FORMAL_EN": "Republic of Cuba", - "POP_EST": 11147407, - "POP_RANK": 14, - "GDP_MD_EST": 132900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CU", - "ISO_A3": "CUB", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 215, - 216 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "N. Cyprus", - "NAME_LONG": "Northern Cyprus", - "ABBREV": "N. Cy.", - "FORMAL_EN": "Turkish Republic of Northern Cyprus", - "POP_EST": 265100, - "POP_RANK": 10, - "GDP_MD_EST": 3600, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -217, - 217 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cyprus", - "NAME_LONG": "Cyprus", - "ABBREV": "Cyp.", - "FORMAL_EN": "Republic of Cyprus", - "POP_EST": 1221549, - "POP_RANK": 12, - "GDP_MD_EST": 29260, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CY", - "ISO_A3": "CYP", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -53, - 218, - 219, - 220 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Czechia", - "NAME_LONG": "Czech Republic", - "ABBREV": "Cz.", - "FORMAL_EN": "Czech Republic", - "POP_EST": 10674723, - "POP_RANK": 14, - "GDP_MD_EST": 350900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "CZ", - "ISO_A3": "CZE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -52, - -163, - 221, - 222, - -63, - 223, - 224, - 225, - 226, - 227, - -219 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Germany", - "NAME_LONG": "Germany", - "ABBREV": "Ger.", - "FORMAL_EN": "Federal Republic of Germany", - "POP_EST": 80594017, - "POP_RANK": 16, - "GDP_MD_EST": 3979000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DE", - "ISO_A3": "DEU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - 228, - 229, - 230, - 231 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Djibouti", - "NAME_LONG": "Djibouti", - "ABBREV": "Dji.", - "FORMAL_EN": "Republic of Djibouti", - "POP_EST": 865267, - "POP_RANK": 11, - "GDP_MD_EST": 3345, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DJ", - "ISO_A3": "DJI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -226, - 232 - ] - ], - [ - [ - 233 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Denmark", - "NAME_LONG": "Denmark", - "ABBREV": "Den.", - "FORMAL_EN": "Kingdom of Denmark", - "POP_EST": 5605948, - "POP_RANK": 13, - "GDP_MD_EST": 264800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DK", - "ISO_A3": "DNK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 234, - 235 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Dominican Rep.", - "NAME_LONG": "Dominican Republic", - "ABBREV": "Dom. Rep.", - "FORMAL_EN": "Dominican Republic", - "POP_EST": 10734247, - "POP_RANK": 14, - "GDP_MD_EST": 161900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DO", - "ISO_A3": "DOM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - 236, - 237, - 238, - 239, - 240, - 241, - 242, - 243 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Algeria", - "NAME_LONG": "Algeria", - "ABBREV": "Alg.", - "FORMAL_EN": "People's Democratic Republic of Algeria", - "POP_EST": 40969443, - "POP_RANK": 15, - "GDP_MD_EST": 609400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "DZ", - "ISO_A3": "DZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -206, - 244, - 245 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ecuador", - "NAME_LONG": "Ecuador", - "ABBREV": "Ecu.", - "FORMAL_EN": "Republic of Ecuador", - "POP_EST": 16290913, - "POP_RANK": 14, - "GDP_MD_EST": 182400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EC", - "ISO_A3": "ECU", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - 246, - 247, - 248, - 249, - 250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Egypt", - "NAME_LONG": "Egypt", - "ABBREV": "Egypt", - "FORMAL_EN": "Arab Republic of Egypt", - "POP_EST": 97041072, - "POP_RANK": 16, - "GDP_MD_EST": 1105000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EG", - "ISO_A3": "EGY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -232, - 251, - 252, - 253 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eritrea", - "NAME_LONG": "Eritrea", - "ABBREV": "Erit.", - "FORMAL_EN": "State of Eritrea", - "POP_EST": 5918919, - "POP_RANK": 13, - "GDP_MD_EST": 9169, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ER", - "ISO_A3": "ERI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 254, - 255, - 256, - 257 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Spain", - "NAME_LONG": "Spain", - "ABBREV": "Sp.", - "FORMAL_EN": "Kingdom of Spain", - "POP_EST": 48958159, - "POP_RANK": 15, - "GDP_MD_EST": 1690000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ES", - "ISO_A3": "ESP", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 258, - 259, - 260 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Estonia", - "NAME_LONG": "Estonia", - "ABBREV": "Est.", - "FORMAL_EN": "Republic of Estonia", - "POP_EST": 1251581, - "POP_RANK": 12, - "GDP_MD_EST": 38700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "EE", - "ISO_A3": "EST", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -231, - 261, - 262, - 263, - 264, - 265, - -252 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ethiopia", - "NAME_LONG": "Ethiopia", - "ABBREV": "Eth.", - "FORMAL_EN": "Federal Democratic Republic of Ethiopia", - "POP_EST": 105350020, - "POP_RANK": 17, - "GDP_MD_EST": 174700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ET", - "ISO_A3": "ETH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - 266, - 267, - 268, - 269 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Finland", - "NAME_LONG": "Finland", - "ABBREV": "Fin.", - "FORMAL_EN": "Republic of Finland", - "POP_EST": 5491218, - "POP_RANK": 13, - "GDP_MD_EST": 224137, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FI", - "ISO_A3": "FIN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - [ - 270 - ] - ], - [ - [ - 271 - ] - ], - [ - [ - 272 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Fiji", - "NAME_LONG": "Fiji", - "ABBREV": "Fiji", - "FORMAL_EN": "Republic of Fiji", - "POP_EST": 920938, - "POP_RANK": 11, - "GDP_MD_EST": 8374, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FJ", - "ISO_A3": "FJI", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - 273 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Falkland Is.", - "NAME_LONG": "Falkland Islands", - "ABBREV": "Flk. Is.", - "FORMAL_EN": "Falkland Islands", - "POP_EST": 2931, - "POP_RANK": 4, - "GDP_MD_EST": 281.8, - "POP_YEAR": 2014, - "GDP_YEAR": 2012, - "ISO_A2": "FK", - "ISO_A3": "FLK", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -65, - 274, - -222, - -162, - 275, - 276, - -256, - 277 - ] - ], - [ - [ - -111, - 278, - 279 - ] - ], - [ - [ - 280 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "France", - "NAME_LONG": "France", - "ABBREV": "Fr.", - "FORMAL_EN": "French Republic", - "POP_EST": 67106161, - "POP_RANK": 16, - "GDP_MD_EST": 2699000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "FR", - "ISO_A3": "FRA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -190, - -204, - 281, - 282 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gabon", - "NAME_LONG": "Gabon", - "ABBREV": "Gabon", - "FORMAL_EN": "Gabonese Republic", - "POP_EST": 1772255, - "POP_RANK": 12, - "GDP_MD_EST": 35980, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GA", - "ISO_A3": "GAB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - 283, - 284 - ] - ], - [ - [ - 285 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United Kingdom", - "NAME_LONG": "United Kingdom", - "ABBREV": "U.K.", - "FORMAL_EN": "United Kingdom of Great Britain and Northern Ireland", - "POP_EST": 64769452, - "POP_RANK": 16, - "GDP_MD_EST": 2788000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GB", - "ISO_A3": "GBR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -32, - 286, - 287, - 288, - -55 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Georgia", - "NAME_LONG": "Georgia", - "ABBREV": "Geo.", - "FORMAL_EN": "Georgia", - "POP_EST": 4926330, - "POP_RANK": 12, - "GDP_MD_EST": 37270, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GE", - "ISO_A3": "GEO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -74, - 289, - 290, - -184 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ghana", - "NAME_LONG": "Ghana", - "ABBREV": "Ghana", - "FORMAL_EN": "Republic of Ghana", - "POP_EST": 27499924, - "POP_RANK": 15, - "GDP_MD_EST": 120800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GH", - "ISO_A3": "GHA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -187, - 291, - 292, - 293, - 294, - 295, - 296 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea", - "NAME_LONG": "Guinea", - "ABBREV": "Gin.", - "FORMAL_EN": "Republic of Guinea", - "POP_EST": 12413867, - "POP_RANK": 14, - "GDP_MD_EST": 16080, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GN", - "ISO_A3": "GIN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - 297, - 298 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Gambia", - "NAME_LONG": "The Gambia", - "ABBREV": "Gambia", - "FORMAL_EN": "Republic of the Gambia", - "POP_EST": 2051363, - "POP_RANK": 12, - "GDP_MD_EST": 3387, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GM", - "ISO_A3": "GMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -295, - 299, - 300 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guinea-Bissau", - "NAME_LONG": "Guinea-Bissau", - "ABBREV": "GnB.", - "FORMAL_EN": "Republic of Guinea-Bissau", - "POP_EST": 1792338, - "POP_RANK": 12, - "GDP_MD_EST": 2851, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GW", - "ISO_A3": "GNB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -191, - -283, - 301 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Eq. Guinea", - "NAME_LONG": "Equatorial Guinea", - "ABBREV": "Eq. G.", - "FORMAL_EN": "Republic of Equatorial Guinea", - "POP_EST": 778358, - "POP_RANK": 11, - "GDP_MD_EST": 31770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GQ", - "ISO_A3": "GNQ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - [ - -14, - 302, - -84, - 303, - 304 - ] - ], - [ - [ - 305 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Greece", - "NAME_LONG": "Greece", - "ABBREV": "Greece", - "FORMAL_EN": "Hellenic Republic", - "POP_EST": 10768477, - "POP_RANK": 14, - "GDP_MD_EST": 290500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GR", - "ISO_A3": "GRC", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 306 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Greenland", - "NAME_LONG": "Greenland", - "ABBREV": "Grlnd.", - "FORMAL_EN": "Greenland", - "POP_EST": 57713, - "POP_RANK": 8, - "GDP_MD_EST": 2173, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "GL", - "ISO_A3": "GRL", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -100, - 307, - 308, - 309, - 310, - 311 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guatemala", - "NAME_LONG": "Guatemala", - "ABBREV": "Guat.", - "FORMAL_EN": "Republic of Guatemala", - "POP_EST": 15460732, - "POP_RANK": 14, - "GDP_MD_EST": 131800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GT", - "ISO_A3": "GTM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -109, - 312, - 313, - 314 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Guyana", - "NAME_LONG": "Guyana", - "ABBREV": "Guy.", - "FORMAL_EN": "Co-operative Republic of Guyana", - "POP_EST": 737718, - "POP_RANK": 11, - "GDP_MD_EST": 6093, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "GY", - "ISO_A3": "GUY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -309, - 315, - 316, - 317, - 318 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Honduras", - "NAME_LONG": "Honduras", - "ABBREV": "Hond.", - "FORMAL_EN": "Republic of Honduras", - "POP_EST": 9038741, - "POP_RANK": 13, - "GDP_MD_EST": 43190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HN", - "ISO_A3": "HND", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -91, - 319, - 320, - 321, - 322, - 323 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Croatia", - "NAME_LONG": "Croatia", - "ABBREV": "Cro.", - "FORMAL_EN": "Republic of Croatia", - "POP_EST": 4292095, - "POP_RANK": 12, - "GDP_MD_EST": 94240, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HR", - "ISO_A3": "HRV", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -236, - 324 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Haiti", - "NAME_LONG": "Haiti", - "ABBREV": "Haiti", - "FORMAL_EN": "Republic of Haiti", - "POP_EST": 10646714, - "POP_RANK": 14, - "GDP_MD_EST": 19340, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HT", - "ISO_A3": "HTI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -48, - 325, - 326, - 327, - 328, - -323, - 329 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Hungary", - "NAME_LONG": "Hungary", - "ABBREV": "Hun.", - "FORMAL_EN": "Republic of Hungary", - "POP_EST": 9850845, - "POP_RANK": 13, - "GDP_MD_EST": 267600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "HU", - "ISO_A3": "HUN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - 330 - ] - ], - [ - [ - 331, - 332 - ] - ], - [ - [ - 333 - ] - ], - [ - [ - 334 - ] - ], - [ - [ - 335 - ] - ], - [ - [ - 336 - ] - ], - [ - [ - 337 - ] - ], - [ - [ - 338 - ] - ], - [ - [ - 339, - 340 - ] - ], - [ - [ - 341 - ] - ], - [ - [ - 342 - ] - ], - [ - [ - 343, - 344 - ] - ], - [ - [ - 345 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Indonesia", - "NAME_LONG": "Indonesia", - "ABBREV": "Indo.", - "FORMAL_EN": "Republic of Indonesia", - "POP_EST": 260580739, - "POP_RANK": 17, - "GDP_MD_EST": 3028000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ID", - "ISO_A3": "IDN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -80, - 346, - 347, - -181, - 348, - -179, - -116, - -178, - 349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "India", - "NAME_LONG": "India", - "ABBREV": "India", - "FORMAL_EN": "Republic of India", - "POP_EST": 1281935911, - "POP_RANK": 18, - "GDP_MD_EST": 8721000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IN", - "ISO_A3": "IND", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -284, - 350 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ireland", - "NAME_LONG": "Ireland", - "ABBREV": "Ire.", - "FORMAL_EN": "Ireland", - "POP_EST": 5011102, - "POP_RANK": 13, - "GDP_MD_EST": 322000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IE", - "ISO_A3": "IRL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -6, - 351, - 352, - 353, - 354, - -59, - -34, - -58, - 355, - 356 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iran", - "NAME_LONG": "Iran", - "ABBREV": "Iran", - "FORMAL_EN": "Islamic Republic of Iran", - "POP_EST": 82021564, - "POP_RANK": 16, - "GDP_MD_EST": 1459000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IR", - "ISO_A3": "IRN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -354, - 357, - 358, - 359, - 360, - 361, - 362 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iraq", - "NAME_LONG": "Iraq", - "ABBREV": "Iraq", - "FORMAL_EN": "Republic of Iraq", - "POP_EST": 39192111, - "POP_RANK": 15, - "GDP_MD_EST": 596700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IQ", - "ISO_A3": "IRQ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 363 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Iceland", - "NAME_LONG": "Iceland", - "ABBREV": "Iceland", - "FORMAL_EN": "Republic of Iceland", - "POP_EST": 339747, - "POP_RANK": 10, - "GDP_MD_EST": 16150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IS", - "ISO_A3": "ISL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - 364, - 365, - 366, - 367, - 368, - 369, - -250 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Israel", - "NAME_LONG": "Israel", - "ABBREV": "Isr.", - "FORMAL_EN": "State of Israel", - "POP_EST": 8299706, - "POP_RANK": 13, - "GDP_MD_EST": 297000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IL", - "ISO_A3": "ISR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - -50, - 370, - 371, - -276, - -161 - ] - ], - [ - [ - 372 - ] - ], - [ - [ - 373 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Italy", - "NAME_LONG": "Italy", - "ABBREV": "Italy", - "FORMAL_EN": "Italian Republic", - "POP_EST": 62137802, - "POP_RANK": 16, - "GDP_MD_EST": 2221000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "IT", - "ISO_A3": "ITA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - 374 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jamaica", - "NAME_LONG": "Jamaica", - "ABBREV": "Jam.", - "FORMAL_EN": "Jamaica", - "POP_EST": 2990561, - "POP_RANK": 12, - "GDP_MD_EST": 25390, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JM", - "ISO_A3": "JAM", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -361, - 375, - 376, - -370, - 377, - -368, - 378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Jordan", - "NAME_LONG": "Jordan", - "ABBREV": "Jord.", - "FORMAL_EN": "Hashemite Kingdom of Jordan", - "POP_EST": 10248069, - "POP_RANK": 14, - "GDP_MD_EST": 86190, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JO", - "ISO_A3": "JOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - [ - 379 - ] - ], - [ - [ - 380 - ] - ], - [ - [ - 381 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Japan", - "NAME_LONG": "Japan", - "ABBREV": "Japan", - "FORMAL_EN": "Japan", - "POP_EST": 126451398, - "POP_RANK": 17, - "GDP_MD_EST": 4932000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "JP", - "ISO_A3": "JPN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -169, - 382, - 383, - 384, - 385, - 386 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kazakhstan", - "NAME_LONG": "Kazakhstan", - "ABBREV": "Kaz.", - "FORMAL_EN": "Republic of Kazakhstan", - "POP_EST": 18556698, - "POP_RANK": 14, - "GDP_MD_EST": 460700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KZ", - "ISO_A3": "KAZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -264, - 387, - 388, - 389, - 390, - 391 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kenya", - "NAME_LONG": "Kenya", - "ABBREV": "Ken.", - "FORMAL_EN": "Republic of Kenya", - "POP_EST": 47615739, - "POP_RANK": 15, - "GDP_MD_EST": 152700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KE", - "ISO_A3": "KEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -168, - 392, - 393, - -383 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kyrgyzstan", - "NAME_LONG": "Kyrgyzstan", - "ABBREV": "Kgz.", - "FORMAL_EN": "Kyrgyz Republic", - "POP_EST": 5789122, - "POP_RANK": 13, - "GDP_MD_EST": 21010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KG", - "ISO_A3": "KGZ", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - 394, - 395, - 396, - 397 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Cambodia", - "NAME_LONG": "Cambodia", - "ABBREV": "Camb.", - "FORMAL_EN": "Kingdom of Cambodia", - "POP_EST": 16204486, - "POP_RANK": 14, - "GDP_MD_EST": 58940, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KH", - "ISO_A3": "KHM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 398, - 399 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Korea", - "NAME_LONG": "Republic of Korea", - "ABBREV": "S.K.", - "FORMAL_EN": "Republic of Korea", - "POP_EST": 51181299, - "POP_RANK": 16, - "GDP_MD_EST": 1929000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KR", - "ISO_A3": "KOR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -17, - 400, - 401, - 402 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kosovo", - "NAME_LONG": "Kosovo", - "ABBREV": "Kos.", - "FORMAL_EN": "Republic of Kosovo", - "POP_EST": 1895250, - "POP_RANK": 12, - "GDP_MD_EST": 18490, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "XK", - "ISO_A3": "-99", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -359, - 403, - 404 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Kuwait", - "NAME_LONG": "Kuwait", - "ABBREV": "Kwt.", - "FORMAL_EN": "State of Kuwait", - "POP_EST": 2875422, - "POP_RANK": 12, - "GDP_MD_EST": 301100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "KW", - "ISO_A3": "KWT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -176, - 405, - -396, - 406, - 407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Laos", - "NAME_LONG": "Lao PDR", - "ABBREV": "Laos", - "FORMAL_EN": "Lao People's Democratic Republic", - "POP_EST": 7126706, - "POP_RANK": 13, - "GDP_MD_EST": 40960, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LA", - "ISO_A3": "LAO", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -366, - 408, - 409 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lebanon", - "NAME_LONG": "Lebanon", - "ABBREV": "Leb.", - "FORMAL_EN": "Lebanese Republic", - "POP_EST": 6229794, - "POP_RANK": 13, - "GDP_MD_EST": 85160, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LB", - "ISO_A3": "LBN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -186, - 410, - 411, - -292 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Liberia", - "NAME_LONG": "Liberia", - "ABBREV": "Liberia", - "FORMAL_EN": "Republic of Liberia", - "POP_EST": 4689021, - "POP_RANK": 12, - "GDP_MD_EST": 3881, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LR", - "ISO_A3": "LBR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -243, - 412, - 413, - -248, - 414, - 415, - 416 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Libya", - "NAME_LONG": "Libya", - "ABBREV": "Libya", - "FORMAL_EN": "Libya", - "POP_EST": 6653210, - "POP_RANK": 13, - "GDP_MD_EST": 90890, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LY", - "ISO_A3": "LBY", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 417 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sri Lanka", - "NAME_LONG": "Sri Lanka", - "ABBREV": "Sri L.", - "FORMAL_EN": "Democratic Socialist Republic of Sri Lanka", - "POP_EST": 22409381, - "POP_RANK": 15, - "GDP_MD_EST": 236700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LK", - "ISO_A3": "LKA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - 418 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lesotho", - "NAME_LONG": "Lesotho", - "ABBREV": "Les.", - "FORMAL_EN": "Kingdom of Lesotho", - "POP_EST": 1958042, - "POP_RANK": 12, - "GDP_MD_EST": 6019, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LS", - "ISO_A3": "LSO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -93, - 419, - 420, - 421, - 422 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Lithuania", - "NAME_LONG": "Lithuania", - "ABBREV": "Lith.", - "FORMAL_EN": "Republic of Lithuania", - "POP_EST": 2823859, - "POP_RANK": 12, - "GDP_MD_EST": 85620, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LT", - "ISO_A3": "LTU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -64, - -223, - -275 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Luxembourg", - "NAME_LONG": "Luxembourg", - "ABBREV": "Lux.", - "FORMAL_EN": "Grand Duchy of Luxembourg", - "POP_EST": 594130, - "POP_RANK": 11, - "GDP_MD_EST": 58740, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LU", - "ISO_A3": "LUX", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - -94, - -423, - 423, - -261, - 424 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Latvia", - "NAME_LONG": "Latvia", - "ABBREV": "Lat.", - "FORMAL_EN": "Republic of Latvia", - "POP_EST": 1944643, - "POP_RANK": 12, - "GDP_MD_EST": 50650, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "LV", - "ISO_A3": "LVA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -240, - 425, - 426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Morocco", - "NAME_LONG": "Morocco", - "ABBREV": "Mor.", - "FORMAL_EN": "Kingdom of Morocco", - "POP_EST": 33986655, - "POP_RANK": 15, - "GDP_MD_EST": 282800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MA", - "ISO_A3": "MAR", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - 427, - 428 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Moldova", - "NAME_LONG": "Moldova", - "ABBREV": "Mda.", - "FORMAL_EN": "Republic of Moldova", - "POP_EST": 3474121, - "POP_RANK": 12, - "GDP_MD_EST": 18540, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MD", - "ISO_A3": "MDA", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 429 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Madagascar", - "NAME_LONG": "Madagascar", - "ABBREV": "Mad.", - "FORMAL_EN": "Republic of Madagascar", - "POP_EST": 25054161, - "POP_RANK": 15, - "GDP_MD_EST": 36860, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MG", - "ISO_A3": "MDG", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -98, - -312, - 430, - 431, - 432 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mexico", - "NAME_LONG": "Mexico", - "ABBREV": "Mex.", - "FORMAL_EN": "United Mexican States", - "POP_EST": 124574795, - "POP_RANK": 17, - "GDP_MD_EST": 2307000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MX", - "ISO_A3": "MEX", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -18, - -403, - 433, - -85, - -303 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Macedonia", - "NAME_LONG": "Macedonia", - "ABBREV": "Mkd.", - "FORMAL_EN": "Former Yugoslav Republic of Macedonia", - "POP_EST": 2103721, - "POP_RANK": 12, - "GDP_MD_EST": 29520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MK", - "ISO_A3": "MKD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -76, - -188, - -297, - 434, - 435, - -237, - 436 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mali", - "NAME_LONG": "Mali", - "ABBREV": "Mali", - "FORMAL_EN": "Republic of Mali", - "POP_EST": 17885245, - "POP_RANK": 14, - "GDP_MD_EST": 38090, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ML", - "ISO_A3": "MLI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -78, - -350, - -177, - -408, - 437, - 438 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Myanmar", - "NAME_LONG": "Myanmar", - "ABBREV": "Myan.", - "FORMAL_EN": "Republic of the Union of Myanmar", - "POP_EST": 55123814, - "POP_RANK": 16, - "GDP_MD_EST": 311100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MM", - "ISO_A3": "MMR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -16, - 439, - -320, - -90, - 440, - -401 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Montenegro", - "NAME_LONG": "Montenegro", - "ABBREV": "Mont.", - "FORMAL_EN": "Montenegro", - "POP_EST": 642550, - "POP_RANK": 11, - "GDP_MD_EST": 10610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ME", - "ISO_A3": "MNE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -171, - 441 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mongolia", - "NAME_LONG": "Mongolia", - "ABBREV": "Mong.", - "FORMAL_EN": "Mongolia", - "POP_EST": 3068243, - "POP_RANK": 12, - "GDP_MD_EST": 37000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MN", - "ISO_A3": "MNG", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - 442, - 443, - 444, - 445, - 446, - 447, - 448, - 449 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mozambique", - "NAME_LONG": "Mozambique", - "ABBREV": "Moz.", - "FORMAL_EN": "Republic of Mozambique", - "POP_EST": 26573706, - "POP_RANK": 15, - "GDP_MD_EST": 35010, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MZ", - "ISO_A3": "MOZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -238, - -436, - 450, - 451, - 452 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Mauritania", - "NAME_LONG": "Mauritania", - "ABBREV": "Mrt.", - "FORMAL_EN": "Islamic Republic of Mauritania", - "POP_EST": 3758571, - "POP_RANK": 12, - "GDP_MD_EST": 16710, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MR", - "ISO_A3": "MRT", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -450, - 453, - 454 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Malawi", - "NAME_LONG": "Malawi", - "ABBREV": "Mal.", - "FORMAL_EN": "Republic of Malawi", - "POP_EST": 19196246, - "POP_RANK": 14, - "GDP_MD_EST": 21200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MW", - "ISO_A3": "MWI", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - [ - -115, - 455, - -344, - 456 - ] - ], - [ - [ - 457, - 458 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Malaysia", - "NAME_LONG": "Malaysia", - "ABBREV": "Malay.", - "FORMAL_EN": "Malaysia", - "POP_EST": 31381992, - "POP_RANK": 15, - "GDP_MD_EST": 863000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "MY", - "ISO_A3": "MYS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -7, - 459, - -119, - 460, - 461 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Namibia", - "NAME_LONG": "Namibia", - "ABBREV": "Nam.", - "FORMAL_EN": "Republic of Namibia", - "POP_EST": 2484780, - "POP_RANK": 12, - "GDP_MD_EST": 25990, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NA", - "ISO_A3": "NAM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - 462 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "New Caledonia", - "NAME_LONG": "New Caledonia", - "ABBREV": "New C.", - "FORMAL_EN": "New Caledonia", - "POP_EST": 279070, - "POP_RANK": 10, - "GDP_MD_EST": 10770, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NC", - "ISO_A3": "NCL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -71, - -77, - -437, - -244, - -417, - 463, - -194, - 464 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Niger", - "NAME_LONG": "Niger", - "ABBREV": "Niger", - "FORMAL_EN": "Republic of Niger", - "POP_EST": 19245344, - "POP_RANK": 14, - "GDP_MD_EST": 20150, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NE", - "ISO_A3": "NER", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -72, - -465, - -193, - 465 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nigeria", - "NAME_LONG": "Nigeria", - "ABBREV": "Nigeria", - "FORMAL_EN": "Federal Republic of Nigeria", - "POP_EST": 190632261, - "POP_RANK": 17, - "GDP_MD_EST": 1089000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NG", - "ISO_A3": "NGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -212, - 466, - -317, - 467 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nicaragua", - "NAME_LONG": "Nicaragua", - "ABBREV": "Nic.", - "FORMAL_EN": "Republic of Nicaragua", - "POP_EST": 6025951, - "POP_RANK": 13, - "GDP_MD_EST": 33550, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NI", - "ISO_A3": "NIC", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -67, - 468, - -224 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Netherlands", - "NAME_LONG": "Netherlands", - "ABBREV": "Neth.", - "FORMAL_EN": "Kingdom of the Netherlands", - "POP_EST": 17084719, - "POP_RANK": 14, - "GDP_MD_EST": 870800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NL", - "ISO_A3": "NLD", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Western Europe" - } - }, - { - "arcs": [ - [ - [ - -268, - 469, - 470, - 471 - ] - ], - [ - [ - 472 - ] - ], - [ - [ - 473 - ] - ], - [ - [ - 474 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Norway", - "NAME_LONG": "Norway", - "ABBREV": "Nor.", - "FORMAL_EN": "Kingdom of Norway", - "POP_EST": 5320045, - "POP_RANK": 13, - "GDP_MD_EST": 364700, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NO", - "ISO_A3": "NOR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -180, - -349 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Nepal", - "NAME_LONG": "Nepal", - "ABBREV": "Nepal", - "FORMAL_EN": "Nepal", - "POP_EST": 29384297, - "POP_RANK": 15, - "GDP_MD_EST": 71520, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NP", - "ISO_A3": "NPL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - [ - 475 - ] - ], - [ - [ - 476 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "New Zealand", - "NAME_LONG": "New Zealand", - "ABBREV": "N.Z.", - "FORMAL_EN": "New Zealand", - "POP_EST": 4510327, - "POP_RANK": 12, - "GDP_MD_EST": 174800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "NZ", - "ISO_A3": "NZL", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Australia and New Zealand" - } - }, - { - "arcs": [ - [ - [ - -20, - 477 - ] - ], - [ - [ - -22, - 478, - 479, - 480 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Oman", - "NAME_LONG": "Oman", - "ABBREV": "Oman", - "FORMAL_EN": "Sultanate of Oman", - "POP_EST": 3424386, - "POP_RANK": 12, - "GDP_MD_EST": 173100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "OM", - "ISO_A3": "OMN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -5, - -182, - -348, - 481, - -352 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Pakistan", - "NAME_LONG": "Pakistan", - "ABBREV": "Pak.", - "FORMAL_EN": "Islamic Republic of Pakistan", - "POP_EST": 204924861, - "POP_RANK": 17, - "GDP_MD_EST": 988200, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PK", - "ISO_A3": "PAK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Southern Asia" - } - }, - { - "arcs": [ - [ - -208, - 482, - -214, - 483 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Panama", - "NAME_LONG": "Panama", - "ABBREV": "Pan.", - "FORMAL_EN": "Republic of Panama", - "POP_EST": 3753142, - "POP_RANK": 12, - "GDP_MD_EST": 93120, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PA", - "ISO_A3": "PAN", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -102, - -166, - 484, - -245, - -205, - -106 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Peru", - "NAME_LONG": "Peru", - "ABBREV": "Peru", - "FORMAL_EN": "Republic of Peru", - "POP_EST": 31036656, - "POP_RANK": 15, - "GDP_MD_EST": 410400, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PE", - "ISO_A3": "PER", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - 485 - ] - ], - [ - [ - 486 - ] - ], - [ - [ - 487 - ] - ], - [ - [ - 488 - ] - ], - [ - [ - 489 - ] - ], - [ - [ - 490 - ] - ], - [ - [ - 491 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Philippines", - "NAME_LONG": "Philippines", - "ABBREV": "Phil.", - "FORMAL_EN": "Republic of the Philippines", - "POP_EST": 104256076, - "POP_RANK": 17, - "GDP_MD_EST": 801900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PH", - "ISO_A3": "PHL", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - -340, - 492 - ] - ], - [ - [ - 493 - ] - ], - [ - [ - 494 - ] - ], - [ - [ - 495 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Papua New Guinea", - "NAME_LONG": "Papua New Guinea", - "ABBREV": "P.N.G.", - "FORMAL_EN": "Independent State of Papua New Guinea", - "POP_EST": 6909701, - "POP_RANK": 13, - "GDP_MD_EST": 28020, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PG", - "ISO_A3": "PNG", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -97, - 496, - 497, - -220, - -228, - 498, - 499, - -420 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Poland", - "NAME_LONG": "Poland", - "ABBREV": "Pol.", - "FORMAL_EN": "Republic of Poland", - "POP_EST": 38476269, - "POP_RANK": 15, - "GDP_MD_EST": 1052000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PL", - "ISO_A3": "POL", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - 500 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Puerto Rico", - "NAME_LONG": "Puerto Rico", - "ABBREV": "P.R.", - "FORMAL_EN": "Commonwealth of Puerto Rico", - "POP_EST": 3351827, - "POP_RANK": 12, - "GDP_MD_EST": 131000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PR", - "ISO_A3": "PRI", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -173, - 501, - 502, - -400, - 503 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "North Korea", - "NAME_LONG": "Dem. Rep. Korea", - "ABBREV": "N.K.", - "FORMAL_EN": "Democratic People's Republic of Korea", - "POP_EST": 25248140, - "POP_RANK": 15, - "GDP_MD_EST": 40000, - "POP_YEAR": 2013, - "GDP_YEAR": 2016, - "ISO_A2": "KP", - "ISO_A3": "PRK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -258, - 504 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Portugal", - "NAME_LONG": "Portugal", - "ABBREV": "Port.", - "FORMAL_EN": "Portuguese Republic", - "POP_EST": 10839514, - "POP_RANK": 14, - "GDP_MD_EST": 297100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PT", - "ISO_A3": "PRT", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -28, - -104, - -105 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Paraguay", - "NAME_LONG": "Paraguay", - "ABBREV": "Para.", - "FORMAL_EN": "Republic of Paraguay", - "POP_EST": 6943739, - "POP_RANK": 13, - "GDP_MD_EST": 64670, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PY", - "ISO_A3": "PRY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -369, - -378 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Palestine", - "NAME_LONG": "Palestine", - "ABBREV": "Pal.", - "FORMAL_EN": "West Bank and Gaza", - "POP_EST": 4543126, - "POP_RANK": 12, - "GDP_MD_EST": 21220.77, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "PS", - "ISO_A3": "PSE", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 505, - 506 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Qatar", - "NAME_LONG": "Qatar", - "ABBREV": "Qatar", - "FORMAL_EN": "State of Qatar", - "POP_EST": 2314307, - "POP_RANK": 12, - "GDP_MD_EST": 334500, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "QA", - "ISO_A3": "QAT", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -81, - 507, - -328, - 508, - -429, - 509, - 510 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Romania", - "NAME_LONG": "Romania", - "ABBREV": "Rom.", - "FORMAL_EN": "Romania", - "POP_EST": 21529967, - "POP_RANK": 15, - "GDP_MD_EST": 441000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RO", - "ISO_A3": "ROU", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - [ - -56, - -289, - 511, - 512, - -95, - -425, - -260, - 513, - -269, - -472, - 514, - -502, - -172, - -442, - -170, - -387, - 515 - ] - ], - [ - [ - -421, - -500, - 516 - ] - ], - [ - [ - 519 - ] - ], - [ - [ - 520 - ] - ], - [ - [ - 521 - ] - ], - [ - [ - 522 - ] - ], - [ - [ - 523 - ] - ], - [ - [ - 524 - ] - ], - [ - [ - 525 - ] - ], - [ - [ - 526 - ] - ], - [ - [ - 527 - ] - ], - [ - [ - 528 - ] - ], - [ - [ - 529 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Russia", - "NAME_LONG": "Russian Federation", - "ABBREV": "Rus.", - "FORMAL_EN": "Russian Federation", - "POP_EST": 142257519, - "POP_RANK": 17, - "GDP_MD_EST": 3745000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RU", - "ISO_A3": "RUS", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -61, - -200, - 530, - 531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Rwanda", - "NAME_LONG": "Rwanda", - "ABBREV": "Rwa.", - "FORMAL_EN": "Republic of Rwanda", - "POP_EST": 11901484, - "POP_RANK": 14, - "GDP_MD_EST": 21970, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RW", - "ISO_A3": "RWA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -239, - -453, - 532, - -426 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "W. Sahara", - "NAME_LONG": "Western Sahara", - "ABBREV": "W. Sah.", - "FORMAL_EN": "Sahrawi Arab Democratic Republic", - "POP_EST": 603253, - "POP_RANK": 11, - "GDP_MD_EST": 906.5, - "POP_YEAR": 2017, - "GDP_YEAR": 2007, - "ISO_A2": "EH", - "ISO_A3": "ESH", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -23, - -481, - 533, - 534, - -376, - -360, - -405, - 535, - -507, - 536 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Saudi Arabia", - "NAME_LONG": "Saudi Arabia", - "ABBREV": "Saud.", - "FORMAL_EN": "Kingdom of Saudi Arabia", - "POP_EST": 28571770, - "POP_RANK": 15, - "GDP_MD_EST": 1731000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SA", - "ISO_A3": "SAU", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -123, - 537, - -415, - -247, - 538, - -253, - -266, - 539 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sudan", - "NAME_LONG": "Sudan", - "ABBREV": "Sudan", - "FORMAL_EN": "Republic of the Sudan", - "POP_EST": 37345935, - "POP_RANK": 15, - "GDP_MD_EST": 176300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SD", - "ISO_A3": "SDN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - -124, - -540, - -265, - -392, - 540, - -198 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "S. Sudan", - "NAME_LONG": "South Sudan", - "ABBREV": "S. Sud.", - "FORMAL_EN": "Republic of South Sudan", - "POP_EST": 13026129, - "POP_RANK": 14, - "GDP_MD_EST": 20880, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SS", - "ISO_A3": "SSD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -296, - -301, - 541, - -299, - 542, - -451, - -435 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Senegal", - "NAME_LONG": "Senegal", - "ABBREV": "Sen.", - "FORMAL_EN": "Republic of Senegal", - "POP_EST": 14668522, - "POP_RANK": 14, - "GDP_MD_EST": 39720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SN", - "ISO_A3": "SEN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - [ - 543 - ] - ], - [ - [ - 544 - ] - ], - [ - [ - 545 - ] - ], - [ - [ - 546 - ] - ], - [ - [ - 547 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Solomon Is.", - "NAME_LONG": "Solomon Islands", - "ABBREV": "S. Is.", - "FORMAL_EN": "", - "POP_EST": 647581, - "POP_RANK": 11, - "GDP_MD_EST": 1198, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SB", - "ISO_A3": "SLB", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -293, - -412, - 548 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sierra Leone", - "NAME_LONG": "Sierra Leone", - "ABBREV": "S.L.", - "FORMAL_EN": "Republic of Sierra Leone", - "POP_EST": 6163195, - "POP_RANK": 13, - "GDP_MD_EST": 10640, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SL", - "ISO_A3": "SLE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -310, - -319, - 549 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "El Salvador", - "NAME_LONG": "El Salvador", - "ABBREV": "El. S.", - "FORMAL_EN": "Republic of El Salvador", - "POP_EST": 6172011, - "POP_RANK": 13, - "GDP_MD_EST": 54790, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SV", - "ISO_A3": "SLV", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Central America" - } - }, - { - "arcs": [ - [ - -230, - 550, - 551, - -262 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somaliland", - "NAME_LONG": "Somaliland", - "ABBREV": "Solnd.", - "FORMAL_EN": "Republic of Somaliland", - "POP_EST": 3500000, - "POP_RANK": 12, - "GDP_MD_EST": 12250, - "POP_YEAR": 2013, - "GDP_YEAR": 2013, - "ISO_A2": "-99", - "ISO_A3": "-99", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -263, - -552, - 552, - -388 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Somalia", - "NAME_LONG": "Somalia", - "ABBREV": "Som.", - "FORMAL_EN": "Federal Republic of Somalia", - "POP_EST": 7531386, - "POP_RANK": 13, - "GDP_MD_EST": 4719, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SO", - "ISO_A3": "SOM", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -86, - -434, - -402, - -441, - -92, - -324, - -329, - -508 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Serbia", - "NAME_LONG": "Serbia", - "ABBREV": "Serb.", - "FORMAL_EN": "Republic of Serbia", - "POP_EST": 7111024, - "POP_RANK": 13, - "GDP_MD_EST": 101800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "RS", - "ISO_A3": "SRB", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -110, - -315, - 553, - -279 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Suriname", - "NAME_LONG": "Suriname", - "ABBREV": "Sur.", - "FORMAL_EN": "Republic of Suriname", - "POP_EST": 591919, - "POP_RANK": 11, - "GDP_MD_EST": 8547, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SR", - "ISO_A3": "SUR", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -54, - -221, - -498, - 554, - -326 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovakia", - "NAME_LONG": "Slovakia", - "ABBREV": "Svk.", - "FORMAL_EN": "Slovak Republic", - "POP_EST": 5445829, - "POP_RANK": 13, - "GDP_MD_EST": 168800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SK", - "ISO_A3": "SVK", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -49, - -330, - -322, - 555, - -371 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Slovenia", - "NAME_LONG": "Slovenia", - "ABBREV": "Slo.", - "FORMAL_EN": "Republic of Slovenia", - "POP_EST": 1972126, - "POP_RANK": 12, - "GDP_MD_EST": 68350, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SI", - "ISO_A3": "SVN", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Southern Europe" - } - }, - { - "arcs": [ - [ - -267, - 556, - -470 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Sweden", - "NAME_LONG": "Sweden", - "ABBREV": "Swe.", - "FORMAL_EN": "Kingdom of Sweden", - "POP_EST": 9960487, - "POP_RANK": 13, - "GDP_MD_EST": 498100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SE", - "ISO_A3": "SWE", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Northern Europe" - } - }, - { - "arcs": [ - [ - -446, - 557 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Swaziland", - "NAME_LONG": "Swaziland", - "ABBREV": "Swz.", - "FORMAL_EN": "Kingdom of Swaziland", - "POP_EST": 1467152, - "POP_RANK": 12, - "GDP_MD_EST": 11060, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "SZ", - "ISO_A3": "SWZ", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -362, - -379, - -367, - -410, - 558, - 559 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Syria", - "NAME_LONG": "Syria", - "ABBREV": "Syria", - "FORMAL_EN": "Syrian Arab Republic", - "POP_EST": 18028549, - "POP_RANK": 14, - "GDP_MD_EST": 50280, - "POP_YEAR": 2017, - "GDP_YEAR": 2015, - "ISO_A2": "SY", - "ISO_A3": "SYR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -122, - -195, - -464, - -416, - -538 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Chad", - "NAME_LONG": "Chad", - "ABBREV": "Chad", - "FORMAL_EN": "Republic of Chad", - "POP_EST": 12075985, - "POP_RANK": 14, - "GDP_MD_EST": 30590, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TD", - "ISO_A3": "TCD", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Middle Africa" - } - }, - { - "arcs": [ - [ - -69, - 560, - -290, - -73 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Togo", - "NAME_LONG": "Togo", - "ABBREV": "Togo", - "FORMAL_EN": "Togolese Republic", - "POP_EST": 7965055, - "POP_RANK": 13, - "GDP_MD_EST": 11610, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TG", - "ISO_A3": "TGO", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Western Africa" - } - }, - { - "arcs": [ - [ - -395, - 561, - -459, - 562, - -438, - -407 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Thailand", - "NAME_LONG": "Thailand", - "ABBREV": "Thai.", - "FORMAL_EN": "Kingdom of Thailand", - "POP_EST": 68414135, - "POP_RANK": 16, - "GDP_MD_EST": 1161000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TH", - "ISO_A3": "THA", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - -3, - 563, - -393, - -167 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tajikistan", - "NAME_LONG": "Tajikistan", - "ABBREV": "Tjk.", - "FORMAL_EN": "Republic of Tajikistan", - "POP_EST": 8468555, - "POP_RANK": 13, - "GDP_MD_EST": 25810, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TJ", - "ISO_A3": "TJK", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -1, - -357, - 564, - -385, - 565 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Turkmenistan", - "NAME_LONG": "Turkmenistan", - "ABBREV": "Turkm.", - "FORMAL_EN": "Turkmenistan", - "POP_EST": 5351277, - "POP_RANK": 13, - "GDP_MD_EST": 94720, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TM", - "ISO_A3": "TKM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -332, - 566 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Timor-Leste", - "NAME_LONG": "Timor-Leste", - "ABBREV": "T.L.", - "FORMAL_EN": "Democratic Republic of Timor-Leste", - "POP_EST": 1291358, - "POP_RANK": 12, - "GDP_MD_EST": 4975, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TL", - "ISO_A3": "TLS", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - 567 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Trinidad and Tobago", - "NAME_LONG": "Trinidad and Tobago", - "ABBREV": "Tr.T.", - "FORMAL_EN": "Republic of Trinidad and Tobago", - "POP_EST": 1218208, - "POP_RANK": 12, - "GDP_MD_EST": 43570, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TT", - "ISO_A3": "TTO", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Caribbean" - } - }, - { - "arcs": [ - [ - -242, - 568, - -413 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tunisia", - "NAME_LONG": "Tunisia", - "ABBREV": "Tun.", - "FORMAL_EN": "Republic of Tunisia", - "POP_EST": 11403800, - "POP_RANK": 14, - "GDP_MD_EST": 130800, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TN", - "ISO_A3": "TUN", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Northern Africa" - } - }, - { - "arcs": [ - [ - [ - -36, - -355, - -363, - -560, - 569, - -287 - ] - ], - [ - [ - -83, - 570, - -304 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Turkey", - "NAME_LONG": "Turkey", - "ABBREV": "Tur.", - "FORMAL_EN": "Republic of Turkey", - "POP_EST": 80845215, - "POP_RANK": 16, - "GDP_MD_EST": 1670000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TR", - "ISO_A3": "TUR", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - 571 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Taiwan", - "NAME_LONG": "Taiwan", - "ABBREV": "Taiwan", - "FORMAL_EN": "", - "POP_EST": 23508428, - "POP_RANK": 15, - "GDP_MD_EST": 1127000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TW", - "ISO_A3": "TWN", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Eastern Asia" - } - }, - { - "arcs": [ - [ - -62, - -532, - 572, - -390, - 573, - -443, - -455, - 574, - -201 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Tanzania", - "NAME_LONG": "Tanzania", - "ABBREV": "Tanz.", - "FORMAL_EN": "United Republic of Tanzania", - "POP_EST": 53950935, - "POP_RANK": 16, - "GDP_MD_EST": 150600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "TZ", - "ISO_A3": "TZA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -199, - -541, - -391, - -573, - -531 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uganda", - "NAME_LONG": "Uganda", - "ABBREV": "Uga.", - "FORMAL_EN": "Republic of Uganda", - "POP_EST": 39570125, - "POP_RANK": 15, - "GDP_MD_EST": 84930, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UG", - "ISO_A3": "UGA", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -96, - -513, - 575, - -518, - 576, - -510, - -428, - -509, - -327, - -555, - -497 - ], - [ - 517, - 518 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Ukraine", - "NAME_LONG": "Ukraine", - "ABBREV": "Ukr.", - "FORMAL_EN": "Ukraine", - "POP_EST": 44033874, - "POP_RANK": 15, - "GDP_MD_EST": 352600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UA", - "ISO_A3": "UKR", - "CONTINENT": "Europe", - "REGION_UN": "Europe", - "SUBREGION": "Eastern Europe" - } - }, - { - "arcs": [ - [ - -30, - -113, - 577 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uruguay", - "NAME_LONG": "Uruguay", - "ABBREV": "Ury.", - "FORMAL_EN": "Oriental Republic of Uruguay", - "POP_EST": 3360148, - "POP_RANK": 12, - "GDP_MD_EST": 73250, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UY", - "ISO_A3": "URY", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - [ - -141, - 578, - -432, - 579 - ] - ], - [ - [ - -139, - 580 - ] - ], - [ - [ - 581 - ] - ], - [ - [ - 582 - ] - ], - [ - [ - 583 - ] - ], - [ - [ - 584 - ] - ], - [ - [ - 585 - ] - ], - [ - [ - 586 - ] - ], - [ - [ - 587 - ] - ], - [ - [ - 588 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "United States of America", - "NAME_LONG": "United States", - "ABBREV": "U.S.A.", - "FORMAL_EN": "United States of America", - "POP_EST": 326625791, - "POP_RANK": 17, - "GDP_MD_EST": 18560000, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "US", - "ISO_A3": "USA", - "CONTINENT": "North America", - "REGION_UN": "Americas", - "SUBREGION": "Northern America" - } - }, - { - "arcs": [ - [ - -2, - -566, - -384, - -394, - -564 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Uzbekistan", - "NAME_LONG": "Uzbekistan", - "ABBREV": "Uzb.", - "FORMAL_EN": "Republic of Uzbekistan", - "POP_EST": 29748859, - "POP_RANK": 15, - "GDP_MD_EST": 202300, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "UZ", - "ISO_A3": "UZB", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Central Asia" - } - }, - { - "arcs": [ - [ - -108, - -210, - 589, - -313 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Venezuela", - "NAME_LONG": "Venezuela", - "ABBREV": "Ven.", - "FORMAL_EN": "Bolivarian Republic of Venezuela", - "POP_EST": 31304016, - "POP_RANK": 15, - "GDP_MD_EST": 468600, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VE", - "ISO_A3": "VEN", - "CONTINENT": "South America", - "REGION_UN": "Americas", - "SUBREGION": "South America" - } - }, - { - "arcs": [ - [ - -175, - 590, - -397, - -406 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Vietnam", - "NAME_LONG": "Vietnam", - "ABBREV": "Viet.", - "FORMAL_EN": "Socialist Republic of Vietnam", - "POP_EST": 96160163, - "POP_RANK": 16, - "GDP_MD_EST": 594900, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VN", - "ISO_A3": "VNM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "South-Eastern Asia" - } - }, - { - "arcs": [ - [ - [ - 591 - ] - ], - [ - [ - 592 - ] - ] - ], - "type": "MultiPolygon", - "properties": { - "NAME": "Vanuatu", - "NAME_LONG": "Vanuatu", - "ABBREV": "Van.", - "FORMAL_EN": "Republic of Vanuatu", - "POP_EST": 282814, - "POP_RANK": 10, - "GDP_MD_EST": 723, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "VU", - "ISO_A3": "VUT", - "CONTINENT": "Oceania", - "REGION_UN": "Oceania", - "SUBREGION": "Melanesia" - } - }, - { - "arcs": [ - [ - -480, - 593, - -534 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Yemen", - "NAME_LONG": "Yemen", - "ABBREV": "Yem.", - "FORMAL_EN": "Republic of Yemen", - "POP_EST": 28036829, - "POP_RANK": 15, - "GDP_MD_EST": 73450, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "YE", - "ISO_A3": "YEM", - "CONTINENT": "Asia", - "REGION_UN": "Asia", - "SUBREGION": "Western Asia" - } - }, - { - "arcs": [ - [ - -118, - 594, - -447, - -558, - -445, - 595, - -461 - ], - [ - -419 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "South Africa", - "NAME_LONG": "South Africa", - "ABBREV": "S.Af.", - "FORMAL_EN": "Republic of South Africa", - "POP_EST": 54841552, - "POP_RANK": 16, - "GDP_MD_EST": 739100, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZA", - "ISO_A3": "ZAF", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Southern Africa" - } - }, - { - "arcs": [ - [ - -10, - -202, - -575, - -454, - -449, - 596, - -120, - -460 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zambia", - "NAME_LONG": "Zambia", - "ABBREV": "Zambia", - "FORMAL_EN": "Republic of Zambia", - "POP_EST": 15972000, - "POP_RANK": 14, - "GDP_MD_EST": 65170, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZM", - "ISO_A3": "ZMB", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - }, - { - "arcs": [ - [ - -121, - -597, - -448, - -595 - ] - ], - "type": "Polygon", - "properties": { - "NAME": "Zimbabwe", - "NAME_LONG": "Zimbabwe", - "ABBREV": "Zimb.", - "FORMAL_EN": "Republic of Zimbabwe", - "POP_EST": 13805084, - "POP_RANK": 14, - "GDP_MD_EST": 28330, - "POP_YEAR": 2017, - "GDP_YEAR": 2016, - "ISO_A2": "ZW", - "ISO_A3": "ZWE", - "CONTINENT": "Africa", - "REGION_UN": "Africa", - "SUBREGION": "Eastern Africa" - } - } - ] - } - } -} diff --git a/explorer/src/components/ComponentError.tsx b/explorer/src/components/ComponentError.tsx deleted file mode 100644 index 00b448fb9e..0000000000 --- a/explorer/src/components/ComponentError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; - -export const ComponentError: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - sx={{ marginTop: 2, color: 'primary.main', fontSize: 10 }} - variant="body1" - data-testid="delegation-total-amount" - > - {text} - </Typography> -); diff --git a/explorer/src/components/ContentCard.tsx b/explorer/src/components/ContentCard.tsx deleted file mode 100644 index c78c719ca7..0000000000 --- a/explorer/src/components/ContentCard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Card, CardHeader, CardContent, Typography } from '@mui/material'; -import React, { ReactEventHandler } from 'react'; - -type ContentCardProps = { - title?: React.ReactNode; - subtitle?: string; - Icon?: React.ReactNode; - Action?: React.ReactNode; - errorMsg?: string; - onClick?: ReactEventHandler; -}; - -export const ContentCard: FCWithChildren<ContentCardProps> = ({ - title, - Icon, - Action, - subtitle, - errorMsg, - children, - onClick, -}) => ( - <Card onClick={onClick} sx={{ height: '100%' }}> - {title && <CardHeader title={title || ''} avatar={Icon} action={Action} subheader={subtitle} />} - {children && <CardContent>{children}</CardContent>} - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {errorMsg} - </Typography> - )} - </Card> -); - -ContentCard.defaultProps = { - title: undefined, - subtitle: undefined, - Icon: null, - Action: null, - errorMsg: undefined, - onClick: () => null, -}; diff --git a/explorer/src/components/CustomColumnHeading.tsx b/explorer/src/components/CustomColumnHeading.tsx deleted file mode 100644 index 1f85806fa2..0000000000 --- a/explorer/src/components/CustomColumnHeading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; - -export const CustomColumnHeading: FCWithChildren<{ headingTitle: string; tooltipInfo?: string }> = ({ - headingTitle, - tooltipInfo, -}) => { - const theme = useTheme(); - - return ( - <Box alignItems="center" display="flex"> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={headingTitle} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - <Typography variant="body2" fontWeight={600} data-testid={headingTitle}> - {headingTitle} - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Delegations/ConfirmationModal.tsx b/explorer/src/components/Delegations/ConfirmationModal.tsx deleted file mode 100644 index 8e859f5eba..0000000000 --- a/explorer/src/components/Delegations/ConfirmationModal.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Button, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - onConfirm: () => void; - onClose?: () => void; - children?: React.ReactNode; - title: React.ReactNode | string; - subTitle?: React.ReactNode | string; - confirmButton: React.ReactNode | string; - disabled?: boolean; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; -} - -export const ConfirmationModal = ({ - open, - onConfirm, - onClose, - children, - title, - subTitle, - confirmButton, - disabled, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - <DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}> - {title} - {subTitle && - (typeof subTitle === 'string' ? ( - <Typography fontWeight={400} variant="subtitle1" fontSize={12} color="grey"> - {subTitle} - </Typography> - ) : ( - subTitle - ))} - </DialogTitle> - ); - const ConfirmButton = - typeof confirmButton === 'string' ? ( - <Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}> - <Typography variant="button" fontSize="large"> - {confirmButton} - </Typography> - </Button> - ) : ( - confirmButton - ); - return ( - <Dialog - open={open} - onClose={onClose} - aria-labelledby="responsive-dialog-title" - maxWidth={maxWidth || 'sm'} - sx={{ textAlign: 'center', ...sx }} - fullWidth={fullWidth} - BackdropProps={backdropProps} - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} - > - {Title} - <DialogContent>{children}</DialogContent> - <DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions> - </Dialog> - ); -}; diff --git a/explorer/src/components/Delegations/DelegateIconButton.tsx b/explorer/src/components/Delegations/DelegateIconButton.tsx deleted file mode 100644 index 36b581a010..0000000000 --- a/explorer/src/components/Delegations/DelegateIconButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { SxProps } from '@mui/system'; -import { useIsMobile } from '@src/hooks'; -import { DelegateIcon } from '@src/icons/DelevateSVG'; - -export const DelegateIconButton: FCWithChildren<{ - size?: 'small' | 'medium'; - disabled?: boolean; - tooltip?: React.ReactNode; - sx?: SxProps; - onDelegate: () => void; -}> = ({ onDelegate, sx, disabled, size = 'medium' }) => { - const isMobile = useIsMobile(); - - const handleOnDelegate = () => { - onDelegate(); - }; - - if (isMobile) { - return ( - <IconButton size="small" disabled={disabled} onClick={handleOnDelegate}> - <DelegateIcon fontSize="small" /> - </IconButton> - ); - } - - return ( - <Button variant="outlined" size={size} disabled={disabled} onClick={handleOnDelegate} sx={sx}> - Delegate - </Button> - ); -}; diff --git a/explorer/src/components/Delegations/DelegateModal.tsx b/explorer/src/components/Delegations/DelegateModal.tsx deleted file mode 100644 index 3bafd1888f..0000000000 --- a/explorer/src/components/Delegations/DelegateModal.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState } from 'react'; -import { Box, SxProps } from '@mui/material'; -import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { CurrencyDenom, DecCoin } from '@nymproject/types'; -import { useWalletContext } from '@src/context/wallet'; -import { useDelegationsContext } from '@src/context/delegations'; -import { SimpleModal } from './SimpleModal'; -import { ModalListItem } from './ModalListItem'; -import { DelegationModalProps } from './DelegationModal'; -import { validateAmount } from '../../utils/currency'; -import { urls } from '../../utils'; - -const MIN_AMOUNT_TO_DELEGATE = 10; - -export const DelegateModal: FCWithChildren<{ - mixId: number; - identityKey: string; - header?: string; - buttonText?: string; - rewardInterval?: string; - estimatedReward?: number; - profitMarginPercentage?: string | null; - nodeUptimePercentage?: number | null; - denom: CurrencyDenom; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - onOk?: (delegationModalProps: DelegationModalProps) => void; -}> = ({ mixId, identityKey, onClose, onOk, denom, sx }) => { - const [amount, setAmount] = useState<DecCoin | undefined>({ amount: '10', denom: 'nym' }); - const [isValidated, setValidated] = useState<boolean>(false); - const [errorAmount, setErrorAmount] = useState<string | undefined>(); - - const { address, balance } = useWalletContext(); - const { handleDelegate } = useDelegationsContext(); - - const validate = async () => { - let newValidatedValue = true; - let errorAmountMessage; - - if (amount && !(await validateAmount(amount.amount, '0'))) { - newValidatedValue = false; - errorAmountMessage = 'Please enter a valid amount'; - } - - if (amount && +amount.amount < MIN_AMOUNT_TO_DELEGATE) { - errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`; - newValidatedValue = false; - } - - if (!amount?.amount.length) { - newValidatedValue = false; - } - - if (amount && balance.data && +balance.data - +amount.amount <= 0) { - errorAmountMessage = 'Not enough funds'; - newValidatedValue = false; - } - - setErrorAmount(errorAmountMessage); - setValidated(newValidatedValue); - }; - - const delegateToMixnode = async ({ - delegationMixId, - delegationAmount, - }: { - delegationMixId: number; - delegationAmount: string; - }) => { - try { - const tx = await handleDelegate(delegationMixId, delegationAmount); - return tx; - } catch (e) { - console.error('Failed to delegate to mixnode', e); - throw e; - } - }; - - const handleConfirm = async () => { - if (mixId && amount && onOk) { - onOk({ - status: 'loading', - }); - try { - if (!address) { - throw new Error('Please connect your wallet'); - } - - const tx = await delegateToMixnode({ - delegationMixId: mixId, - delegationAmount: amount.amount, - }); - - if (!tx) { - throw new Error('Failed to delegate'); - } - - onOk({ - status: 'success', - message: 'Delegation can take up to one hour to process', - transactions: [ - { url: `${urls('MAINNET').blockExplorer}/tx/${tx.transactionHash}`, hash: tx.transactionHash }, - ], - }); - } catch (e) { - console.error('Failed to delegate', e); - onOk({ - status: 'error', - message: (e as Error).message, - }); - } - } - }; - - const handleAmountChanged = (newAmount: DecCoin) => { - setAmount(newAmount); - }; - - React.useEffect(() => { - validate(); - }, [amount, identityKey, mixId]); - - return ( - <SimpleModal - open - onClose={onClose} - onOk={handleConfirm} - header="Delegate" - okLabel="Delegate" - okDisabled={!isValidated} - sx={sx} - > - <Box sx={{ mt: 3 }} gap={2}> - <IdentityKeyFormField - required - fullWidth - label="Node identity key" - onChanged={() => undefined} - initialValue={identityKey} - readOnly - showTickOnValid={false} - /> - </Box> - - <Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}> - <CurrencyFormField - required - fullWidth - autoFocus - label="Amount" - initialValue={amount?.amount || '10'} - onChanged={handleAmountChanged} - denom={denom} - validationError={errorAmount} - /> - </Box> - <Box sx={{ mt: 3 }}> - <ModalListItem label="Account balance" value={`${balance.data} NYM`} divider fontWeight={600} /> - </Box> - - <ModalListItem label="Est. fee for this transaction will be calculated in your connected wallet" /> - </SimpleModal> - ); -}; diff --git a/explorer/src/components/Delegations/DelegationModal.tsx b/explorer/src/components/Delegations/DelegationModal.tsx deleted file mode 100644 index db5b51aefe..0000000000 --- a/explorer/src/components/Delegations/DelegationModal.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Typography, SxProps, Stack } from '@mui/material'; -import { Link } from '@nymproject/react/link/Link'; -import { LoadingModal } from './LoadingModal'; -import { ConfirmationModal } from './ConfirmationModal'; -import { ErrorModal } from './ErrorModal'; - -export type DelegationModalProps = { - status: 'loading' | 'success' | 'error' | 'info'; - message?: string; - transactions?: { - url: string; - hash: string; - }[]; -}; - -export const DelegationModal: FCWithChildren< - DelegationModalProps & { - open: boolean; - onClose: () => void; - sx?: SxProps; - backdropProps?: object; - children?: React.ReactNode; - } -> = ({ status, message, transactions, open, onClose, children, sx, backdropProps }) => { - if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />; - - if (status === 'error') { - return ( - <ErrorModal message={message} sx={sx} open={open} onClose={onClose}> - {children} - </ErrorModal> - ); - } - - if (status === 'info') { - return ( - <ConfirmationModal open={open} title="Connect wallet" confirmButton="OK" onConfirm={onClose}> - <Typography>{message}</Typography> - </ConfirmationModal> - ); - } - - return ( - <ConfirmationModal - open={open} - onConfirm={onClose || (() => {})} - title="Transaction successful" - confirmButton="Done" - > - <Stack alignItems="center" spacing={2} mb={0}> - {message && <Typography>{message}</Typography>} - {transactions?.length === 1 && ( - <Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon /> - )} - {transactions && transactions.length > 1 && ( - <Stack alignItems="center" spacing={1}> - <Typography>View the transactions on blockchain:</Typography> - {transactions.map(({ url, hash }) => ( - <Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon /> - ))} - </Stack> - )} - </Stack> - </ConfirmationModal> - ); -}; diff --git a/explorer/src/components/Delegations/ErrorModal.tsx b/explorer/src/components/Delegations/ErrorModal.tsx deleted file mode 100644 index b9218d1e1e..0000000000 --- a/explorer/src/components/Delegations/ErrorModal.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const ErrorModal: FCWithChildren<{ - open: boolean; - title?: string; - message?: string; - sx?: SxProps; - backdropProps?: object; - onClose: () => void; - children?: React.ReactNode; -}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( - <Modal open={open} onClose={onClose} BackdropProps={backdropProps}> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Typography color={(theme) => theme.palette.error.main} mb={1}> - {title || 'Oh no! Something went wrong...'} - </Typography> - <Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}> - {message} - </Typography> - {children} - <Button variant="contained" onClick={onClose}> - Close - </Button> - </Box> - </Modal> -); diff --git a/explorer/src/components/Delegations/LoadingModal.tsx b/explorer/src/components/Delegations/LoadingModal.tsx deleted file mode 100644 index eda13938d4..0000000000 --- a/explorer/src/components/Delegations/LoadingModal.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; -import { modalStyle } from './SimpleModal'; - -export const LoadingModal: FCWithChildren<{ - text?: string; - sx?: SxProps; - backdropProps?: object; -}> = ({ sx, text = 'Please wait...' }) => ( - <Modal open> - <Box sx={{ ...modalStyle(), ...sx }} textAlign="center"> - <Stack spacing={4} direction="row" alignItems="center"> - <CircularProgress /> - <Typography sx={{ color: 'text.primary' }}>{text}</Typography> - </Stack> - </Box> - </Modal> -); diff --git a/explorer/src/components/Delegations/ModalDivider.tsx b/explorer/src/components/Delegations/ModalDivider.tsx deleted file mode 100644 index 6258e0bfac..0000000000 --- a/explorer/src/components/Delegations/ModalDivider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Box, SxProps } from '@mui/material'; - -export const ModalDivider: FCWithChildren<{ - sx?: SxProps; -}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />; diff --git a/explorer/src/components/Delegations/ModalListItem.tsx b/explorer/src/components/Delegations/ModalListItem.tsx deleted file mode 100644 index 830c25705e..0000000000 --- a/explorer/src/components/Delegations/ModalListItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material'; -import { ModalDivider } from './ModalDivider'; - -export const ModalListItem: FCWithChildren<{ - label: string; - divider?: boolean; - hidden?: boolean; - fontWeight?: TypographyProps['fontWeight']; - fontSize?: TypographyProps['fontSize']; - light?: boolean; - value?: React.ReactNode; - sxValue?: SxProps; -}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => ( - <Box sx={{ display: hidden ? 'none' : 'block' }}> - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}> - {label} - </Typography> - {value && ( - <Typography - fontSize="smaller" - fontWeight={fontWeight} - sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }} - > - {value} - </Typography> - )} - </Stack> - {divider && <ModalDivider />} - </Box> -); diff --git a/explorer/src/components/Delegations/SimpleModal.tsx b/explorer/src/components/Delegations/SimpleModal.tsx deleted file mode 100644 index 88dff4d9d7..0000000000 --- a/explorer/src/components/Delegations/SimpleModal.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import ErrorOutline from '@mui/icons-material/ErrorOutline'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export const modalStyle = (width: number | string = 600) => ({ - position: 'absolute' as 'absolute', - top: '50%', - left: '50%', - width, - transform: 'translate(-50%, -50%)', - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}); - -export const StyledBackButton = ({ - onBack, - label, - fullWidth, - sx, -}: { - onBack: () => void; - label?: string; - fullWidth?: boolean; - sx?: SxProps; -}) => ( - <Button disableFocusRipple size="large" fullWidth={fullWidth} variant="outlined" onClick={onBack} sx={sx}> - {label || <ArrowBackIosNewIcon fontSize="small" />} - </Button> -); - -export const SimpleModal: FCWithChildren<{ - open: boolean; - hideCloseIcon?: boolean; - displayErrorIcon?: boolean; - displayInfoIcon?: boolean; - headerStyles?: SxProps; - subHeaderStyles?: SxProps; - buttonFullWidth?: boolean; - onClose?: () => void; - onOk?: () => Promise<void>; - onBack?: () => void; - header: string | React.ReactNode; - subHeader?: string; - okLabel: string; - backLabel?: string; - backButtonFullWidth?: boolean; - okDisabled?: boolean; - sx?: SxProps; - children?: React.ReactNode; -}> = ({ - open, - hideCloseIcon, - displayErrorIcon, - displayInfoIcon, - headerStyles, - buttonFullWidth, - onClose, - okDisabled, - onOk, - onBack, - header, - subHeader, - okLabel, - backLabel, - backButtonFullWidth, - sx, - children, -}) => { - const isMobile = useIsMobile(); - - return ( - <Modal open={open} onClose={onClose}> - <Box sx={{ ...modalStyle(isMobile ? '90%' : 600), ...sx }}> - {displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />} - {displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />} - <Stack direction="row" justifyContent="space-between" alignItems="center"> - {typeof header === 'string' ? ( - <Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}> - {header} - </Typography> - ) : ( - header - )} - {!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />} - </Stack> - - <Typography mt={subHeader ? 0.5 : 0} mb={3} fontSize={12} color={(theme) => theme.palette.text.secondary}> - {subHeader} - </Typography> - - {children} - - {(onOk || onBack) && ( - <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}> - {onBack && <StyledBackButton onBack={onBack} label={backLabel} fullWidth={backButtonFullWidth} />} - {onOk && ( - <Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}> - {okLabel} - </Button> - )} - </Box> - )} - </Box> - </Modal> - ); -}; diff --git a/explorer/src/components/Delegations/index.ts b/explorer/src/components/Delegations/index.ts deleted file mode 100644 index da51de9bd9..0000000000 --- a/explorer/src/components/Delegations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './ConfirmationModal'; -export * from './DelegateIconButton'; -export * from './DelegationModal'; -export * from './DelegateModal'; -export * from './ErrorModal'; -export * from './LoadingModal'; -export * from './ModalDivider'; -export * from './ModalListItem'; -export * from './SimpleModal'; -export * from './styles'; diff --git a/explorer/src/components/Delegations/styles.ts b/explorer/src/components/Delegations/styles.ts deleted file mode 100644 index 9b26551767..0000000000 --- a/explorer/src/components/Delegations/styles.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material/styles'; - -export const backDropStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { - style: { - left: mode === 'light' ? '0' : '50%', - width: '50%', - }, - }; -}; - -export const modalStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '25%' : '75%' }; -}; - -export const dialogStyles = (theme: Theme) => { - const { mode } = theme.palette; - return { left: mode === 'light' ? '-50%' : '50%' }; -}; diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx deleted file mode 100644 index 39355d0607..0000000000 --- a/explorer/src/components/DetailTable.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from 'react'; -import { - Link, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TableCellProps, -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { Box } from '@mui/system'; -import { unymToNym } from '../utils/currency'; -import { GatewayEnrichedRowType } from './Gateways'; -import { MixnodeRowType } from './MixNodes'; -import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar'; - -export type ColumnsType = { - field: string; - title: string; - headerAlign?: TableCellProps['align']; - width?: string | number; - tooltipInfo?: string; -}; - -export interface UniversalTableProps<T = any> { - tableName: string; - columnsData: ColumnsType[]; - rows: T[]; -} - -function formatCellValues(val: string | number, field: string) { - if (field === 'identity_key' && typeof val === 'string') { - return ( - <Box display="flex" justifyContent="flex-end"> - <CopyToClipboard - sx={{ mr: 1, mt: 0.5, fontSize: '18px' }} - value={val} - tooltip={`Copy identity key ${val} to clipboard`} - /> - <span>{val}</span> - </Box> - ); - } - - if (field === 'bond') { - return unymToNym(val, 6); - } - - if (field === 'owner') { - return ( - <Link underline="none" color="inherit" target="_blank" href={`https://mixnet.explorers.guru/account/${val}`}> - {val} - </Link> - ); - } - - if (field === 'stake_saturation') { - return <StakeSaturationProgressBar value={Number(val)} threshold={100} />; - } - - return val; -} - -export const DetailTable: FCWithChildren<{ - tableName: string; - columnsData: ColumnsType[]; - rows: MixnodeRowType[] | GatewayEnrichedRowType[]; -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { - const theme = useTheme(); - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 1080 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, width, tooltipInfo }) => ( - <TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}> - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Box sx={{ display: 'flex', alignItems: 'center' }}> - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - </Box> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((eachRow) => ( - <TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> - {columnsData?.map((data, index) => ( - <TableCell - key={data.title} - component="th" - scope="row" - variant="body" - sx={{ - padding: 2, - width: 200, - fontSize: 14, - }} - data-testid={`${data.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} - </TableCell> - ))} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ); -}; diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx deleted file mode 100644 index 0823ef6579..0000000000 --- a/explorer/src/components/Filters/Filters.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { - Button, - Dialog, - DialogContent, - DialogActions, - DialogTitle, - Slider, - Typography, - Box, - Snackbar, - Slide, - Alert, -} from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { useMainContext } from '../../context/main'; -import { MixnodeStatusWithAll, toMixnodeStatus } from '../../typeDefs/explorer-api'; -import { EnumFilterKey, TFilterItem, TFilters } from '../../typeDefs/filters'; -import { formatOnSave, generateFilterSchema } from './filterSchema'; -import { Api } from '../../api'; -import { useIsMobile } from '../../hooks/useIsMobile'; -import FiltersButton from './FiltersButton'; - -const FilterItem = ({ - label, - id, - tooltipInfo, - value, - isSmooth, - marks, - scale, - min, - max, - onChange, -}: TFilterItem & { - onChange: (id: EnumFilterKey, newValue: number[]) => void; -}) => ( - <Box sx={{ p: 2 }}> - <Typography gutterBottom>{label}</Typography> - <Typography fontSize={12}>{tooltipInfo}</Typography> - <Slider - value={value} - onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])} - valueLabelDisplay={isSmooth ? 'auto' : 'off'} - marks={marks} - step={isSmooth ? 1 : null} - scale={scale} - min={min} - max={max} - valueLabelFormat={(val: number) => (val === 100 && id === 'stakeSaturation' ? '>100' : val)} - /> - </Box> -); - -export const Filters = () => { - const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext(); - const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); - const isMobile = useIsMobile(); - - const [showFilters, setShowFilters] = useState(false); - const [isFiltered, setIsFiltered] = useState(false); - const [filters, setFilters] = React.useState<TFilters>(); - const [upperSaturationValue, setUpperSaturationValue] = React.useState<number>(100); - - const baseFilters = useRef<TFilters>(); - const prevFilters = useRef<TFilters>(); - - const handleToggleShowFilters = () => setShowFilters(!showFilters); - - const initialiseFilters = useCallback(async () => { - const allMixnodes = await Api.fetchMixnodes(); - if (allMixnodes) { - setUpperSaturationValue(Math.round(Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1)); - const initFilters = generateFilterSchema(); - baseFilters.current = initFilters; - prevFilters.current = initFilters; - setFilters(initFilters); - } - }, []); - - const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { - if (id === 'stakeSaturation' && newValue[1] === 100) { - newValue.splice(1, 1, upperSaturationValue); - } - setFilters((ftrs) => { - if (ftrs) - return { - ...ftrs, - [id]: { - ...ftrs[id], - value: newValue, - }, - }; - return undefined; - }); - }; - - const handleOnSave = async () => { - setShowFilters(false); - await filterMixnodes(formatOnSave(filters!), status); - setIsFiltered(true); - prevFilters.current = filters; - }; - - const handleOnCancel = () => { - setShowFilters(false); - setFilters(prevFilters.current); - }; - - const resetFilters = () => { - setFilters(baseFilters.current); - setIsFiltered(false); - prevFilters.current = baseFilters.current; - }; - - const onClearFilters = async () => { - await fetchMixnodes(toMixnodeStatus(status)); - resetFilters(); - }; - - useEffect(() => { - initialiseFilters(); - }, [initialiseFilters]); - - useEffect(() => { - resetFilters(); - }, [status]); - - if (!filters) return null; - - return ( - <> - <Snackbar - open={isFiltered} - anchorOrigin={{ vertical: 'top', horizontal: 'center' }} - message="Filters applied" - TransitionComponent={Slide} - transitionDuration={250} - > - <Alert - severity="info" - variant={isMobile ? 'standard' : 'outlined'} - sx={{ color: (t) => t.palette.info.light }} - action={ - <Button size="small" onClick={onClearFilters}> - CLEAR FILTERS - </Button> - } - > - {mixnodes?.data?.length} mixnodes matched your criteria - </Alert> - </Snackbar> - <FiltersButton onClick={handleToggleShowFilters} fullWidth /> - <Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth> - <DialogTitle>Mixnode filters</DialogTitle> - <DialogContent dividers> - {Object.values(filters).map((v) => ( - <FilterItem {...v} key={v.id} onChange={handleOnChange} /> - ))} - </DialogContent> - <DialogActions> - <Button size="large" onClick={handleOnCancel}> - Cancel - </Button> - <Button variant="contained" size="large" onClick={handleOnSave}> - Save - </Button> - </DialogActions> - </Dialog> - </> - ); -}; diff --git a/explorer/src/components/Filters/FiltersButton.tsx b/explorer/src/components/Filters/FiltersButton.tsx deleted file mode 100644 index 1e6d3e2940..0000000000 --- a/explorer/src/components/Filters/FiltersButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Button, IconButton } from '@mui/material'; -import { Tune } from '@mui/icons-material'; - -type FiltersButtonProps = { - iconOnly?: boolean; - fullWidth?: boolean; - onClick: () => void; -}; - -const FiltersButton = ({ iconOnly, fullWidth, onClick }: FiltersButtonProps) => { - if (iconOnly) { - return ( - <IconButton onClick={onClick} color="primary"> - <Tune /> - </IconButton> - ); - } - - return ( - <Button - fullWidth={fullWidth} - size="large" - variant="contained" - endIcon={<Tune />} - onClick={onClick} - sx={{ textTransform: 'none' }} - > - Filters - </Button> - ); -}; - -export default FiltersButton; diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts deleted file mode 100644 index c5011114b1..0000000000 --- a/explorer/src/components/Filters/filterSchema.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; - -export const generateFilterSchema = () => ({ - profitMargin: { - label: 'Profit margin (%)', - id: EnumFilterKey.profitMargin, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: - 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', - }, - stakeSaturation: { - label: 'Stake saturation (%)', - id: EnumFilterKey.stakeSaturation, - value: [0, 100], - isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ - value: value < 100 ? value : 100, - label: value < 100 ? value : '>100', - })), - tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", - }, - routingScore: { - label: 'Routing score (%)', - id: EnumFilterKey.routingScore, - value: [0, 100], - isSmooth: true, - marks: [ - { label: '0', value: 0 }, - { label: '10', value: 10 }, - { label: '20', value: 20 }, - { label: '30', value: 30 }, - { label: '40', value: 40 }, - { label: '50', value: 50 }, - { label: '60', value: 60 }, - { label: '70', value: 70 }, - { label: '80', value: 80 }, - { label: '90', value: 90 }, - { label: '100', value: 100 }, - ], - tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', - }, -}); - -const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { - const lowerValue = value_1 / 100; - const upperValue = value_2 / 100; - - return [lowerValue, upperValue]; -}; - -export const formatOnSave = (filters: TFilters) => ({ - routingScore: filters.routingScore.value, - profitMargin: filters.profitMargin.value, - stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), -}); diff --git a/explorer/src/components/Footer.tsx b/explorer/src/components/Footer.tsx deleted file mode 100644 index 375b5c9b1a..0000000000 --- a/explorer/src/components/Footer.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import Box from '@mui/material/Box'; -import MuiLink from '@mui/material/Link'; -import { Link } from 'react-router-dom'; -import Typography from '@mui/material/Typography'; -import { Socials } from './Socials'; -import { useIsMobile } from '../hooks/useIsMobile'; -import { NymVpnIcon } from '../icons/NymVpn'; - -export const Footer: FCWithChildren = () => { - const isMobile = useIsMobile(); - - return ( - <Box - sx={{ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - width: '100%', - height: 'auto', - mt: 3, - pt: 3, - pb: 3, - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - justifyContent: 'center', - alignItems: 'center', - mb: 2, - }} - > - <MuiLink component={Link} to="http://nymvpn.com" target="_blank" underline="none" marginRight={1}> - <NymVpnIcon /> - </MuiLink> - <Socials isFooter /> - </Box> - - <Typography - sx={{ - fontSize: 12, - textAlign: isMobile ? 'center' : 'end', - color: 'nym.muted.onDarkBg', - }} - > - © {new Date().getFullYear()} Nym Technologies SA, all rights reserved - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts deleted file mode 100644 index 966a3c987e..0000000000 --- a/explorer/src/components/Gateways.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GatewayResponse, GatewayBond, GatewayReportResponse } from '../typeDefs/explorer-api'; -import { toPercentInteger } from '../utils'; - -export type GatewayRowType = { - id: string; - owner: string; - identity_key: string; - bond: number; - host: string; - location: string; - version: string; - node_performance: number; -}; - -export type GatewayEnrichedRowType = GatewayRowType & { - routingScore: string; - avgUptime: string; - clientsPort: number; - mixPort: number; -}; - -export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { - return !arrayOfGateways - ? [] - : arrayOfGateways.map((gw) => ({ - id: gw.owner, - owner: gw.owner, - identity_key: gw.gateway.identity_key || '', - location: gw.location?.country_name.toUpperCase() || '', - bond: gw.pledge_amount.amount || 0, - host: gw.gateway.host || '', - version: gw.gateway.version || '', - node_performance: toPercentInteger(gw.node_performance.last_24h), - })); -} - -export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType { - return { - id: gateway.owner, - owner: gateway.owner, - identity_key: gateway.gateway.identity_key || '', - location: gateway.location?.country_name.toUpperCase() || '', - bond: gateway.pledge_amount.amount || 0, - host: gateway.gateway.host || '', - version: gateway.gateway.version || '', - clientsPort: gateway.gateway.clients_port || 0, - mixPort: gateway.gateway.mix_port || 0, - routingScore: `${report.most_recent}%`, - avgUptime: `${report.last_day || report.last_hour}%`, - node_performance: toPercentInteger(gateway.node_performance.most_recent), - }; -} diff --git a/explorer/src/components/Gateways/VersionDisplaySelector.tsx b/explorer/src/components/Gateways/VersionDisplaySelector.tsx deleted file mode 100644 index e9be02b419..0000000000 --- a/explorer/src/components/Gateways/VersionDisplaySelector.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { FormControl, MenuItem, Select } from '@mui/material'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export enum VersionSelectOptions { - latestVersion = 'Latest versions', - olderVersions = 'Older versions', - all = 'All', -} -export const VersionDisplaySelector = ({ - selected, - handleChange, -}: { - selected: VersionSelectOptions; - handleChange: (option: VersionSelectOptions) => void; -}) => { - const isMobile = useIsMobile(); - - return ( - <FormControl size="small"> - <Select - value={selected} - onChange={(e) => handleChange(e.target.value as VersionSelectOptions)} - labelId="simple-select-label" - id="simple-select" - sx={{ - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem value={VersionSelectOptions.latestVersion} data-testid="show-gateway-latest-version"> - {VersionSelectOptions.latestVersion} - </MenuItem> - <MenuItem value={VersionSelectOptions.olderVersions} data-testid="show-gateway-old-versions"> - {VersionSelectOptions.olderVersions} - </MenuItem> - <MenuItem value={VersionSelectOptions.all} data-testid="show-gateway-all-versions"> - {VersionSelectOptions.all} - </MenuItem> - </Select> - </FormControl> - ); -}; diff --git a/explorer/src/components/Icons.ts b/explorer/src/components/Icons.ts deleted file mode 100644 index 88761b9d21..0000000000 --- a/explorer/src/components/Icons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; -import { MixnodeStatus } from '../typeDefs/explorer-api'; - -export const Icons = { - Mixnodes: { - Status: { - Active: CheckCircleOutlineIcon, - Standby: PauseCircleOutlineIcon, - Inactive: CircleOutlinedIcon, - }, - }, -}; - -export const getMixNodeIcon = (value: any) => { - if (value && typeof value === 'string') { - switch (value) { - case MixnodeStatus.active: - return Icons.Mixnodes.Status.Active; - case MixnodeStatus.standby: - return Icons.Mixnodes.Status.Standby; - default: - return Icons.Mixnodes.Status.Inactive; - } - } - return Icons.Mixnodes.Status.Inactive; -}; diff --git a/explorer/src/components/MixNodes/BondBreakdown.tsx b/explorer/src/components/MixNodes/BondBreakdown.tsx deleted file mode 100644 index 7fd66efb24..0000000000 --- a/explorer/src/components/MixNodes/BondBreakdown.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from 'react'; -import { Alert, Box, CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import { ExpandMore } from '@mui/icons-material'; -import { currencyToString } from '../../utils/currency'; -import { useMixnodeContext } from '../../context/mixnode'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -export const BondBreakdownTable: FCWithChildren = () => { - const { mixNode, delegations, uniqDelegations } = useMixnodeContext(); - const [showDelegations, toggleShowDelegations] = React.useState<boolean>(false); - - const [bonds, setBonds] = React.useState({ - delegations: '0', - pledges: '0', - bondsTotal: '0', - hasLoaded: false, - }); - const theme = useTheme(); - const isMobile = useIsMobile(); - - React.useEffect(() => { - if (mixNode?.data) { - // delegations - const decimalisedDelegations = currencyToString({ - amount: mixNode.data.total_delegation.amount.toString(), - denom: mixNode.data.total_delegation.denom, - }); - - // pledges - const decimalisedPledges = currencyToString({ - amount: mixNode.data.pledge_amount.amount.toString(), - denom: mixNode.data.pledge_amount.denom, - }); - - // bonds total (del + pledges) - const pledgesSum = Number(mixNode.data.pledge_amount.amount); - const delegationsSum = Number(mixNode.data.total_delegation.amount); - const bondsTotal = currencyToString({ - amount: (pledgesSum + delegationsSum).toString(), - }); - - setBonds({ - delegations: decimalisedDelegations, - pledges: decimalisedPledges, - bondsTotal, - hasLoaded: true, - }); - } - }, [mixNode]); - - const expandDelegations = () => { - if (delegations?.data && delegations.data.length > 0) { - toggleShowDelegations(!showDelegations); - } - }; - const calcBondPercentage = (num: number) => { - if (mixNode?.data) { - const rawDelegationAmount = Number(mixNode.data.total_delegation.amount); - const rawPledgeAmount = Number(mixNode.data.pledge_amount.amount); - const rawTotalBondsAmount = rawDelegationAmount + rawPledgeAmount; - return ((num * 100) / rawTotalBondsAmount).toFixed(1); - } - return 0; - }; - - if (mixNode?.isLoading || delegations?.isLoading) { - return <CircularProgress />; - } - - if (mixNode?.error) { - return <Alert severity="error">Mixnode not found</Alert>; - } - if (delegations?.error) { - return <Alert severity="error">Unable to get delegations for mixnode</Alert>; - } - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals"> - <TableBody> - <TableRow sx={isMobile ? { minWidth: '70vw' } : null}> - <TableCell - sx={{ - fontWeight: 400, - width: '150px', - }} - align="left" - > - Stake total - </TableCell> - <TableCell align="left" data-testid="bond-total-amount"> - {bonds.bondsTotal} - </TableCell> - </TableRow> - <TableRow> - <TableCell align="left">Bond</TableCell> - <TableCell align="left" data-testid="pledge-total-amount"> - {bonds.pledges} - </TableCell> - </TableRow> - <TableRow> - <TableCell onClick={expandDelegations} align="left"> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - }} - > - Delegation total {'\u00A0'} - {delegations?.data && delegations?.data?.length > 0 && <ExpandMore />} - </Box> - </TableCell> - <TableCell align="left" data-testid="delegation-total-amount"> - {bonds.delegations} - </TableCell> - </TableRow> - </TableBody> - </Table> - - {showDelegations && ( - <Box - sx={{ - maxHeight: 400, - overflowY: 'scroll', - p: 2, - background: theme.palette.background.paper, - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'baseline', - width: '100%', - p: 2, - borderBottom: `1px solid ${theme.palette.divider}`, - }} - data-testid="delegations-total-amount" - > - <Typography - sx={{ - fontSize: 16, - fontWeight: 600, - }} - > - Delegations   - </Typography> - </Box> - <Table stickyHeader> - <TableHead> - <TableRow> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Delegators - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - }} - align="left" - > - Amount - </TableCell> - <TableCell - sx={{ - fontWeight: 600, - background: theme.palette.background.paper, - width: '200px', - }} - align="left" - > - Share of stake - </TableCell> - </TableRow> - </TableHead> - - <TableBody> - {uniqDelegations?.data?.map(({ owner, amount: { amount } }) => ( - <TableRow key={owner}> - <TableCell sx={isMobile ? { width: 190 } : null} align="left"> - {owner} - </TableCell> - <TableCell align="left">{currencyToString({ amount: amount.toString() })}</TableCell> - <TableCell align="left">{calcBondPercentage(amount)}%</TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </Box> - )} - </TableContainer> - ); -}; diff --git a/explorer/src/components/MixNodes/DetailSection.tsx b/explorer/src/components/MixNodes/DetailSection.tsx deleted file mode 100644 index 6becf5efe4..0000000000 --- a/explorer/src/components/MixNodes/DetailSection.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import * as React from 'react'; -import { Box, Button, Grid, Typography, useTheme } from '@mui/material'; -import Identicon from 'react-identicons'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { MixnodeRowType } from '.'; -import { getMixNodeStatusText, MixNodeStatus } from './Status'; -import { MixNodeDescriptionResponse } from '../../typeDefs/explorer-api'; - -interface MixNodeDetailProps { - mixNodeRow: MixnodeRowType; - mixnodeDescription: MixNodeDescriptionResponse; -} - -export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNodeRow, mixnodeDescription }) => { - const theme = useTheme(); - const palette = [theme.palette.text.primary]; - const isMobile = useIsMobile(); - const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]); - - return ( - <Grid container> - <Grid item xs={12} md={6}> - <Box display="flex" flexDirection={isMobile ? 'column' : 'row'} width="100%"> - <Box - width={72} - height={72} - sx={{ - minWidth: 72, - minHeight: 72, - borderWidth: 1, - borderColor: theme.palette.text.primary, - borderStyle: 'solid', - borderRadius: '50%', - display: 'grid', - placeItems: 'center', - }} - > - <Identicon size={43} string={mixNodeRow.identity_key} palette={palette} /> - </Box> - <Box ml={isMobile ? 0 : 2} mt={isMobile ? 2 : 0}> - <Typography fontSize={21}>{mixnodeDescription.name}</Typography> - <Typography>{(mixnodeDescription.description || '').slice(0, 1000)}</Typography> - <Button - component="a" - variant="text" - sx={{ - mt: isMobile ? 2 : 4, - borderRadius: '30px', - fontWeight: 600, - padding: 0, - }} - href={mixnodeDescription.link} - target="_blank" - > - <Typography - component="span" - textOverflow="ellipsis" - whiteSpace="nowrap" - overflow="hidden" - maxWidth="250px" - > - {mixnodeDescription.link} - </Typography> - </Button> - </Box> - </Box> - </Grid> - <Grid - item - xs={12} - md={6} - display="flex" - justifyContent={isMobile ? 'start' : 'end'} - mt={isMobile ? 3 : undefined} - > - <Box display="flex" flexDirection="column"> - <Typography fontWeight="600" alignSelf={isMobile ? 'start' : 'self-end'}> - Node status: - </Typography> - <Box mt={2} alignSelf={isMobile ? 'start' : 'self-end'}> - <MixNodeStatus status={mixNodeRow.status} /> - </Box> - <Typography - mt={1} - alignSelf={isMobile ? 'start' : 'self-end'} - color={theme.palette.text.secondary} - fontSize="smaller" - > - This node is {statusText} in this epoch - </Typography> - </Box> - </Grid> - </Grid> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/Columns.ts b/explorer/src/components/MixNodes/Economics/Columns.ts deleted file mode 100644 index b2115c0749..0000000000 --- a/explorer/src/components/MixNodes/Economics/Columns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ColumnsType } from '../../DetailTable'; - -export const EconomicsInfoColumns: ColumnsType[] = [ - { - field: 'estimatedTotalReward', - title: 'Estimated Total Reward', - width: '15%', - tooltipInfo: - 'Estimated node reward (total for the operator and delegators) in the current epoch. There are roughly 24 epochs in a day.', - }, - { - field: 'estimatedOperatorReward', - title: 'Estimated Operator Reward', - width: '15%', - tooltipInfo: - "Estimated operator's reward (including PM and Operating Cost) in the current epoch. There are roughly 24 epochs in a day.", - }, - { - field: 'selectionChance', - title: 'Active Set Probability', - width: '12.5%', - tooltipInfo: - 'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.', - }, - { - field: 'profitMargin', - title: 'Profit Margin', - width: '12.5%', - tooltipInfo: - 'Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators.', - }, - { - field: 'operatingCost', - title: 'Operating Cost', - width: '10%', - tooltipInfo: - 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', - }, - { - field: 'nodePerformance', - title: 'Routing Score', - width: '10%', - tooltipInfo: - "Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - tooltipInfo: "Mixnode's average routing score in the last 24 hour", - }, -]; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx deleted file mode 100644 index aef36113df..0000000000 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { EconomicsProgress } from './EconomicsProgress'; - -export default { - title: 'Mix Node Detail/Economics/ProgressBar', - component: EconomicsProgress, -} as ComponentMeta<typeof EconomicsProgress>; - -const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />; - -export const Empty = Template.bind({}); -Empty.args = {}; - -export const OverThreshold = Template.bind({}); -OverThreshold.args = { - threshold: 100, - value: 120, -}; - -export const UnderThreshold = Template.bind({}); -UnderThreshold.args = { - threshold: 100, - value: 80, -}; - -export const OnThreshold = Template.bind({}); -OnThreshold.args = { - threshold: 100, - value: 100, -}; diff --git a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx b/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx deleted file mode 100644 index 24db62d167..0000000000 --- a/explorer/src/components/MixNodes/Economics/EconomicsProgress.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; -import { Box } from '@mui/system'; - -const parseToNumber = (value: number | undefined | string) => - typeof value === 'string' ? parseInt(value || '', 10) : value || 0; - -export const EconomicsProgress: FCWithChildren< - LinearProgressProps & { - threshold?: number; - color: string; - } -> = ({ threshold, color, ...props }) => { - const theme = useTheme(); - const { value } = props; - - const valueNumber: number = parseToNumber(value); - const thresholdNumber: number = parseToNumber(threshold); - const percentageToDisplay = Math.min(valueNumber, thresholdNumber); - - return ( - <Box - sx={{ - width: 6 / 10, - color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee, - }} - > - <LinearProgress - {...props} - variant="determinate" - color={color} - value={percentageToDisplay} - sx={{ width: '100%', borderRadius: '5px' }} - /> - </Box> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx deleted file mode 100644 index c9c82c0438..0000000000 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import * as React from 'react'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { DelegatorsInfoTable } from './Table'; -import { EconomicsInfoColumns } from './Columns'; -import { EconomicsInfoRowWithIndex } from './types'; - -export default { - title: 'Mix Node Detail/Economics', - component: DelegatorsInfoTable, -} as ComponentMeta<typeof DelegatorsInfoTable>; - -const row: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: 'High', - }, - - estimatedOperatorReward: { - value: '80000.123456 NYM', - }, - estimatedTotalReward: { - value: '80000.123456 NYM', - }, - profitMargin: { - value: '10 %', - }, - operatingCost: { - value: '11121 NYM', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Good', - }, -}; - -const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Low', - }, -}; - -const emptyRow: EconomicsInfoRowWithIndex = { - id: 1, - selectionChance: { - value: '-', - progressBarValue: 0, - }, - - estimatedOperatorReward: { - value: '-', - }, - estimatedTotalReward: { - value: '-', - }, - profitMargin: { - value: '-', - }, - operatingCost: { - value: '-', - }, - avgUptime: { - value: '-', - }, - nodePerformance: { - value: '-', - }, -}; - -const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />; - -export const Empty = Template.bind({}); -Empty.args = { - rows: [emptyRow], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceHigh = Template.bind({}); -selectionChanceHigh.args = { - rows: [row], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceGood = Template.bind({}); -selectionChanceGood.args = { - rows: [rowGoodProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - -export const selectionChanceLow = Template.bind({}); -selectionChanceLow.args = { - rows: [rowLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts deleted file mode 100644 index ce947b0a4f..0000000000 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { currencyToString, unymToNym } from '../../../utils/currency'; -import { useMixnodeContext } from '../../../context/mixnode'; -import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api'; -import { EconomicsInfoRowWithIndex } from './types'; -import { toPercentIntegerString } from '../../../utils'; - -const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => - economicDynamicsStats?.data?.active_set_inclusion_probability || '-'; - -export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { - const { economicDynamicsStats, mixNode } = useMixnodeContext(); - - const estimatedNodeRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_total_node_reward.toString() || '', - }) || '-'; - const estimatedOperatorRewards = - currencyToString({ - amount: economicDynamicsStats?.data?.estimated_operator_reward.toString() || '', - }) || '-'; - const profitMargin = mixNode?.data?.profit_margin_percent - ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) - : '-'; - const avgUptime = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.last_24h) - : '-'; - const nodePerformance = mixNode?.data?.node_performance - ? toPercentIntegerString(mixNode?.data?.node_performance.most_recent) - : '-'; - - const opCost = mixNode?.data?.operating_cost; - - return { - id: 1, - estimatedTotalReward: { - value: estimatedNodeRewards, - }, - estimatedOperatorReward: { - value: estimatedOperatorRewards, - }, - selectionChance: { - value: selectionChance(economicDynamicsStats), - }, - profitMargin: { - value: profitMargin ? `${profitMargin} %` : '-', - }, - operatingCost: { - value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', - }, - avgUptime: { - value: avgUptime ? `${avgUptime} %` : '-', - }, - nodePerformance: { - value: nodePerformance, - }, - }; -}; diff --git a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx b/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx deleted file mode 100644 index 918d024643..0000000000 --- a/explorer/src/components/MixNodes/Economics/StakeSaturationProgressBar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useIsMobile } from '../../../hooks/useIsMobile'; -import { EconomicsProgress } from './EconomicsProgress'; - -export const StakeSaturationProgressBar = ({ value, threshold }: { value: number; threshold: number }) => { - const isTablet = useIsMobile('lg'); - const percentageColor = value > (threshold || 100) ? 'warning' : 'inherit'; - const textColor = percentageColor === 'warning' ? 'warning.main' : 'nym.wallet.fee'; - - return ( - <Box - sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} - id="field" - color={percentageColor} - > - <Typography - sx={{ - mr: isTablet ? 0 : 1, - mb: isTablet ? 1 : 0, - fontWeight: '600', - fontSize: '12px', - color: textColor, - }} - id="stake-saturation-progress-bar" - > - {value}% - </Typography> - <EconomicsProgress value={value} threshold={threshold} color={percentageColor} /> - </Box> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx deleted file mode 100644 index e6630334db..0000000000 --- a/explorer/src/components/MixNodes/Economics/Table.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; -import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; -import { Box } from '@mui/system'; -import { useTheme } from '@mui/material/styles'; -import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; -import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types'; -import { UniversalTableProps } from '../../DetailTable'; -import { textColour } from '../../../utils'; - -const formatCellValues = (value: EconomicsRowsType, field: string) => ( - <Box sx={{ display: 'flex', alignItems: 'center' }} id="field"> - <Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}> - {value.value} - </Typography> - </Box> -); - -export const DelegatorsInfoTable: FCWithChildren<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({ - tableName, - columnsData, - rows, -}) => { - const theme = useTheme(); - - return ( - <TableContainer component={Paper}> - <Table sx={{ minWidth: 650 }} aria-label={tableName}> - <TableHead> - <TableRow> - {columnsData?.map(({ field, title, tooltipInfo, width }) => ( - <TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, width }}> - <Box sx={{ display: 'flex', alignItems: 'center' }}> - {tooltipInfo && ( - <Tooltip - title={tooltipInfo} - id={field} - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - )} - {title} - </Box> - </TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows?.map((eachRow) => ( - <TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> - {columnsData?.map((_, index: number) => { - const { field } = columnsData[index]; - const value: EconomicsRowsType = (eachRow as any)[field]; - return ( - <TableCell - key={_.title} - sx={{ - color: textColour(value, field, theme), - }} - data-testid={`${_.title.replace(/ /g, '-')}-value`} - > - {formatCellValues(value, columnsData[index].field)} - </TableCell> - ); - })} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - ); -}; diff --git a/explorer/src/components/MixNodes/Economics/index.ts b/explorer/src/components/MixNodes/Economics/index.ts deleted file mode 100644 index 6dec752246..0000000000 --- a/explorer/src/components/MixNodes/Economics/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DelegatorsInfoTable } from './Table'; -export { EconomicsInfoColumns } from './Columns'; -export { EconomicsInfoRows } from './Rows'; diff --git a/explorer/src/components/MixNodes/Economics/types.ts b/explorer/src/components/MixNodes/Economics/types.ts deleted file mode 100644 index 0bc550253f..0000000000 --- a/explorer/src/components/MixNodes/Economics/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type EconomicsRowsType = { - progressBarValue?: number; - value: string; -}; - -type TEconomicsInfoProperties = - | 'estimatedTotalReward' - | 'estimatedOperatorReward' - | 'estimatedOperatorReward' - | 'selectionChance' - | 'profitMargin' - | 'avgUptime' - | 'nodePerformance' - | 'operatingCost'; - -export type EconomicsInfoRow = { - [k in TEconomicsInfoProperties]: EconomicsRowsType; -}; - -export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; diff --git a/explorer/src/components/MixNodes/Status.tsx b/explorer/src/components/MixNodes/Status.tsx deleted file mode 100644 index 627b1f5964..0000000000 --- a/explorer/src/components/MixNodes/Status.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Typography } from '@mui/material'; -import * as React from 'react'; -import { getMixNodeIcon } from '@src/components/Icons'; -import { MixnodeStatus } from '@src/typeDefs/explorer-api'; -import { useGetMixNodeStatusColor } from '@src/hooks/useGetMixnodeStatusColor'; - -interface MixNodeStatusProps { - status: MixnodeStatus; -} -// TODO: should be done with i18n -export const getMixNodeStatusText = (status: MixnodeStatus) => { - switch (status) { - case MixnodeStatus.active: - return 'active'; - case MixnodeStatus.standby: - return 'on standby'; - default: - return 'inactive'; - } -}; - -export const MixNodeStatus: FCWithChildren<MixNodeStatusProps> = ({ status }) => { - const Icon = React.useMemo(() => getMixNodeIcon(status), [status]); - const color = useGetMixNodeStatusColor(status); - - return ( - <Typography color={color} display="flex" alignItems="center"> - <Icon /> - <Typography ml={1} component="span" color="inherit"> - {`${status[0].toUpperCase()}${status.slice(1)}`} - </Typography> - </Typography> - ); -}; diff --git a/explorer/src/components/MixNodes/StatusDropdown.tsx b/explorer/src/components/MixNodes/StatusDropdown.tsx deleted file mode 100644 index 9cb34a2f0c..0000000000 --- a/explorer/src/components/MixNodes/StatusDropdown.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as React from 'react'; -import { MenuItem } from '@mui/material'; -import Select from '@mui/material/Select'; -import { SelectChangeEvent } from '@mui/material/Select/SelectInput'; -import { SxProps } from '@mui/system'; -import { MixNodeStatus } from './Status'; -import { MixnodeStatus, MixnodeStatusWithAll } from '../../typeDefs/explorer-api'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -// TODO: replace with i18n -const ALL_NODES = 'All nodes'; - -interface MixNodeStatusDropdownProps { - status?: MixnodeStatusWithAll; - sx?: SxProps; - onSelectionChanged?: (status?: MixnodeStatusWithAll) => void; -} - -export const MixNodeStatusDropdown: FCWithChildren<MixNodeStatusDropdownProps> = ({ - status, - onSelectionChanged, - sx, -}) => { - const isMobile = useIsMobile(); - const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(status || MixnodeStatusWithAll.all); - const onChange = React.useCallback( - (event: SelectChangeEvent) => { - setStatusValue(event.target.value as MixnodeStatusWithAll); - if (onSelectionChanged) { - onSelectionChanged(event.target.value as MixnodeStatusWithAll); - } - }, - [onSelectionChanged], - ); - - return ( - <Select - labelId="mixnodeStatusSelect_label" - id="mixnodeStatusSelect" - value={statusValue} - onChange={onChange} - renderValue={(value) => { - switch (value) { - case MixnodeStatusWithAll.active: - case MixnodeStatusWithAll.standby: - case MixnodeStatusWithAll.inactive: - return <MixNodeStatus status={value as unknown as MixnodeStatus} />; - default: - return ALL_NODES; - } - }} - sx={{ - width: isMobile ? '50%' : 200, - ...sx, - }} - > - <MenuItem value={MixnodeStatus.active} data-testid="mixnodeStatusSelectOption_active"> - <MixNodeStatus status={MixnodeStatus.active} /> - </MenuItem> - <MenuItem value={MixnodeStatus.standby} data-testid="mixnodeStatusSelectOption_standby"> - <MixNodeStatus status={MixnodeStatus.standby} /> - </MenuItem> - <MenuItem value={MixnodeStatus.inactive} data-testid="mixnodeStatusSelectOption_inactive"> - <MixNodeStatus status={MixnodeStatus.inactive} /> - </MenuItem> - <MenuItem value={MixnodeStatusWithAll.all} data-testid="mixnodeStatusSelectOption_allNodes"> - {ALL_NODES} - </MenuItem> - </Select> - ); -}; - -MixNodeStatusDropdown.defaultProps = { - onSelectionChanged: undefined, - status: undefined, - sx: undefined, -}; diff --git a/explorer/src/components/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts deleted file mode 100644 index ccb6c5a8b3..0000000000 --- a/explorer/src/components/MixNodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Status'; -export * from './StatusDropdown'; -export * from './mappings'; diff --git a/explorer/src/components/MixNodes/mappings.ts b/explorer/src/components/MixNodes/mappings.ts deleted file mode 100644 index aba4c6fdca..0000000000 --- a/explorer/src/components/MixNodes/mappings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable camelcase */ -import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; -import { toPercentInteger, toPercentIntegerString } from '../../utils'; -import { unymToNym } from '../../utils/currency'; - -export type MixnodeRowType = { - mix_id: number; - id: string; - status: MixnodeStatus; - owner: string; - location: string; - identity_key: string; - bond: number; - self_percentage: string; - pledge_amount: number; - host: string; - layer: string; - profit_percentage: number; - avg_uptime: string; - stake_saturation: React.ReactNode; - operating_cost: number; - node_performance: number; - blacklisted: boolean; -}; - -export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { - return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType); -} - -export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType { - const pledge = Number(item.pledge_amount.amount) || 0; - const delegations = Number(item.total_delegation.amount) || 0; - const totalBond = pledge + delegations; - const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = toPercentInteger(item.profit_margin_percent) || 0; - const uncappedSaturation = typeof item.uncapped_saturation === 'number' ? item.uncapped_saturation * 100 : 0; - - return { - mix_id: item.mix_id, - id: item.owner, - status: item.status, - owner: item.owner, - identity_key: item.mix_node.identity_key || '', - bond: totalBond || 0, - location: item?.location?.country_name || '', - self_percentage: selfPercentage, - pledge_amount: pledge, - host: item?.mix_node?.host || '', - layer: item?.layer || '', - profit_percentage: profitPercentage, - avg_uptime: `${toPercentIntegerString(item.node_performance.last_24h)}%`, - stake_saturation: Number(uncappedSaturation.toFixed(2)), - operating_cost: Number(unymToNym(item.operating_cost?.amount, 6)) || 0, - node_performance: toPercentInteger(item.node_performance.most_recent), - blacklisted: item.blacklisted, - }; -} diff --git a/explorer/src/components/MobileNav.tsx b/explorer/src/components/MobileNav.tsx deleted file mode 100644 index ac4b313467..0000000000 --- a/explorer/src/components/MobileNav.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; -import { AppBar, Box, Drawer, IconButton, List, ListItem, ListItemButton, ListItemIcon, Toolbar } from '@mui/material'; -import { Menu } from '@mui/icons-material'; -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { useMainContext } from '../context/main'; -import { MobileDrawerClose } from '../icons/MobileDrawerClose'; -import { Footer } from './Footer'; -import { ExpandableButton } from './Nav'; -import { ConnectKeplrWallet } from './Wallet/ConnectKeplrWallet'; -import NetworkTitle from './NetworkTitle'; - -export const MobileNav: FCWithChildren = ({ children }) => { - const theme = useTheme(); - const { navState, updateNavState } = useMainContext(); - const [drawerOpen, setDrawerOpen] = React.useState(false); - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false); - const isSmallMobile = useIsMobile(400); - - const toggleDrawer = () => { - setDrawerOpen(!drawerOpen); - }; - - const handleClick = (url: string) => { - updateNavState(url); - toggleDrawer(); - }; - - const openDrawer = () => { - setDrawerOpen(true); - }; - - return ( - <Box sx={{ display: 'flex', flexDirection: 'column' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner open={openMaintenance} onClick={() => setOpenMaintenance(false)} /> - <Toolbar - sx={{ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }} - > - <IconButton onClick={toggleDrawer}> - <Menu sx={{ color: 'primary.contrastText' }} /> - </IconButton> - {!isSmallMobile && <NetworkTitle />} - </Box> - <ConnectKeplrWallet /> - </Toolbar> - </AppBar> - <Drawer - anchor="left" - open={drawerOpen} - onClose={toggleDrawer} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - }, - }} - > - <Box role="presentation"> - <List sx={{ pt: 0, pb: 0 }}> - <ListItem - disablePadding - disableGutters - sx={{ - height: 64, - background: theme.palette.nym.networkExplorer.nav.background, - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - }} - > - <ListItemButton - onClick={toggleDrawer} - sx={{ - pt: 2, - pb: 2, - background: theme.palette.nym.networkExplorer.nav.background, - display: 'flex', - justifyContent: 'flex-start', - }} - > - <ListItemIcon> - <MobileDrawerClose /> - </ListItemIcon> - </ListItemButton> - </ListItem> - {navState.map((props) => ( - <ExpandableButton - key={props.url} - title={props.title} - openDrawer={openDrawer} - url={props.url} - drawIsTempOpen={drawerOpen === true} - drawIsFixed={false} - Icon={props.Icon} - setToActive={handleClick} - nested={props.nested} - isMobile - isActive={props.isActive} - /> - ))} - </List> - </Box> - </Drawer> - - <Box sx={{ width: '100%', p: 4, mt: 7 }}> - {children} - <Footer /> - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/Nav.tsx b/explorer/src/components/Nav.tsx deleted file mode 100644 index a0f5b16fec..0000000000 --- a/explorer/src/components/Nav.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { ExpandLess, ExpandMore, Menu } from '@mui/icons-material'; -import { CSSObject, styled, Theme, useTheme } from '@mui/material/styles'; -import Button from '@mui/material/Button'; -import MuiLink from '@mui/material/Link'; -import Box from '@mui/material/Box'; -import ListItem from '@mui/material/ListItem'; -import MuiDrawer from '@mui/material/Drawer'; -import AppBar from '@mui/material/AppBar'; -import Toolbar from '@mui/material/Toolbar'; -import List from '@mui/material/List'; -import Typography from '@mui/material/Typography'; -import IconButton from '@mui/material/IconButton'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; -import { MaintenanceBanner } from '@nymproject/react/banners/MaintenanceBanner'; -import { NYM_WEBSITE } from '../api/constants'; -import { useMainContext } from '../context/main'; -import { MobileDrawerClose } from '../icons/MobileDrawerClose'; -import { Footer } from './Footer'; -import { DarkLightSwitchDesktop } from './Switch'; -import { NavOptionType } from '../context/nav'; -import { ConnectKeplrWallet } from './Wallet/ConnectKeplrWallet'; - -const drawerWidth = 255; -const bannerHeight = 80; - -const openedMixin = (theme: Theme): CSSObject => ({ - width: drawerWidth, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, - }), - overflowX: 'hidden', -}); - -const closedMixin = (theme: Theme): CSSObject => ({ - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - overflowX: 'hidden', - width: `calc(${theme.spacing(7)} + 1px)`, -}); - -const DrawerHeader = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), - height: 64, -})); - -const Drawer = styled(MuiDrawer, { - shouldForwardProp: (prop) => prop !== 'open', -})(({ theme, open }) => ({ - width: drawerWidth, - flexShrink: 0, - whiteSpace: 'nowrap', - boxSizing: 'border-box', - ...(open && { - ...openedMixin(theme), - '& .MuiDrawer-paper': openedMixin(theme), - }), - ...(!open && { - ...closedMixin(theme), - '& .MuiDrawer-paper': closedMixin(theme), - }), -})); - -type ExpandableButtonType = { - title: string; - url: string; - isActive?: boolean; - Icon?: React.ReactNode; - nested?: NavOptionType[]; - isChild?: boolean; - openDrawer: () => void; - closeDrawer?: () => void; - drawIsTempOpen: boolean; - drawIsFixed: boolean; - fixDrawerClose?: () => void; - isMobile: boolean; - setToActive: (url: string) => void; -}; - -export const ExpandableButton: FCWithChildren<ExpandableButtonType> = ({ - url, - setToActive, - isActive, - openDrawer, - closeDrawer, - drawIsTempOpen, - drawIsFixed, - fixDrawerClose, - Icon, - title, - nested, - isMobile, - isChild, -}) => { - const [dynamicStyle, setDynamicStyle] = React.useState({}); - const [nestedOptions, toggleNestedOptions] = React.useState(false); - const [isExternal, setIsExternal] = React.useState<boolean>(false); - const { palette } = useTheme(); - - const handleClick = () => { - setToActive(url); - if (title === 'Network Components' && nested) { - openDrawer(); - toggleNestedOptions(!nestedOptions); - } - if (!nested && !drawIsFixed) { - closeDrawer?.(); - } - if (!nested && isMobile) { - fixDrawerClose?.(); - } - }; - - React.useEffect(() => { - if (url) { - setIsExternal(url.includes('http')); - } - if (nested) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - }); - } - if (isChild) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.nested, - fontWeight: 600, - }); - } - if (!nested && !isChild) { - setDynamicStyle({ - background: palette.nym.networkExplorer.nav.selected.main, - borderRight: `3px solid ${palette.nym.highlight}`, - }); - } - }, [url]); - - React.useEffect(() => { - if (!drawIsTempOpen && nestedOptions) { - toggleNestedOptions(false); - } - }, [drawIsTempOpen]); - - const linkProps = isExternal - ? { - component: 'a', - href: url, - target: '_blank', - } - : { component: !nested ? Link : 'div', to: url }; - - return ( - <> - <ListItem - disablePadding - disableGutters - {...linkProps} - sx={{ - borderBottom: isChild ? 'none' : '1px solid rgba(255, 255, 255, 0.1)', - ...(isActive - ? dynamicStyle - : { - background: palette.nym.networkExplorer.nav.background, - borderRight: 'none', - }), - }} - > - <ListItemButton - onClick={handleClick} - sx={{ - pt: 2, - pb: 2, - background: isChild ? palette.nym.networkExplorer.nav.selected.nested : 'none', - }} - > - <ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon> - <ListItemText - primary={title} - sx={{ - color: palette.nym.networkExplorer.nav.text, - }} - primaryTypographyProps={{ - style: { - fontWeight: isActive ? 600 : 400, - }, - }} - /> - {nested && nestedOptions && <ExpandLess />} - {nested && !nestedOptions && <ExpandMore />} - </ListItemButton> - </ListItem> - {nestedOptions && - nested?.map((each) => ( - <ExpandableButton - url={each.url} - key={each.title} - title={each.title} - openDrawer={openDrawer} - drawIsTempOpen={drawIsTempOpen} - closeDrawer={closeDrawer} - setToActive={setToActive} - drawIsFixed={drawIsFixed} - fixDrawerClose={fixDrawerClose} - isMobile={isMobile} - isChild - /> - ))} - </> - ); -}; - -ExpandableButton.defaultProps = { - Icon: null, - nested: undefined, - isChild: false, - isActive: false, - fixDrawerClose: undefined, - closeDrawer: undefined, -}; - -export const Nav: FCWithChildren = ({ children }) => { - const { updateNavState, navState, environment } = useMainContext(); - const [drawerIsOpen, setDrawerToOpen] = React.useState(false); - const [fixedOpen, setFixedOpen] = React.useState(false); - // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(false); - const theme = useTheme(); - - const explorerName = - `${environment && environment.charAt(0).toUpperCase() + environment.slice(1)} Explorer` || 'Mainnet Explorer'; - - const switchNetworkText = environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'; - const switchNetworkLink = - environment === 'mainnet' ? 'https://sandbox-explorer.nymtech.net' : 'https://explorer.nymtech.net'; - - const setToActive = (url: string) => { - updateNavState(url); - }; - - const fixDrawerOpen = () => { - setFixedOpen(true); - setDrawerToOpen(true); - }; - - const fixDrawerClose = () => { - setFixedOpen(false); - setDrawerToOpen(false); - }; - - const tempDrawerOpen = () => { - if (!fixedOpen) { - setDrawerToOpen(true); - } - }; - - const tempDrawerClose = () => { - if (!fixedOpen) { - setDrawerToOpen(false); - } - }; - - return ( - <Box sx={{ display: 'flex' }}> - <AppBar - sx={{ - background: theme.palette.nym.networkExplorer.topNav.appBar, - borderRadius: 0, - }} - > - <MaintenanceBanner open={openMaintenance} onClick={() => setOpenMaintenance(false)} height={bannerHeight} /> - <Toolbar - disableGutters - sx={{ - display: 'flex', - justifyContent: 'space-between', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - ml: 0.5, - }} - > - <IconButton component="a" href={NYM_WEBSITE} target="_blank"> - <NymLogo height="40px" width="40px" /> - </IconButton> - <Typography - variant="h6" - noWrap - sx={{ - color: theme.palette.nym.networkExplorer.nav.text, - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink component={Link} to="/" underline="none" color="inherit"> - {explorerName} - </MuiLink> - <Button - size="small" - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ borderRadius: 2, textTransform: 'none', width: 150, ml: 4, fontSize: 14, fontWeight: 600 }} - > - {switchNetworkText} - </Button> - </Typography> - </Box> - <Box - sx={{ - mr: 2, - alignItems: 'center', - display: 'flex', - }} - > - <Box - sx={{ - display: 'flex', - flexDirection: 'row', - width: 'auto', - pr: 0, - pl: 2, - justifyContent: 'flex-end', - alignItems: 'center', - }} - > - <Box sx={{ mr: 1 }}> - <ConnectKeplrWallet /> - </Box> - <DarkLightSwitchDesktop defaultChecked /> - </Box> - </Box> - </Toolbar> - </AppBar> - <Drawer - variant="permanent" - open={drawerIsOpen} - PaperProps={{ - style: { - background: theme.palette.nym.networkExplorer.nav.background, - borderRadius: 0, - top: openMaintenance ? bannerHeight : 0, - }, - }} - > - <DrawerHeader - sx={{ - borderBottom: '1px solid rgba(255, 255, 255, 0.1)', - justifyContent: 'flex-start', - paddingLeft: 0, - }} - > - <IconButton - onClick={drawerIsOpen ? fixDrawerClose : fixDrawerOpen} - sx={{ - padding: 1, - ml: 1, - color: theme.palette.nym.networkExplorer.nav.text, - }} - > - {drawerIsOpen ? <MobileDrawerClose /> : <Menu />} - </IconButton> - </DrawerHeader> - - <List sx={{ pt: 0, pb: 0 }} onMouseEnter={tempDrawerOpen} onMouseLeave={tempDrawerClose}> - {navState.map((props) => ( - <ExpandableButton - key={props.url} - closeDrawer={tempDrawerClose} - drawIsTempOpen={drawerIsOpen} - drawIsFixed={fixedOpen} - fixDrawerClose={fixDrawerClose} - openDrawer={tempDrawerOpen} - setToActive={setToActive} - isMobile={false} - {...props} - /> - ))} - </List> - </Drawer> - <Box sx={{ width: '100%', py: 5, px: 6, mt: 7 }}> - {children} - <Footer /> - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/NetworkTitle.tsx b/explorer/src/components/NetworkTitle.tsx deleted file mode 100644 index 7dd87d7e42..0000000000 --- a/explorer/src/components/NetworkTitle.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Button, Typography } from '@mui/material'; -import { Link } from 'react-router-dom'; -import MuiLink from '@mui/material/Link'; -import { useMainContext } from '@src/context/main'; - -type NetworkTitleProps = { - showToggleNetwork?: boolean; -}; - -const NetworkTitle = ({ showToggleNetwork }: NetworkTitleProps) => { - const { environment } = useMainContext(); - - const explorerName = - `${environment && environment.charAt(0).toUpperCase() + environment.slice(1)} Explorer` || 'Mainnet Explorer'; - - const switchNetworkText = environment === 'mainnet' ? 'Switch to Testnet' : 'Switch to Mainnet'; - const switchNetworkLink = - environment === 'mainnet' ? 'https://sandbox-explorer.nymtech.net' : 'https://explorer.nymtech.net'; - return ( - <Typography - variant="h6" - noWrap - sx={{ - color: 'nym.networkExplorer.nav.text', - fontSize: '18px', - fontWeight: 600, - }} - > - <MuiLink component={Link} to="/overview" underline="none" color="inherit" fontWeight={700}> - {explorerName} - </MuiLink> - {showToggleNetwork && ( - <Button - variant="outlined" - color="inherit" - href={switchNetworkLink} - sx={{ textTransform: 'none', width: 114, fontSize: '12px', fontWeight: 600, ml: 1 }} - > - {switchNetworkText} - </Button> - )} - </Typography> - ); -}; - -export default NetworkTitle; diff --git a/explorer/src/components/Socials.tsx b/explorer/src/components/Socials.tsx deleted file mode 100644 index e1bb33970b..0000000000 --- a/explorer/src/components/Socials.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { Box, IconButton } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { TelegramIcon } from '../icons/socials/TelegramIcon'; -import { GitHubIcon } from '../icons/socials/GitHubIcon'; -import { TwitterIcon } from '../icons/socials/TwitterIcon'; -import { DiscordIcon } from '../icons/socials/DiscordIcon'; - -// socials -export const TELEGRAM_LINK = 'https://nymtech.net/go/telegram'; -export const TWITTER_LINK = 'https://nymtech.net/go/x'; -export const GITHUB_LINK = 'https://nymtech.net/go/github'; -export const DISCORD_LINK = 'https://nymtech.net/go/discord'; - -export const Socials: FCWithChildren<{ isFooter?: boolean }> = ({ isFooter }) => { - const theme = useTheme(); - const color = isFooter - ? theme.palette.nym.networkExplorer.footer.socialIcons - : theme.palette.nym.networkExplorer.topNav.socialIcons; - return ( - <Box> - <IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram"> - <TelegramIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord"> - <DiscordIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter"> - <TwitterIcon color={color} size={24} /> - </IconButton> - <IconButton component="a" href={GITHUB_LINK} target="_blank" data-testid="github"> - <GitHubIcon color={color} size={24} /> - </IconButton> - </Box> - ); -}; - -Socials.defaultProps = { - isFooter: false, -}; diff --git a/explorer/src/components/StatsCard.tsx b/explorer/src/components/StatsCard.tsx deleted file mode 100644 index 36355b1759..0000000000 --- a/explorer/src/components/StatsCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react'; -import { Box, Card, CardContent, IconButton, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import EastIcon from '@mui/icons-material/East'; - -interface StatsCardProps { - icon: React.ReactNode; - title: string; - count?: string | number; - errorMsg?: Error | string; - onClick?: () => void; - color?: string; -} -export const StatsCard: FCWithChildren<StatsCardProps> = ({ - icon, - title, - count, - onClick, - errorMsg, - color: colorProp, -}) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <Card onClick={onClick} sx={{ height: '100%' }}> - <CardContent - sx={{ - padding: 1.5, - paddingLeft: 3, - '&:last-child': { - paddingBottom: 1.5, - }, - cursor: 'pointer', - fontSize: 14, - fontWeight: 600, - }} - > - <Box display="flex" alignItems="center" color={color}> - <Box display="flex"> - {icon} - <Typography ml={3} mr={0.75} fontSize="inherit" fontWeight="inherit" data-testid={`${title}-amount`}> - {count === undefined || count === null ? '' : count} - </Typography> - <Typography mr={1} fontSize="inherit" fontWeight="inherit" data-testid={title}> - {title} - </Typography> - </Box> - <IconButton color="inherit" sx={{ fontSize: '16px' }}> - <EastIcon fontSize="inherit" /> - </IconButton> - </Box> - {errorMsg && ( - <Typography variant="body2" sx={{ color: 'danger', padding: 2 }}> - {typeof errorMsg === 'string' ? errorMsg : errorMsg.message || 'Oh no! An error occurred'} - </Typography> - )} - </CardContent> - </Card> - ); -}; - -StatsCard.defaultProps = { - onClick: undefined, - errorMsg: undefined, - color: undefined, - count: undefined, -}; diff --git a/explorer/src/components/StyledLink.tsx b/explorer/src/components/StyledLink.tsx deleted file mode 100644 index fd101e5b4a..0000000000 --- a/explorer/src/components/StyledLink.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Link as MuiLink, SxProps } from '@mui/material'; -import { Link as RRDL } from 'react-router-dom'; - -type StyledLinkProps = { - to: string; - children: string; - target?: React.HTMLAttributeAnchorTarget; - dataTestId?: string; - color?: string; - sx?: SxProps; -}; - -const StyledLink = ({ to, children, dataTestId, target, color = 'inherit', sx }: StyledLinkProps) => ( - <MuiLink - sx={{ ...sx }} - color={color} - target={target} - underline="none" - component={RRDL} - to={to} - data-testid={dataTestId} - > - {children} - </MuiLink> -); - -export default StyledLink; diff --git a/explorer/src/components/Switch.tsx b/explorer/src/components/Switch.tsx deleted file mode 100644 index 840530a327..0000000000 --- a/explorer/src/components/Switch.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import Switch from '@mui/material/Switch'; -import { Button } from '@mui/material'; -import { useMainContext } from '../context/main'; -import { LightSwitchSVG } from '../icons/LightSwitchSVG'; - -export const DarkLightSwitch = styled(Switch)(({ theme }) => ({ - width: 55, - height: 34, - padding: 7, - '& .MuiSwitch-switchBase': { - margin: 1, - padding: 2, - transform: 'translateX(4px)', - '&.Mui-checked': { - color: '#fff', - transform: 'translateX(22px)', - '& .MuiSwitch-thumb:before': { - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>\')', - }, - '& + .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - }, - }, - }, - '& .MuiSwitch-thumb': { - backgroundColor: theme.palette.nym.networkExplorer.nav.text, - width: 25, - height: 25, - marginTop: '2px', - '&:before': { - content: "''", - position: 'absolute', - width: '100%', - height: '100%', - left: 0, - top: 0, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: - 'url(\'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="black" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>\')', - }, - }, - '& .MuiSwitch-track': { - opacity: 1, - backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be', - borderRadius: 20 / 2, - }, -})); - -export const DarkLightSwitchMobile: FCWithChildren = () => { - const { toggleMode } = useMainContext(); - return ( - <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}> - <LightSwitchSVG /> - </Button> - ); -}; - -export const DarkLightSwitchDesktop: FCWithChildren<{ defaultChecked: boolean }> = ({ defaultChecked }) => { - const { toggleMode } = useMainContext(); - return ( - <Button sx={{ paddingLeft: 0 }} onClick={() => toggleMode()} data-testid="switch-button"> - <DarkLightSwitch defaultChecked={defaultChecked} /> - </Button> - ); -}; diff --git a/explorer/src/components/TableToolbar.tsx b/explorer/src/components/TableToolbar.tsx deleted file mode 100644 index d3af522168..0000000000 --- a/explorer/src/components/TableToolbar.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { Box, TextField, MenuItem, FormControl, IconButton, Select, SelectChangeEvent } from '@mui/material'; -import { Close } from '@mui/icons-material'; -import { Filters } from './Filters/Filters'; -import { useIsMobile } from '../hooks/useIsMobile'; - -const fieldsHeight = '42.25px'; - -type TableToolBarProps = { - onChangeSearch?: (arg: string) => void; - onChangePageSize: (event: SelectChangeEvent<string>) => void; - pageSize: string; - searchTerm?: string; - withFilters?: boolean; - childrenBefore?: React.ReactNode; - childrenAfter?: React.ReactNode; -}; - -export const TableToolbar: FCWithChildren<TableToolBarProps> = ({ - searchTerm, - onChangeSearch, - onChangePageSize, - pageSize, - childrenBefore, - childrenAfter, - withFilters, -}) => { - const isMobile = useIsMobile(); - return ( - <Box - sx={{ - width: '100%', - marginBottom: 2, - display: 'flex', - flexDirection: isMobile ? 'column' : 'row', - justifyContent: 'space-between', - }} - > - <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column-reverse' : 'row', alignItems: 'middle' }}> - <Box sx={{ display: 'flex', justifyContent: 'space-between', height: fieldsHeight }}> - {childrenBefore} - <FormControl size="small"> - <Select - value={pageSize} - onChange={onChangePageSize} - sx={{ - width: isMobile ? '100%' : 200, - marginRight: isMobile ? 0 : 2, - }} - > - <MenuItem value={10} data-testid="ten"> - 10 - </MenuItem> - <MenuItem value={30} data-testid="thirty"> - 30 - </MenuItem> - <MenuItem value={50} data-testid="fifty"> - 50 - </MenuItem> - <MenuItem value={100} data-testid="hundred"> - 100 - </MenuItem> - </Select> - </FormControl> - </Box> - {!!onChangeSearch && ( - <TextField - sx={{ - width: isMobile ? '100%' : 200, - marginBottom: isMobile ? 2 : 0, - }} - size="small" - value={searchTerm} - data-testid="search-box" - placeholder="Search" - InputProps={{ - endAdornment: searchTerm?.length ? ( - <IconButton size="small" onClick={() => onChangeSearch('')}> - <Close fontSize="small" /> - </IconButton> - ) : undefined, - }} - onChange={(event) => onChangeSearch(event.target.value)} - /> - )} - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'end', - gap: 1, - marginTop: isMobile ? 2 : 0, - }} - > - {withFilters && <Filters />} - {childrenAfter} - </Box> - </Box> - ); -}; diff --git a/explorer/src/components/Title.tsx b/explorer/src/components/Title.tsx deleted file mode 100644 index 032837d3c8..0000000000 --- a/explorer/src/components/Title.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { Typography } from '@mui/material'; - -export const Title: FCWithChildren<{ text: string }> = ({ text }) => ( - <Typography - variant="h5" - sx={{ - fontWeight: 600, - }} - data-testid={text} - > - {text} - </Typography> -); diff --git a/explorer/src/components/Tooltip.tsx b/explorer/src/components/Tooltip.tsx deleted file mode 100644 index 0febd9f891..0000000000 --- a/explorer/src/components/Tooltip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; - -type ValueType<T> = T[keyof T]; - -type Props = { - text: string; - id: string; - placement?: ValueType<Pick<TooltipProps, 'placement'>>; - tooltipSx?: TooltipComponentsPropsOverrides; - children: React.ReactNode; -}; - -export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( - <MUITooltip - title={text} - id={id} - placement={placement || 'top-start'} - componentsProps={{ - tooltip: { - sx: { - maxWidth: 200, - background: (t) => t.palette.nym.networkExplorer.tooltip.background, - color: (t) => t.palette.nym.networkExplorer.tooltip.color, - '& .MuiTooltip-arrow': { - color: (t) => t.palette.nym.networkExplorer.tooltip.background, - }, - }, - ...tooltipSx, - }, - }} - arrow - > - {children as ReactElement<any, any>} - </MUITooltip> -); diff --git a/explorer/src/components/TwoColSmallTable.tsx b/explorer/src/components/TwoColSmallTable.tsx deleted file mode 100644 index a211c29bdd..0000000000 --- a/explorer/src/components/TwoColSmallTable.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableRow from '@mui/material/TableRow'; -import Paper from '@mui/material/Paper'; -import CheckCircleSharpIcon from '@mui/icons-material/CheckCircleSharp'; -import ErrorIcon from '@mui/icons-material/Error'; - -interface TableProps { - title?: string; - icons?: boolean[]; - keys: string[]; - values: number[]; - marginBottom?: boolean; - error?: string; - loading: boolean; -} - -export const TwoColSmallTable: FCWithChildren<TableProps> = ({ - loading, - title, - icons, - keys, - values, - marginBottom, - error, -}) => ( - <> - {title && <Typography sx={{ marginTop: 2 }}>{title}</Typography>} - - <TableContainer component={Paper} sx={marginBottom ? { marginBottom: 4, marginTop: 2 } : { marginTop: 2 }}> - <Table aria-label="two col small table"> - <TableBody> - {keys.map((each: string, i: number) => ( - <TableRow key={each}> - {icons && <TableCell>{icons[i] ? <CheckCircleSharpIcon /> : <ErrorIcon />}</TableCell>} - <TableCell sx={error ? { opacity: 0.4 } : null} data-testid={each.replace(/ /g, '')}> - {each} - </TableCell> - <TableCell - sx={error ? { opacity: 0.4 } : null} - align="right" - data-testid={`${each.replace(/ /g, '-')}-value`} - > - {values[i]} - </TableCell> - {error && ( - <TableCell align="right" sx={{ opacity: 0.4 }}> - {values[i]} - </TableCell> - )} - {!error && loading && ( - <TableCell align="right"> - <CircularProgress /> - </TableCell> - )} - {error && !icons && ( - <TableCell sx={{ opacity: 0.2 }} align="right"> - <ErrorIcon /> - </TableCell> - )} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> -); - -TwoColSmallTable.defaultProps = { - title: undefined, - icons: undefined, - marginBottom: false, - error: undefined, -}; diff --git a/explorer/src/components/Universal-DataGrid.tsx b/explorer/src/components/Universal-DataGrid.tsx deleted file mode 100644 index 6598dfb485..0000000000 --- a/explorer/src/components/Universal-DataGrid.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import * as React from 'react'; -import { makeStyles } from '@mui/styles'; -import { DataGrid, GridColDef, GridEventListener, useGridApiContext, useGridState } from '@mui/x-data-grid'; -import Pagination from '@mui/material/Pagination'; -import { LinearProgress } from '@mui/material'; -import { GridInitialStateCommunity } from '@mui/x-data-grid/models/gridStateCommunity'; - -const useStyles = makeStyles({ - root: { - display: 'flex', - }, -}); - -const CustomPagination = () => { - const apiRef = useGridApiContext(); - const [state] = useGridState(apiRef); - - const classes = useStyles(); - - return ( - <Pagination - className={classes.root} - sx={{ mt: 2 }} - color="primary" - count={state.pagination.pageCount} - page={state.pagination.page + 1} - onChange={(_, value) => apiRef.current.setPage(value - 1)} - /> - ); -}; - -type DataGridProps = { - columns: GridColDef[]; - pagination?: true | undefined; - pageSize?: string | undefined; - rows: any; - loading?: boolean; - initialState?: GridInitialStateCommunity; - onRowClick?: GridEventListener<'rowClick'> | undefined; -}; -export const UniversalDataGrid: FCWithChildren<DataGridProps> = ({ - rows, - columns, - loading, - pagination, - pageSize, - initialState, - onRowClick, -}) => { - if (loading) return <LinearProgress />; - - return ( - <DataGrid - onRowClick={onRowClick} - pagination={pagination} - rows={rows} - components={{ - Pagination: CustomPagination, - }} - columns={columns} - pageSize={Number(pageSize)} - disableSelectionOnClick - autoHeight - hideFooter={!pagination} - initialState={initialState} - style={{ - width: '100%', - border: 'none', - }} - sx={{ - '*::-webkit-scrollbar': { - width: '1em', - }, - '*::-webkit-scrollbar-track': { - background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, - outline: (t) => `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, - boxShadow: 'auto', - borderRadius: 'auto', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, - borderRadius: '20px', - width: '.4em', - border: (t) => `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, - shadow: 'auto', - }, - }} - /> - ); -}; - -UniversalDataGrid.defaultProps = { - loading: false, - pagination: undefined, - pageSize: '10', -}; diff --git a/explorer/src/components/UptimeChart.tsx b/explorer/src/components/UptimeChart.tsx deleted file mode 100644 index 00a981a3af..0000000000 --- a/explorer/src/components/UptimeChart.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { Chart } from 'react-google-charts'; -import { format } from 'date-fns'; -import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; - -interface ChartProps { - title?: string; - xLabel: string; - yLabel?: string; - uptimeStory: ApiState<UptimeStoryResponse>; - loading: boolean; -} - -type FormattedDateRecord = [string, number]; -type FormattedChartHeadings = string[]; -type FormattedChartData = [FormattedChartHeadings | FormattedDateRecord]; - -export const UptimeChart: FCWithChildren<ChartProps> = ({ title, xLabel, yLabel, uptimeStory, loading }) => { - const [formattedChartData, setFormattedChartData] = React.useState<FormattedChartData>(); - const theme = useTheme(); - const color = theme.palette.text.primary; - React.useEffect(() => { - if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; - uptimeStory.data.history.forEach((eachDate) => { - const formattedDateUptimeRecord: FormattedDateRecord = [ - format(new Date(eachDate.date), 'MMM dd'), - eachDate.uptime, - ]; - allFormattedChartData.push(formattedDateUptimeRecord); - }); - setFormattedChartData(allFormattedChartData); - } else { - const emptyData: any = [ - ['Date', 'Score'], - ['Jul 27', 10], - ]; - setFormattedChartData(emptyData); - } - }, [uptimeStory]); - - return ( - <> - {title && <Typography>{title}</Typography>} - {loading && <CircularProgress />} - - {!loading && uptimeStory && ( - <Chart - style={{ minHeight: 480 }} - chartType="LineChart" - loader={<p>...</p>} - data={ - uptimeStory.data - ? formattedChartData - : [ - ['Date', 'Routing Score'], - [format(new Date(Date.now()), 'MMM dd'), 0], - ] - } - options={{ - backgroundColor: - theme.palette.mode === 'dark' ? theme.palette.nym.networkExplorer.background.tertiary : undefined, - color: uptimeStory.error ? 'rgba(255, 255, 255, 0.4)' : 'rgba(255, 255, 255, 1)', - colors: ['#FB7A21'], - legend: { - textStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - - intervals: { style: 'sticks' }, - hAxis: { - // horizontal / date - title: xLabel, - titleTextStyle: { - color, - }, - textStyle: { - color, - // fontSize: 11 - }, - gridlines: { - count: -1, - }, - }, - vAxis: { - // vertical / % Routing Score - viewWindow: { - min: 0, - max: 100, - }, - title: yLabel, - titleTextStyle: { - color, - opacity: uptimeStory.error ? 0.4 : 1, - }, - textStyle: { - color, - fontSize: 11, - opacity: uptimeStory.error ? 0.4 : 1, - }, - }, - }} - /> - )} - </> - ); -}; - -UptimeChart.defaultProps = { - title: undefined, -}; diff --git a/explorer/src/components/Wallet/ConnectKeplrWallet.tsx b/explorer/src/components/Wallet/ConnectKeplrWallet.tsx deleted file mode 100644 index ff5d2691c6..0000000000 --- a/explorer/src/components/Wallet/ConnectKeplrWallet.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { Button, IconButton, Stack, CircularProgress } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import { useIsMobile } from '@src/hooks/useIsMobile'; -import { useWalletContext } from '@src/context/wallet'; -import { WalletAddress, WalletBalance } from '@src/components/Wallet'; - -export const ConnectKeplrWallet = () => { - const { connectWallet, disconnectWallet, isWalletConnected, isWalletConnecting } = useWalletContext(); - const isMobile = useIsMobile(1200); - - if (!connectWallet || !disconnectWallet) { - return null; - } - - if (isWalletConnected) { - return ( - <Stack direction="row" spacing={1}> - <WalletBalance /> - <WalletAddress /> - <IconButton - size="small" - onClick={async () => { - await disconnectWallet(); - }} - > - <CloseIcon fontSize="small" sx={{ color: 'white' }} /> - </IconButton> - </Stack> - ); - } - - return ( - <Button - variant="outlined" - onClick={() => connectWallet()} - disabled={isWalletConnecting} - endIcon={isWalletConnecting && <CircularProgress size={14} color="inherit" />} - > - Connect {isMobile ? '' : ' Wallet'} - </Button> - ); -}; diff --git a/explorer/src/components/Wallet/WalletAddress.tsx b/explorer/src/components/Wallet/WalletAddress.tsx deleted file mode 100644 index 8be6c9f291..0000000000 --- a/explorer/src/components/Wallet/WalletAddress.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { ElipsSVG } from '@src/icons/ElipsSVG'; -import { trimAddress } from '@src/utils'; -import { useWalletContext } from '@src/context/wallet'; - -export const WalletAddress = () => { - const { address } = useWalletContext(); - - const displayAddress = trimAddress(address, 7); - - return ( - <Box display="flex" alignItems="center" gap={0.5}> - <ElipsSVG /> - <Typography variant="body1" fontWeight={600}> - {displayAddress} - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Wallet/WalletBalance.tsx b/explorer/src/components/Wallet/WalletBalance.tsx deleted file mode 100644 index f94935ac23..0000000000 --- a/explorer/src/components/Wallet/WalletBalance.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { useWalletContext } from '@src/context/wallet'; -import { useIsMobile } from '@src/hooks'; -import { TokenSVG } from '@src/icons/TokenSVG'; - -export const WalletBalance = () => { - const { balance } = useWalletContext(); - const isMobile = useIsMobile(1200); - - const showBalance = !isMobile && balance.status === 'success'; - - if (!showBalance) { - return null; - } - - return ( - <Box display="flex" alignItems="center" gap={1}> - <TokenSVG /> - <Typography variant="body1" fontWeight={600}> - {balance.data} NYM - </Typography> - </Box> - ); -}; diff --git a/explorer/src/components/Wallet/index.ts b/explorer/src/components/Wallet/index.ts deleted file mode 100644 index 645c8b4ffb..0000000000 --- a/explorer/src/components/Wallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './WalletBalance'; -export * from './WalletAddress'; diff --git a/explorer/src/components/WorldMap.tsx b/explorer/src/components/WorldMap.tsx deleted file mode 100644 index 7a86f34095..0000000000 --- a/explorer/src/components/WorldMap.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import * as React from 'react'; -import { scaleLinear } from 'd3-scale'; -import { ComposableMap, Geographies, Geography, Marker, ZoomableGroup } from 'react-simple-maps'; -import ReactTooltip from 'react-tooltip'; -import { CircularProgress } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { ApiState, CountryDataResponse } from '../typeDefs/explorer-api'; -import MAP_TOPOJSON from '../assets/world-110m.json'; - -type MapProps = { - userLocation?: [number, number]; - countryData?: ApiState<CountryDataResponse>; - loading: boolean; -}; - -export const WorldMap: FCWithChildren<MapProps> = ({ countryData, userLocation, loading }) => { - const { palette } = useTheme(); - - const colorScale = React.useMemo(() => { - if (countryData?.data) { - const heighestNumberOfNodes = Math.max(...Object.values(countryData.data).map((country) => country.nodes)); - return scaleLinear<string, string>() - .domain([0, 1, heighestNumberOfNodes / 4, heighestNumberOfNodes / 2, heighestNumberOfNodes]) - .range(palette.nym.networkExplorer.map.fills) - .unknown(palette.nym.networkExplorer.map.fills[0]); - } - return () => palette.nym.networkExplorer.map.fills[0]; - }, [countryData, palette]); - - const [tooltipContent, setTooltipContent] = React.useState<string | null>(null); - - if (loading) { - return <CircularProgress />; - } - - return ( - <> - <ComposableMap - data-tip="" - style={{ - backgroundColor: palette.nym.networkExplorer.background.tertiary, - width: '100%', - height: 'auto', - }} - viewBox="0, 50, 800, 350" - projection="geoMercator" - projectionConfig={{ - scale: userLocation ? 200 : 100, - center: userLocation, - }} - > - <ZoomableGroup> - <Geographies geography={MAP_TOPOJSON}> - {({ geographies }) => - geographies.map((geo) => { - const d = (countryData?.data || {})[geo.properties.ISO_A3]; - return ( - <Geography - key={geo.rsmKey} - geography={geo} - fill={colorScale(d?.nodes || 0)} - stroke={palette.nym.networkExplorer.map.stroke} - strokeWidth={0.2} - onMouseEnter={() => { - const { NAME_LONG } = geo.properties; - if (!userLocation) { - setTooltipContent(`${NAME_LONG} | ${d?.nodes || 0}`); - } - }} - onMouseLeave={() => { - setTooltipContent(''); - }} - style={{ - hover: - !userLocation && countryData - ? { - fill: palette.nym.highlight, - outline: 'white', - } - : undefined, - }} - /> - ); - }) - } - </Geographies> - - {userLocation && ( - <Marker coordinates={userLocation}> - <g - fill="grey" - stroke="#FF5533" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - transform="translate(-12, -10)" - > - <circle cx="12" cy="10" r="5" /> - </g> - </Marker> - )} - </ZoomableGroup> - </ComposableMap> - <ReactTooltip>{tooltipContent}</ReactTooltip> - </> - ); -}; - -WorldMap.defaultProps = { - userLocation: undefined, - countryData: undefined, -}; diff --git a/explorer/src/components/delegatorsInfo/types.ts b/explorer/src/components/delegatorsInfo/types.ts deleted file mode 100644 index b0e3fd6e99..0000000000 --- a/explorer/src/components/delegatorsInfo/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type RowsType = { - value?: string | number; - visualProgressValue?: number; -}; - -export interface DelegatorsInfoRow { - estimated_total_reward: RowsType; - estimated_operator_reward: RowsType; - active_set_probability: RowsType; - stake_saturation: RowsType; - profit_margin: RowsType; - avg_uptime: RowsType; -} - -export type DelegatorsInfoRowWithIndex = DelegatorsInfoRow & { id: number }; diff --git a/explorer/src/components/index.ts b/explorer/src/components/index.ts deleted file mode 100644 index 92eeebd010..0000000000 --- a/explorer/src/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './CustomColumnHeading'; -export * from './Title'; -export * from './Universal-DataGrid'; -export * from './Tooltip'; -export { default as StyledLink } from './StyledLink'; -export * from './Delegations'; -export * from './MixNodes'; -export * from './TableToolbar'; -export * from './Icons'; diff --git a/explorer/src/context/cosmos-kit.tsx b/explorer/src/context/cosmos-kit.tsx deleted file mode 100644 index ce82b21a17..0000000000 --- a/explorer/src/context/cosmos-kit.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { ChainProvider } from '@cosmos-kit/react'; -import { wallets as keplr } from '@cosmos-kit/keplr-extension'; -import { assets, chains } from 'chain-registry'; -import { Chain, AssetList } from '@chain-registry/types'; -import { VALIDATOR_BASE_URL } from '@src/api/constants'; - -const nymSandbox: Chain = { - chain_name: 'sandbox', - chain_id: 'sandbox', - bech32_prefix: 'n', - network_type: 'devnet', - pretty_name: 'Nym Sandbox', - status: 'active', - slip44: 118, - apis: { - rpc: [ - { - address: 'https://rpc.sandbox.nymtech.net', - }, - ], - }, -}; - -const nymSandboxAssets = { - chain_name: 'sandbox', - assets: [ - { - name: 'Nym', - base: 'unym', - symbol: 'NYM', - display: 'NYM', - denom_units: [], - }, - ], -}; - -const CosmosKitProvider = ({ children }: { children: React.ReactNode }) => { - // Only use the nyx chains - const chainsFixedUp = React.useMemo(() => { - const nyx = chains.find((chain) => chain.chain_id === 'nyx'); - - return nyx ? [nymSandbox, nyx] : [nymSandbox]; - }, [chains]); - - // Only use the nyx assets - const assetsFixedUp = React.useMemo(() => { - const nyx = assets.find((asset) => asset.chain_name === 'nyx'); - - return nyx ? [nyx] : [nymSandboxAssets]; - }, [assets]) as AssetList[]; - - return ( - <ChainProvider - chains={chainsFixedUp} - assetLists={assetsFixedUp} - wallets={[...keplr]} - endpointOptions={{ - endpoints: { - nyx: { - rpc: [VALIDATOR_BASE_URL], - }, - }, - }} - > - {children} - </ChainProvider> - ); -}; - -export default CosmosKitProvider; diff --git a/explorer/src/context/delegations.tsx b/explorer/src/context/delegations.tsx deleted file mode 100644 index 28a50345e0..0000000000 --- a/explorer/src/context/delegations.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { Delegation, PendingEpochEvent, PendingEpochEventKind } from '@nymproject/contract-clients/Mixnet.types'; -import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'; -import { useWalletContext } from './wallet'; -import { useMainContext } from './main'; - -const fee = { gas: '1000000', amount: [{ amount: '1000000', denom: 'unym' }] }; - -export type PendingEvent = ReturnType<typeof getEventsByAddress>; - -export type DelegationWithRewards = Delegation & { - rewards: string; - identityKey: string; - pending: PendingEvent; -}; - -const getEventsByAddress = (kind: PendingEpochEventKind, address: String) => { - if ('delegate' in kind && kind.delegate.owner === address) { - return { - kind: 'delegate' as const, - mixId: kind.delegate.mix_id, - amount: kind.delegate.amount, - }; - } - - if ('undelegate' in kind && kind.undelegate.owner === address) { - return { - kind: 'undelegate' as const, - mixId: kind.undelegate.mix_id, - }; - } - - return undefined; -}; - -interface DelegationsState { - delegations?: DelegationWithRewards[]; - handleGetDelegations: () => Promise<void>; - handleDelegate: (mixId: number, amount: string) => Promise<ExecuteResult | undefined>; - handleUndelegate: (mixId: number) => Promise<ExecuteResult | undefined>; -} - -export const DelegationsContext = createContext<DelegationsState>({ - delegations: undefined, - handleGetDelegations: async () => { - throw new Error('Please connect your wallet'); - }, - handleDelegate: async () => { - throw new Error('Please connect your wallet'); - }, - handleUndelegate: async () => { - throw new Error('Please connect your wallet'); - }, -}); - -export const DelegationsProvider = ({ children }: { children: React.ReactNode }) => { - const [delegations, setDelegations] = useState<DelegationWithRewards[]>(); - const { address, nymQueryClient, nymClient } = useWalletContext(); - const { fetchMixnodes } = useMainContext(); - - const handleGetPendingEvents = async () => { - if (!nymQueryClient) { - return undefined; - } - - if (!address) { - return undefined; - } - - const response = await nymQueryClient.getPendingEpochEvents({}); - const pendingEvents: PendingEvent[] = []; - - response.events.forEach((e: PendingEpochEvent) => { - const event = getEventsByAddress(e.event.kind, address); - if (event) { - pendingEvents.push(event); - } - }); - - return pendingEvents; - }; - - const handleGetDelegationRewards = async (mixId: number) => { - if (!nymQueryClient) { - return undefined; - } - - if (!address) { - return undefined; - } - - const response = await nymQueryClient.getPendingDelegatorReward({ address, mixId }); - - return response; - }; - - const handleGetDelegations = useCallback(async () => { - if (!nymQueryClient) { - setDelegations(undefined); - return undefined; - } - - if (!address) { - setDelegations(undefined); - return undefined; - } - - // Get all mixnodes - Required to get the identity key for each delegation - const mixnodes = await fetchMixnodes(); - - // Get delegations - const delegationsResponse = await nymQueryClient.getDelegatorDelegations({ delegator: address }); - - // Get rewards for each delegation - const rewardsResponse = await Promise.all( - delegationsResponse.delegations.map((d: Delegation) => handleGetDelegationRewards(d.mix_id)), - ); - - // Get all pending events - const pendingEvents = await handleGetPendingEvents(); - - const delegationsWithRewards: DelegationWithRewards[] = []; - - // Merge delegations with rewards and pending events - delegationsResponse.delegations.forEach((d: Delegation, index: number) => { - delegationsWithRewards.push({ - ...d, - pending: pendingEvents?.find((e: PendingEvent) => (e?.mixId === d.mix_id ? e.kind : undefined)), - identityKey: mixnodes?.find((m) => m.mix_id === d.mix_id)?.mix_node.identity_key || '', - rewards: rewardsResponse[index]?.amount_earned_detailed || '0', - }); - }); - - // Add pending events that are not in the delegations list - pendingEvents?.forEach((e) => { - if (e && !delegationsWithRewards.find((d: DelegationWithRewards) => d.mix_id === e.mixId)) { - delegationsWithRewards.push({ - mix_id: e.mixId, - height: 0, - cumulative_reward_ratio: '0', - owner: address, - amount: { - amount: '0', - denom: 'unym', - }, - rewards: '0', - identityKey: mixnodes?.find((m) => m.mix_id === e.mixId)?.mix_node.identity_key || '', - pending: e, - }); - } - }); - - setDelegations(delegationsWithRewards); - - return undefined; - }, [address, nymQueryClient]); - - const handleDelegate = async (mixId: number, amount: string) => { - if (!address) { - throw new Error('Please connect your wallet'); - } - - const amountToDelegate = (Number(amount) * 1000000).toString(); - const uNymFunds = [{ amount: amountToDelegate, denom: 'unym' }]; - try { - const tx = await nymClient?.delegateToMixnode({ mixId }, fee, 'Delegation from Nym Explorer', uNymFunds); - - return tx as unknown as ExecuteResult; - } catch (e) { - console.error('Failed to delegate to mixnode', e); - throw e; - } - }; - - const handleUndelegate = async (mixId: number) => { - const tx = await nymClient?.undelegateFromMixnode({ mixId }, fee, 'Undelegation from Nym Explorer'); - - return tx as unknown as ExecuteResult; - }; - - const contextValue: DelegationsState = useMemo( - () => ({ - delegations, - handleGetDelegations, - handleDelegate, - handleUndelegate, - }), - [delegations, handleGetDelegations], - ); - - return <DelegationsContext.Provider value={contextValue}>{children}</DelegationsContext.Provider>; -}; - -export const useDelegationsContext = () => { - const context = useContext(DelegationsContext); - if (!context) { - throw new Error('useDelegationsContext must be used within a DelegationsProvider'); - } - return context; -}; diff --git a/explorer/src/context/gateway.tsx b/explorer/src/context/gateway.tsx deleted file mode 100644 index dd96634518..0000000000 --- a/explorer/src/context/gateway.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api'; -import { Api } from '../api'; -import { useApiState } from './hooks'; - -/** - * This context provides the state for a single gateway by identity key. - */ - -interface GatewayState { - uptimeReport?: ApiState<GatewayReportResponse>; - uptimeStory?: ApiState<UptimeStoryResponse>; -} - -export const GatewayContext = React.createContext<GatewayState>({}); - -export const useGatewayContext = (): React.ContextType<typeof GatewayContext> => - React.useContext<GatewayState>(GatewayContext); - -/** - * Provides a state context for a gateway by identity - * @param gatewayIdentityKey The identity key of the gateway - */ -export const GatewayContextProvider = ({ - gatewayIdentityKey, - children, -}: { - gatewayIdentityKey: string; - children: JSX.Element; -}) => { - const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>( - gatewayIdentityKey, - Api.fetchGatewayReportById, - 'Failed to fetch gateway uptime report by id', - ); - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>( - gatewayIdentityKey, - Api.fetchGatewayUptimeStoryById, - 'Failed to fetch gateway uptime history', - ); - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearUptimeReportById(); - clearUptimeHistory(); - Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]); - }, [gatewayIdentityKey]); - - const state = React.useMemo<GatewayState>( - () => ({ - uptimeReport, - uptimeStory, - }), - [uptimeReport, uptimeStory], - ); - - return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>; -}; diff --git a/explorer/src/context/hooks.ts b/explorer/src/context/hooks.ts deleted file mode 100644 index 9c5579c899..0000000000 --- a/explorer/src/context/hooks.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { ApiState } from '../typeDefs/explorer-api'; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -export const useApiState = <T>( - id: string, - fn: (argId: string) => Promise<T>, - errorMessage: string, -): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => { - // stores the state - const [value, setValue] = React.useState<ApiState<T>>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - setValue({ isLoading: true }); - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState<T> = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState<T> = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn, id, errorMessage]); - return [value, wrappedFetchFn, clearValueFn]; -}; diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx deleted file mode 100644 index c7a74a3d0d..0000000000 --- a/explorer/src/context/main.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import * as React from 'react'; -import { PaletteMode } from '@mui/material'; -import { - ApiState, - BlockResponse, - CountryDataResponse, - GatewayResponse, - MixNodeResponse, - MixnodeStatus, - SummaryOverviewResponse, - ValidatorsResponse, - Environment, - DirectoryServiceProvider, -} from '../typeDefs/explorer-api'; -import { EnumFilterKey } from '../typeDefs/filters'; -import { Api, getEnvironment } from '../api'; -import { NavOptionType, originalNavOptions } from './nav'; -import { toPercentIntegerString } from '../utils'; - -interface StateData { - summaryOverview?: ApiState<SummaryOverviewResponse>; - block?: ApiState<BlockResponse>; - countryData?: ApiState<CountryDataResponse>; - gateways?: ApiState<GatewayResponse>; - globalError?: string | undefined; - mixnodes?: ApiState<MixNodeResponse>; - mode: PaletteMode; - navState: NavOptionType[]; - validators?: ApiState<ValidatorsResponse>; - environment?: Environment; - serviceProviders?: ApiState<DirectoryServiceProvider[]>; -} - -interface StateApi { - fetchMixnodes: (status?: MixnodeStatus) => Promise<MixNodeResponse | undefined>; - filterMixnodes: (filters: any, status: any) => void; - toggleMode: () => void; - updateNavState: (title: string) => void; -} - -type State = StateData & StateApi; - -export const MainContext = React.createContext<State>({ - mode: 'dark', - updateNavState: () => null, - navState: originalNavOptions, - toggleMode: () => undefined, - filterMixnodes: () => null, - fetchMixnodes: () => Promise.resolve(undefined), -}); - -export const useMainContext = (): React.ContextType<typeof MainContext> => React.useContext<State>(MainContext); - -export const MainContextProvider: FCWithChildren = ({ children }) => { - // network explorer environment - const [environment, setEnvironment] = React.useState<Environment>(); - - // light/dark mode - const [mode, setMode] = React.useState<PaletteMode>('dark'); - - // nav state - const [navState, updateNav] = React.useState<NavOptionType[]>(originalNavOptions); - - // global / banner error messaging - const [globalError] = React.useState<string>(); - - // various APIs for Overview page - const [summaryOverview, setSummaryOverview] = React.useState<ApiState<SummaryOverviewResponse>>(); - const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>(); - const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>(); - const [validators, setValidators] = React.useState<ApiState<ValidatorsResponse>>(); - const [block, setBlock] = React.useState<ApiState<BlockResponse>>(); - const [countryData, setCountryData] = React.useState<ApiState<CountryDataResponse>>(); - const [serviceProviders, setServiceProviders] = React.useState<ApiState<DirectoryServiceProvider[]>>(); - - const toggleMode = () => setMode((m) => (m !== 'light' ? 'light' : 'dark')); - - const fetchOverviewSummary = async () => { - try { - const data = await Api.fetchOverviewSummary(); - setSummaryOverview({ data, isLoading: false }); - } catch (error) { - setSummaryOverview({ - error: error instanceof Error ? error : new Error('Overview summary api fail'), - isLoading: false, - }); - } - }; - - const fetchMixnodes = async (status?: MixnodeStatus) => { - let data; - setMixnodes((d) => ({ ...d, isLoading: true })); - try { - data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); - setMixnodes({ data, isLoading: false }); - } catch (error) { - setMixnodes({ - error: error instanceof Error ? error : new Error('Mixnode api fail'), - isLoading: false, - }); - } - return data; - }; - - const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => { - setMixnodes((d) => ({ ...d, isLoading: true })); - const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); - - const filtered = mxns?.filter( - (m) => - +m.profit_margin_percent >= filters.profitMargin[0] / 100 && - +m.profit_margin_percent <= filters.profitMargin[1] / 100 && - m.stake_saturation >= filters.stakeSaturation[0] && - m.stake_saturation <= filters.stakeSaturation[1] && - m.avg_uptime >= filters.routingScore[0] && - m.avg_uptime <= filters.routingScore[1], - ); - - setMixnodes({ data: filtered, isLoading: false }); - }; - - const fetchGateways = async () => { - setGateways((d) => ({ ...d, isLoading: true })); - try { - const data = await Api.fetchGateways(); - setGateways({ data, isLoading: false }); - } catch (error) { - setGateways({ - error: error instanceof Error ? error : new Error('Gateways api fail'), - isLoading: false, - }); - } - }; - const fetchValidators = async () => { - try { - const data = await Api.fetchValidators(); - setValidators({ data, isLoading: false }); - } catch (error) { - setValidators({ - error: error instanceof Error ? error : new Error('Validators api fail'), - isLoading: false, - }); - } - }; - const fetchBlock = async () => { - try { - const data = await Api.fetchBlock(); - setBlock({ data, isLoading: false }); - } catch (error) { - setBlock({ - error: error instanceof Error ? error : new Error('Block api fail'), - isLoading: false, - }); - } - }; - const fetchCountryData = async () => { - setCountryData({ data: undefined, isLoading: true }); - try { - const res = await Api.fetchCountryData(); - setCountryData({ data: res, isLoading: false }); - } catch (error) { - setCountryData({ - error: error instanceof Error ? error : new Error('Country Data api fail'), - isLoading: false, - }); - } - }; - - const fetchServiceProviders = async () => { - setServiceProviders({ data: undefined, isLoading: true }); - try { - const res = await Api.fetchServiceProviders(); - const resWithRoutingScorePercentage = res.map((item) => ({ - ...item, - routing_score: item.routing_score - ? `${toPercentIntegerString(item.routing_score.toString())}%` - : item.routing_score, - })); - setServiceProviders({ data: resWithRoutingScorePercentage, isLoading: false }); - } catch (error) { - setServiceProviders({ - error: error instanceof Error ? error : new Error('Service provider api fail'), - isLoading: false, - }); - } - }; - - const updateNavState = (url: string) => { - const updated = navState.map((option) => ({ - ...option, - isActive: option.url === url, - })); - updateNav(updated); - }; - - React.useEffect(() => { - if (environment === 'mainnet') { - fetchServiceProviders(); - } - }, [environment]); - - React.useEffect(() => { - setEnvironment(getEnvironment()); - Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]); - }, []); - - const state = React.useMemo<State>( - () => ({ - environment, - block, - countryData, - fetchMixnodes, - filterMixnodes, - gateways, - globalError, - mixnodes, - mode, - navState, - summaryOverview, - toggleMode, - updateNavState, - validators, - serviceProviders, - }), - [ - environment, - block, - countryData, - gateways, - globalError, - mixnodes, - mode, - navState, - summaryOverview, - validators, - serviceProviders, - ], - ); - - return <MainContext.Provider value={state}>{children}</MainContext.Provider>; -}; diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx deleted file mode 100644 index 36e43d568b..0000000000 --- a/explorer/src/context/mixnode.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import * as React from 'react'; -import { - ApiState, - DelegationsResponse, - UniqDelegationsResponse, - MixNodeDescriptionResponse, - MixNodeEconomicDynamicsStatsResponse, - MixNodeResponseItem, - StatsResponse, - StatusResponse, - UptimeStoryResponse, -} from '../typeDefs/explorer-api'; -import { Api } from '../api'; -import { useApiState } from './hooks'; -import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes'; - -/** - * This context provides the state for a single mixnode by identity key. - */ - -interface MixnodeState { - delegations?: ApiState<DelegationsResponse>; - uniqDelegations?: ApiState<UniqDelegationsResponse>; - description?: ApiState<MixNodeDescriptionResponse>; - economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>; - mixNode?: ApiState<MixNodeResponseItem | undefined>; - mixNodeRow?: MixnodeRowType; - stats?: ApiState<StatsResponse>; - status?: ApiState<StatusResponse>; - uptimeStory?: ApiState<UptimeStoryResponse>; -} - -export const MixnodeContext = React.createContext<MixnodeState>({}); - -export const useMixnodeContext = (): React.ContextType<typeof MixnodeContext> => - React.useContext<MixnodeState>(MixnodeContext); - -interface MixnodeContextProviderProps { - mixId: string; - children: React.ReactNode; -} - -/** - * Provides a state context for a mixnode by identity - * @param mixId The mixID of the mixnode - */ -export const MixnodeContextProvider: FCWithChildren<MixnodeContextProviderProps> = ({ mixId, children }) => { - const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState<MixNodeResponseItem | undefined>( - mixId, - Api.fetchMixnodeByID, - 'Failed to fetch mixnode by id', - ); - - const [mixNodeRow, setMixnodeRow] = React.useState<MixnodeRowType | undefined>(); - - const [delegations, fetchDelegations, clearDelegations] = useApiState<DelegationsResponse>( - mixId, - Api.fetchDelegationsById, - 'Failed to fetch delegations for mixnode', - ); - - const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = useApiState<UniqDelegationsResponse>( - mixId, - Api.fetchUniqDelegationsById, - 'Failed to fetch delegations for mixnode', - ); - - const [status, fetchStatus, clearStatus] = useApiState<StatusResponse>( - mixId, - Api.fetchStatusById, - 'Failed to fetch mixnode status', - ); - - const [stats, fetchStats, clearStats] = useApiState<StatsResponse>( - mixId, - Api.fetchStatsById, - 'Failed to fetch mixnode stats', - ); - - const [description, fetchDescription, clearDescription] = useApiState<MixNodeDescriptionResponse>( - mixId, - Api.fetchMixnodeDescriptionById, - 'Failed to fetch mixnode description', - ); - - const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] = - useApiState<MixNodeEconomicDynamicsStatsResponse>( - mixId, - Api.fetchMixnodeEconomicDynamicsStatsById, - 'Failed to fetch mixnode dynamics stats by id', - ); - - const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>( - mixId, - Api.fetchUptimeStoryById, - 'Failed to fetch mixnode uptime history', - ); - - React.useEffect(() => { - // when the identity key changes, remove all previous data - clearMixnodeById(); - clearDelegations(); - clearUniqDelegations(); - clearStatus(); - clearStats(); - clearDescription(); - clearEconomicDynamicsStats(); - clearUptimeHistory(); - - // fetch the mixnode, then get all the other stuff - fetchMixnodeById().then((value) => { - if (!value.data || value.error) { - setMixnodeRow(undefined); - return; - } - setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data)); - Promise.all([ - fetchDelegations(), - fetchUniqDelegations(), - fetchStatus(), - fetchStats(), - fetchDescription(), - fetchEconomicDynamicsStats(), - fetchUptimeHistory(), - ]); - }); - }, [mixId]); - - const state = React.useMemo<MixnodeState>( - () => ({ - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }), - [ - { - delegations, - uniqDelegations, - mixNode, - mixNodeRow, - description, - economicDynamicsStats, - stats, - status, - uptimeStory, - }, - ], - ); - - return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>; -}; diff --git a/explorer/src/context/nav.tsx b/explorer/src/context/nav.tsx deleted file mode 100644 index 6df749f585..0000000000 --- a/explorer/src/context/nav.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import { DelegateIcon } from '@src/icons/DelevateSVG'; -import { BIG_DIPPER } from '../api/constants'; -import { OverviewSVG } from '../icons/OverviewSVG'; -import { NodemapSVG } from '../icons/NodemapSVG'; -import { NetworkComponentsSVG } from '../icons/NetworksSVG'; - -export type NavOptionType = { - isActive?: boolean; - url: string; - title: string; - Icon?: React.ReactNode; - nested?: NavOptionType[]; - isExpandedChild?: boolean; -}; - -export const originalNavOptions: NavOptionType[] = [ - { - isActive: false, - url: '/', - title: 'Overview', - Icon: <OverviewSVG />, - }, - { - isActive: false, - url: '/network-components', - title: 'Network Components', - Icon: <NetworkComponentsSVG />, - nested: [ - { - url: '/network-components/mixnodes', - title: 'Mixnodes', - }, - { - url: '/network-components/gateways', - title: 'Gateways', - }, - { - url: `${BIG_DIPPER}/validators`, - title: 'Validators', - }, - { - url: 'network-components/service-providers', - title: 'Service Providers', - }, - ], - }, - { - isActive: false, - url: '/nodemap', - title: 'Nodemap', - Icon: <NodemapSVG />, - }, - { - isActive: false, - url: '/delegations', - title: 'Delegations', - Icon: <DelegateIcon />, - }, -]; diff --git a/explorer/src/context/wallet.tsx b/explorer/src/context/wallet.tsx deleted file mode 100644 index 8ac26ee7ca..0000000000 --- a/explorer/src/context/wallet.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; -import { Wallet } from '@cosmos-kit/core'; -import { unymToNym } from '@src/utils/currency'; -import { useNymClient } from '@src/hooks'; -import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client'; -import { COSMOS_KIT_USE_CHAIN } from '@src/api/constants'; - -interface WalletState { - balance: { status: 'loading' | 'success'; data?: string }; - address?: string; - isWalletConnected: boolean; - isWalletConnecting: boolean; - wallet?: Wallet; - nymClient?: MixnetClient; - nymQueryClient?: MixnetQueryClient; - connectWallet: () => Promise<void>; - disconnectWallet: () => Promise<void>; -} - -export const WalletContext = createContext<WalletState>({ - address: undefined, - balance: { status: 'loading', data: undefined }, - isWalletConnected: false, - isWalletConnecting: false, - nymClient: undefined, - nymQueryClient: undefined, - connectWallet: async () => { - throw new Error('Please connect your wallet'); - }, - disconnectWallet: async () => { - throw new Error('Please connect your wallet'); - }, -}); - -export const WalletProvider = ({ children }: { children: React.ReactNode }) => { - const [balance, setBalance] = useState<WalletState['balance']>({ status: 'loading', data: undefined }); - - const { connect, disconnect, wallet, address, isWalletConnected, isWalletConnecting, getCosmWasmClient } = - useChain(COSMOS_KIT_USE_CHAIN); - - const { nymClient, nymQueryClient } = useNymClient(address); - - const getBalance = async (walletAddress: string) => { - const account = await getCosmWasmClient(); - const uNYMBalance = await account.getBalance(walletAddress, 'unym'); - const NYMBalance = unymToNym(uNYMBalance.amount); - - return NYMBalance; - }; - - const init = async (walletAddress: string) => { - const walletBalance = await getBalance(walletAddress); - setBalance({ status: 'success', data: walletBalance }); - }; - - useEffect(() => { - if (isWalletConnected && address) { - init(address); - } - }, [address, isWalletConnected]); - - const handleConnectWallet = async () => { - await connect(); - }; - - const handleDisconnectWallet = async () => { - await disconnect(); - setBalance({ status: 'loading', data: undefined }); - }; - - const contextValue: WalletState = useMemo( - () => ({ - address, - balance, - wallet, - isWalletConnected, - isWalletConnecting, - nymClient, - nymQueryClient, - connectWallet: handleConnectWallet, - disconnectWallet: handleDisconnectWallet, - }), - [address, balance, wallet, isWalletConnected, isWalletConnecting, nymClient, nymQueryClient], - ); - - return <WalletContext.Provider value={contextValue}>{children}</WalletContext.Provider>; -}; - -export const useWalletContext = () => useContext(WalletContext); diff --git a/explorer/src/errors/ErrorBoundaryContent.tsx b/explorer/src/errors/ErrorBoundaryContent.tsx deleted file mode 100644 index 0389588e26..0000000000 --- a/explorer/src/errors/ErrorBoundaryContent.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; -import { FallbackProps } from 'react-error-boundary'; -import { Alert, AlertTitle, Container } from '@mui/material'; -import { NymThemeProvider } from '@nymproject/mui-theme'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; - -export const ErrorBoundaryContent: FCWithChildren<FallbackProps> = ({ error }) => ( - <NymThemeProvider mode="dark"> - <Container sx={{ py: 4 }}> - <NymLogo height="75px" width="75px" /> - <h1>Oh no! Sorry, something went wrong</h1> - <Alert severity="error" data-testid="error-message"> - <AlertTitle>{error.name}</AlertTitle> - {error.message} - </Alert> - {process.env.NODE_ENV === 'development' && ( - <Alert severity="info" sx={{ mt: 2 }} data-testid="stack-trace"> - <AlertTitle>Stack trace</AlertTitle> - {error.stack} - </Alert> - )} - </Container> - </NymThemeProvider> -); diff --git a/explorer/src/hooks/index.ts b/explorer/src/hooks/index.ts deleted file mode 100644 index ba04855f77..0000000000 --- a/explorer/src/hooks/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './useIsMobile'; -export * from './useIsMounted'; -export * from './useGetMixnodeStatusColor'; -export * from './useNymClient'; diff --git a/explorer/src/hooks/useGetMixnodeStatusColor.ts b/explorer/src/hooks/useGetMixnodeStatusColor.ts deleted file mode 100644 index d9d71fe7e1..0000000000 --- a/explorer/src/hooks/useGetMixnodeStatusColor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useTheme } from '@mui/material'; -import { MixnodeStatus } from '@src/typeDefs/explorer-api'; - -export const useGetMixNodeStatusColor = (status: MixnodeStatus) => { - const theme = useTheme(); - - switch (status) { - case MixnodeStatus.active: - return theme.palette.nym.networkExplorer.mixnodes.status.active; - - case MixnodeStatus.standby: - return theme.palette.nym.networkExplorer.mixnodes.status.standby; - - default: - return theme.palette.nym.networkExplorer.mixnodes.status.inactive; - } -}; diff --git a/explorer/src/hooks/useIsMobile.ts b/explorer/src/hooks/useIsMobile.ts deleted file mode 100644 index 225964b464..0000000000 --- a/explorer/src/hooks/useIsMobile.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Breakpoint, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); - - return isMobile; -}; diff --git a/explorer/src/hooks/useIsMounted.ts b/explorer/src/hooks/useIsMounted.ts deleted file mode 100644 index b18f3a72b7..0000000000 --- a/explorer/src/hooks/useIsMounted.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useRef, useEffect, useCallback } from 'react'; - -export function useIsMounted(): () => boolean { - const ref = useRef(false); - - useEffect(() => { - ref.current = true; - return () => { - ref.current = false; - }; - }, []); - - return useCallback(() => ref.current, [ref]); -} diff --git a/explorer/src/hooks/useNymClient.tsx b/explorer/src/hooks/useNymClient.tsx deleted file mode 100644 index 26b2f529db..0000000000 --- a/explorer/src/hooks/useNymClient.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useChain } from '@cosmos-kit/react'; -import { contracts } from '@nymproject/contract-clients'; -import { MixnetClient, MixnetQueryClient } from '@nymproject/contract-clients/Mixnet.client'; -import { COSMOS_KIT_USE_CHAIN, NYM_MIXNET_CONTRACT } from '@src/api/constants'; - -export const useNymClient = (address?: string) => { - const [nymClient, setNymClient] = useState<MixnetClient>(); - const [nymQueryClient, setNymQueryClient] = useState<MixnetQueryClient>(); - - const { getCosmWasmClient, getSigningCosmWasmClient } = useChain(COSMOS_KIT_USE_CHAIN); - - useEffect(() => { - if (address) { - const init = async () => { - const cosmWasmSigningClient = await getSigningCosmWasmClient(); - const cosmWasmClient = await getCosmWasmClient(); - - const client = new contracts.Mixnet.MixnetClient(cosmWasmSigningClient as any, address, NYM_MIXNET_CONTRACT); - const queryClient = new contracts.Mixnet.MixnetQueryClient(cosmWasmClient as any, NYM_MIXNET_CONTRACT); - - setNymClient(client); - setNymQueryClient(queryClient); - }; - - init(); - } - }, [address, getCosmWasmClient, getSigningCosmWasmClient]); - - return { nymClient, nymQueryClient }; -}; diff --git a/explorer/src/icons/DelevateSVG.tsx b/explorer/src/icons/DelevateSVG.tsx deleted file mode 100644 index 56180d1b8a..0000000000 --- a/explorer/src/icons/DelevateSVG.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const DelegateIcon = (props: SvgIconProps) => ( - <SvgIcon {...props}> - <path d="M4 12V15H6V12H4ZM16 7L14.59 5.59L13 7.17V2H11V7.19L9.39 5.61L8 7L12 11L16 7ZM4 17H20V15H4V17Z" /> - <path d="M20 21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V20H20V21Z" /> - <rect x="18" y="12" width="2" height="3" /> - <rect x="18" y="17" width="2" height="3" /> - <rect x="4" y="17" width="2" height="3" /> - </SvgIcon> -); diff --git a/explorer/src/icons/ElipsSVG.tsx b/explorer/src/icons/ElipsSVG.tsx deleted file mode 100644 index 4a430d6cb6..0000000000 --- a/explorer/src/icons/ElipsSVG.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -export const ElipsSVG: FCWithChildren = () => ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <circle cx="12" cy="12.5" r="10" fill="url(#paint0_angular_2549_7570)" /> - <defs> - <radialGradient - id="paint0_angular_2549_7570" - cx="0" - cy="0" - r="1" - gradientUnits="userSpaceOnUse" - gradientTransform="translate(12 12.5) rotate(90) scale(12)" - > - <stop stopColor="#22D27E" /> - <stop offset="1" stopColor="#9002FF" /> - </radialGradient> - </defs> - </svg> -); diff --git a/explorer/src/icons/GatewaysSVG.tsx b/explorer/src/icons/GatewaysSVG.tsx deleted file mode 100644 index 00e4a21198..0000000000 --- a/explorer/src/icons/GatewaysSVG.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const GatewaysSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M16.2 12H22.7" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M1.30005 12H12" stroke={color} strokeWidth="1.3" strokeMiterlimit="10" strokeLinecap="round" /> - <path - d="M20.1 9.40015L22.7 12.0001L20.1 14.6001" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M13.2 22.7001H8.59998C6.89998 22.7001 5.59998 21.4001 5.59998 19.7001V4.30005C5.59998 2.60005 6.89998 1.30005 8.59998 1.30005H13.2C14.9 1.30005 16.2 2.60005 16.2 4.30005V19.6C16.2 21.3001 14.8 22.7001 13.2 22.7001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - </svg> - ); -}; diff --git a/explorer/src/icons/LightSwitchSVG.tsx b/explorer/src/icons/LightSwitchSVG.tsx deleted file mode 100644 index 7a32590dfc..0000000000 --- a/explorer/src/icons/LightSwitchSVG.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const LightSwitchSVG: FCWithChildren = () => { - const { palette } = useTheme(); - return ( - <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12C22 6.5 17.5 2 12 2Z" - fill={palette.background.default} - /> - <path d="M12 20C7.6 20 4 16.4 4 12C4 7.6 7.6 4 12 4V20Z" fill={palette.text.primary} /> - </svg> - ); -}; diff --git a/explorer/src/icons/MixnodesSVG.tsx b/explorer/src/icons/MixnodesSVG.tsx deleted file mode 100644 index 56902cb0de..0000000000 --- a/explorer/src/icons/MixnodesSVG.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const MixnodesSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M23.0437 13.0291H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 2.99512H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0625H2.97681" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M23.0437 23.0621L2.97681 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M13.0103 23.0621L23.0437 2.99512" stroke={color} strokeMiterlimit="10" /> - <path d="M2.97681 2.99512L13.0103 23.0621" stroke={color} strokeMiterlimit="10" /> - <path - d="M13.0099 13.0289L23.0437 23.0621L13.0099 2.99512L2.97681 23.0621L13.0099 2.99512" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.097 12.9846L13.0892 2.97681L3.08142 12.9846L13.0892 22.9924L23.097 12.9846Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M23.0232 4.9536C24.1149 4.9536 25 4.06856 25 2.9768C25 1.88504 24.1149 1 23.0232 1C21.9314 1 21.0464 1.88504 21.0464 2.9768C21.0464 4.06856 21.9314 4.9536 23.0232 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 4.9536C14.0648 4.9536 14.9499 4.06856 14.9499 2.9768C14.9499 1.88504 14.0648 1 12.9731 1C11.8813 1 10.9963 1.88504 10.9963 2.9768C10.9963 4.06856 11.8813 4.9536 12.9731 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 4.9536C4.06856 4.9536 4.9536 4.06856 4.9536 2.9768C4.9536 1.88504 4.06856 1 2.9768 1C1.88504 1 1 1.88504 1 2.9768C1 4.06856 1.88504 4.9536 2.9768 4.9536Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 15.0029C24.1149 15.0029 25 14.1179 25 13.0261C25 11.9344 24.1149 11.0493 23.0232 11.0493C21.9314 11.0493 21.0464 11.9344 21.0464 13.0261C21.0464 14.1179 21.9314 15.0029 23.0232 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 15.0029C14.0648 15.0029 14.9499 14.1179 14.9499 13.0261C14.9499 11.9344 14.0648 11.0493 12.9731 11.0493C11.8813 11.0493 10.9963 11.9344 10.9963 13.0261C10.9963 14.1179 11.8813 15.0029 12.9731 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 15.0029C4.06856 15.0029 4.9536 14.1179 4.9536 13.0261C4.9536 11.9344 4.06856 11.0493 2.9768 11.0493C1.88504 11.0493 1 11.9344 1 13.0261C1 14.1179 1.88504 15.0029 2.9768 15.0029Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M23.0232 25C24.1149 25 25 24.1149 25 23.0232C25 21.9314 24.1149 21.0464 23.0232 21.0464C21.9314 21.0464 21.0464 21.9314 21.0464 23.0232C21.0464 24.1149 21.9314 25 23.0232 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M12.9731 25C14.0648 25 14.9499 24.1149 14.9499 23.0232C14.9499 21.9314 14.0648 21.0464 12.9731 21.0464C11.8813 21.0464 10.9963 21.9314 10.9963 23.0232C10.9963 24.1149 11.8813 25 12.9731 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - <path - d="M2.9768 25C4.06856 25 4.9536 24.1149 4.9536 23.0232C4.9536 21.9314 4.06856 21.0464 2.9768 21.0464C1.88504 21.0464 1 21.9314 1 23.0232C1 24.1149 1.88504 25 2.9768 25Z" - fill="#242C3D" - stroke={color} - strokeWidth="1.2" - strokeMiterlimit="10" - /> - </svg> - ); -}; diff --git a/explorer/src/icons/MobileDrawerClose.tsx b/explorer/src/icons/MobileDrawerClose.tsx deleted file mode 100644 index 6c8ecfacbc..0000000000 --- a/explorer/src/icons/MobileDrawerClose.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; - -export const MobileDrawerClose: FCWithChildren = (props) => ( - <svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -5 24 24" width="25" height="25" {...props}> - <path - d="M0 12H13V10H0V12ZM0 7H10V5H0V7ZM0 0V2H13V0H0ZM18 9.59L14.42 6L18 2.41L16.59 1L11.59 6L16.59 11L18 9.59Z" - fill="#F2F2F2" - /> - </svg> -); diff --git a/explorer/src/icons/NetworksSVG.tsx b/explorer/src/icons/NetworksSVG.tsx deleted file mode 100644 index 0db2b9bdfb..0000000000 --- a/explorer/src/icons/NetworksSVG.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NetworkComponentsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M17.2 10.5V4.40002L12 1.40002L6.8 4.40002V10.5L12 13.5L17.2 10.5Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M12 19.6V13.5L6.8 10.5L1.5 13.5V19.6L6.8 22.6L12 19.6Z" stroke={color} strokeMiterlimit="10" /> - <path d="M22.5 19.6V13.5L17.2 10.5L12 13.5V19.6L17.2 22.6L22.5 19.6Z" stroke={color} strokeMiterlimit="10" /> - </svg> - ); -}; diff --git a/explorer/src/icons/NodemapSVG.tsx b/explorer/src/icons/NodemapSVG.tsx deleted file mode 100644 index 9486d64dcc..0000000000 --- a/explorer/src/icons/NodemapSVG.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const NodemapSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - return ( - <svg width="25" height="25" viewBox="0 0 19 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M1 9.6999C1 5.0999 4.7 1.3999 9.3 1.3999C13.9 1.3999 17.6 5.0999 17.6 9.6999C17.6 14.2999 9.3 21.5999 9.3 21.5999C9.3 21.5999 1 14.2999 1 9.6999Z" - stroke={color} - strokeMiterlimit="10" - /> - <path - d="M9.30005 12C11.233 12 12.8 10.433 12.8 8.5C12.8 6.567 11.233 5 9.30005 5C7.36705 5 5.80005 6.567 5.80005 8.5C5.80005 10.433 7.36705 12 9.30005 12Z" - stroke={color} - strokeMiterlimit="10" - /> - <path d="M1.5 22.5999H17.1" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer/src/icons/NymVpn.tsx b/explorer/src/icons/NymVpn.tsx deleted file mode 100644 index 788b78a4f9..0000000000 --- a/explorer/src/icons/NymVpn.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react'; - -interface DiscordIconProps { - size?: { width: number; height: number }; -} - -export const NymVpnIcon: FCWithChildren<DiscordIconProps> = ({ size }) => ( - <svg width={size?.width} height={size?.height} viewBox="0 0 170 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path - d="M19.6118 0.128906H19.5405V0.187854V20.7961L10.7849 0.164277L10.773 0.128906H10.7255H5.75959H0.187819H0.128418V0.187854V23.8142V23.8732H0.187819H5.75959H5.81899V23.8142V3.17063L14.6103 23.8378L14.6222 23.8732H14.6697H19.6118H25.1717H25.2311V23.8142V0.187854V0.128906H25.1717H19.6118Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M19.4121 0H25.3603V24H14.5297L14.4901 23.8819L5.94824 3.80121V24H0V0H10.8663L10.906 0.118132L19.4121 20.1621V0ZM19.5409 20.7951L10.7853 0.163225L10.7734 0.127854H0.128835V23.8721H5.81941V3.16958L14.6107 23.8368L14.6226 23.8721H25.2315V0.127854H19.5409V20.7951Z" - fill="white" - /> - <path - d="M89.8116 0.128906H79.1908H79.1314L79.1195 0.176068L73.6784 20.8904L68.2255 0.176068L68.2136 0.128906H68.1661H57.5215H57.4502V0.187854V23.8142V23.8732H57.5215H63.0814H63.1408V23.8142V3.33568L68.5225 23.826L68.5343 23.8732H68.5937H78.7394H78.7869L78.7988 23.826L84.1804 3.33568V23.8142V23.8732H84.2398H89.8116H89.871V23.8142V0.187854V0.128906H89.8116Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M79.0312 0H90.0003V24H84.052V4.33208L78.9242 23.856L78.9238 23.8572L78.8879 24H68.4342L68.3982 23.8572L68.3979 23.856L63.27 4.33208V24H57.3218V0H68.3146L68.3505 0.142699L68.3509 0.144015L73.6787 20.383L78.9949 0.144015L78.9953 0.142765L79.0312 0ZM73.6788 20.8894L68.2259 0.175015L68.214 0.127854H57.4506V23.8721H63.1412V3.33463L68.5229 23.825L68.5348 23.8721H78.7873L78.7992 23.825L84.1809 3.33463V23.8721H89.8714V0.127854H79.1318L79.1199 0.175015L73.6788 20.8894Z" - fill="white" - /> - <path - d="M48.2909 0.128906H48.2553L48.2434 0.152487L41.4836 11.8124L34.6882 0.152487L34.6763 0.128906H34.6407H28.2135H28.0947L28.1541 0.223225L38.6205 18.2142V23.8142V23.8732H38.6799H44.2517H44.3111V23.8142V18.2142L54.7775 0.223225L54.8369 0.128906H54.7181H48.2909Z" - fill="white" - /> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M48.1757 0H55.0693L54.8879 0.288036L44.4399 18.2474V24H38.4917V18.2474L28.0437 0.288036L27.8623 0H34.756L34.8017 0.0907854L41.4833 11.5555L48.1299 0.0909153L48.1757 0ZM48.2434 0.151434L41.4836 11.8114L34.6882 0.151434L34.6763 0.127854H28.0948L28.1542 0.222173L38.6205 18.2131V23.8721H44.3111V18.2131L54.7775 0.222173L54.8369 0.127854H48.2553L48.2434 0.151434Z" - fill="white" - /> - <path - d="M169.238 0V24H166.422C166.006 24 165.654 23.9341 165.366 23.8023C165.088 23.6596 164.811 23.418 164.534 23.0776L153.542 8.76321C153.584 9.19149 153.611 9.60878 153.622 10.0151C153.643 10.4104 153.654 10.7838 153.654 11.1352V24H148.886V0H151.734C151.968 0 152.166 0.0109813 152.326 0.032944C152.486 0.0549066 152.63 0.0988326 152.758 0.164722C152.886 0.219629 153.008 0.30199 153.126 0.411805C153.243 0.521619 153.376 0.669869 153.526 0.856553L164.614 15.2697C164.56 14.8085 164.523 14.3638 164.502 13.9355C164.48 13.4962 164.47 13.0844 164.47 12.7001V0H169.238Z" - fill="#A8A6A6" - /> - <path - d="M134.206 11.7776C135.614 11.7776 136.627 11.4317 137.246 10.7399C137.865 10.048 138.174 9.08167 138.174 7.84077C138.174 7.29169 138.094 6.79204 137.934 6.3418C137.774 5.89156 137.529 5.50721 137.198 5.18874C136.878 4.8593 136.467 4.60673 135.966 4.43102C135.475 4.25532 134.889 4.16747 134.206 4.16747H131.39V11.7776H134.206ZM134.206 0C135.849 0 137.257 0.203157 138.43 0.609471C139.614 1.0048 140.585 1.55388 141.342 2.25669C142.11 2.95951 142.675 3.78861 143.038 4.74399C143.401 5.69938 143.582 6.73164 143.582 7.84077C143.582 9.03775 143.395 10.1359 143.022 11.1352C142.649 12.1345 142.078 12.9911 141.31 13.7049C140.542 14.4187 139.566 14.9787 138.382 15.385C137.209 15.7804 135.817 15.978 134.206 15.978H131.39V24H125.982V0H134.206Z" - fill="#A8A6A6" - /> - <path - d="M121.584 0L112.24 24H107.344L98 0H102.352C102.821 0 103.2 0.115305 103.488 0.345915C103.776 0.565545 103.995 0.851064 104.144 1.20247L108.656 14.0508C108.869 14.6108 109.077 15.2258 109.28 15.8957C109.483 16.5546 109.675 17.2464 109.856 17.9712C110.005 17.2464 110.171 16.5546 110.352 15.8957C110.544 15.2258 110.747 14.6108 110.96 14.0508L115.44 1.20247C115.557 0.894989 115.765 0.620452 116.064 0.378861C116.373 0.126287 116.752 0 117.2 0H121.584Z" - fill="#A8A6A6" - /> - </svg> -); - -NymVpnIcon.defaultProps = { - size: { width: 80, height: 12 }, -}; diff --git a/explorer/src/icons/OverviewSVG.tsx b/explorer/src/icons/OverviewSVG.tsx deleted file mode 100644 index 2ced0bb17b..0000000000 --- a/explorer/src/icons/OverviewSVG.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const OverviewSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.nym.networkExplorer.nav.text; - - return ( - <svg width="25" height="25" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M1.4 21.6H22.6" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M14.1 2.40002H9.9V21.5H14.1V2.40002Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M20.8 6.59998H16.6V21.5H20.8V6.59998Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - <path d="M7.4 11.8H3.2V21.6H7.4V11.8Z" stroke={color} strokeMiterlimit="10" strokeLinecap="round" /> - </svg> - ); -}; diff --git a/explorer/src/icons/TokenSVG.tsx b/explorer/src/icons/TokenSVG.tsx deleted file mode 100644 index 94ab1468c9..0000000000 --- a/explorer/src/icons/TokenSVG.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export const TokenSVG: FCWithChildren = () => { - const color = 'white'; - - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none"> - <g clipPath="url(#clip0_2549_7563)"> - <path - d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z" - fill={color} - /> - <path - d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_2549_7563"> - <rect width="24" height="24" fill="white" transform="translate(0 0.5)" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer/src/icons/ValidatorsSVG.tsx b/explorer/src/icons/ValidatorsSVG.tsx deleted file mode 100644 index cf03f1c330..0000000000 --- a/explorer/src/icons/ValidatorsSVG.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -export const ValidatorsSVG: FCWithChildren = () => { - const theme = useTheme(); - const color = theme.palette.text.primary; - return ( - <svg width="24" height="24" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clipPath="url(#clip0)"> - <path - d="M18.2001 18.4V19.7001C18.2001 21.4001 16.9 22.7001 15.2 22.7001H4.30005C2.60005 22.7001 1.30005 21.4001 1.30005 19.7001V4.30005C1.30005 2.60005 2.60005 1.30005 4.30005 1.30005H15.1C16.8 1.30005 18.1 2.60005 18.1 4.30005V5.60005V18.4H18.2001Z" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M13.4 22.7001H17.4C19.1 22.7001 20.4 21.4001 20.4 19.7001V18.4V5.60005V4.30005C20.4 2.60005 19.1 1.30005 17.4 1.30005H11.5" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M15.2 22.7001H19.7C21.4 22.7001 22.7 21.4001 22.7 19.7001V18.4V5.60005V4.30005C22.7 2.60005 21.4 1.30005 19.7 1.30005H13.8" - stroke={color} - strokeWidth="1.3" - strokeMiterlimit="10" - strokeLinecap="round" - /> - <path - d="M5 12.3L7.9 15.3L14.5 8.69995" - stroke={color} - strokeWidth="2" - strokeMiterlimit="10" - strokeLinecap="round" - strokeLinejoin="round" - /> - </g> - <defs> - <clipPath id="clip0"> - <rect width="24" height="24" fill="white" /> - </clipPath> - </defs> - </svg> - ); -}; diff --git a/explorer/src/icons/socials/DiscordIcon.tsx b/explorer/src/icons/socials/DiscordIcon.tsx deleted file mode 100644 index 11a9fd1c67..0000000000 --- a/explorer/src/icons/socials/DiscordIcon.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface DiscordIconProps { - size?: number | string; - color?: string; -} - -export const DiscordIcon: FCWithChildren<DiscordIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2296)"> - <path - d="M12.4 0C5.80002 0 0.400024 5.4 0.400024 12C0.400024 18.6 5.80002 24 12.4 24C19 24 24.4 18.6 24.4 12C24.4 5.4 19 0 12.4 0ZM20.1 15.9C18.8 16.9 17.5 17.5 16.2 17.9C16.2 17.9 16.2 17.9 16.1 17.9C15.8 17.5 15.5 17.1 15.3 16.6V16.5C15.7 16.3 16.1 16.1 16.5 15.9V15.8C16.4 15.7 16.3 15.7 16.3 15.6C16.3 15.6 16.3 15.6 16.2 15.6C13.7 16.8 10.9 16.8 8.40002 15.6C8.40002 15.6 8.40002 15.6 8.30002 15.6C8.20002 15.7 8.10002 15.7 8.10002 15.8V15.9C8.50002 16.1 8.90002 16.3 9.30002 16.5C9.30002 16.5 9.30002 16.5 9.30002 16.6C9.10002 17.1 8.80002 17.5 8.50002 17.9C8.50002 17.9 8.50002 17.9 8.40002 17.9C7.10002 17.5 5.90002 16.9 4.50002 15.9C4.40002 13 5.00002 10.1 7.00002 7.1C8.00002 6.6 9.00002 6.3 10.2 6.1C10.2 6.1 10.2 6.1 10.3 6.1C10.4 6.3 10.6 6.7 10.7 6.9C11.9 6.7 13.1 6.7 14.2 6.9C14.3 6.7 14.5 6.3 14.6 6.1C14.6 6.1 14.6 6.1 14.7 6.1C15.8 6.3 16.9 6.6 17.9 7.1C19.5 9.7 20.4 12.6 20.1 15.9Z" - fill={color} - /> - <path - d="M15 11C14.2 11 13.6 11.7 13.6 12.6C13.6 13.5 14.2 14.2 15 14.2C15.8 14.2 16.4 13.5 16.4 12.6C16.4 11.7 15.8 11 15 11Z" - fill={color} - /> - <path - d="M9.80002 11C9.10002 11 8.40002 11.7 8.40002 12.6C8.40002 13.5 9.00002 14.2 9.80002 14.2C10.6 14.2 11.2 13.5 11.2 12.6C11.2 11.7 10.6 11 9.80002 11Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2296"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -DiscordIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/GitHubIcon.tsx b/explorer/src/icons/socials/GitHubIcon.tsx deleted file mode 100644 index a8fde9b0a9..0000000000 --- a/explorer/src/icons/socials/GitHubIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface GitHubIconProps { - size?: number | string; - color?: string; -} - -export const GitHubIcon: FCWithChildren<GitHubIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2302)"> - <path - fillRule="evenodd" - clipRule="evenodd" - d="M12.7 0C5.90002 0 0.400024 5.5 0.400024 12.3C0.400024 17.7 3.90002 22.3 8.80002 24C9.40002 24.1 9.60002 23.7 9.60002 23.4C9.60002 23.1 9.60002 22.1 9.60002 21.1C6.50002 21.7 5.70002 20.3 5.50002 19.7C5.40002 19.3 4.80002 18.3 4.20002 18C3.80002 17.8 3.20002 17.2 4.20002 17.2C5.20002 17.2 5.90002 18.1 6.10002 18.5C7.20002 20.4 9.00002 19.8 9.70002 19.5C9.80002 18.7 10.1 18.2 10.5 17.9C7.80002 17.6 4.90002 16.5 4.90002 11.8C4.90002 10.5 5.40002 9.4 6.20002 8.5C6.00002 8 5.60002 6.8 6.30002 5.1C6.30002 5.1 7.30002 4.8 9.70002 6.4C10.7 6.1 11.7 6 12.8 6C13.8 6 14.9 6.1 15.9 6.4C18.3 4.8 19.3 5.1 19.3 5.1C20 6.8 19.5 8.1 19.4 8.4C20.2 9.3 20.7 10.4 20.7 11.7C20.7 16.4 17.8 17.5 15.1 17.8C15.5 18.2 15.9 18.9 15.9 20.1C15.9 21.7 15.9 23.1 15.9 23.5C15.9 23.8 16.1 24.2 16.7 24.1C21.6 22.5 25.1 17.9 25.1 12.4C25 5.5 19.5 0 12.7 0Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2302"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -GitHubIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/TelegramIcon.tsx b/explorer/src/icons/socials/TelegramIcon.tsx deleted file mode 100644 index cf150a4a2f..0000000000 --- a/explorer/src/icons/socials/TelegramIcon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface TelegramIconProps { - size?: number | string; - color?: string; -} - -export const TelegramIcon: FCWithChildren<TelegramIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <path - d="M12.4 24C19.029 24 24.4 18.629 24.4 12C24.4 5.371 19.029 0 12.4 0C5.77102 0 0.400024 5.371 0.400024 12C0.400024 18.629 5.77102 24 12.4 24ZM5.89102 11.74L17.461 7.279C17.998 7.085 18.467 7.41 18.293 8.222L18.294 8.221L16.324 17.502C16.178 18.16 15.787 18.32 15.24 18.01L12.24 15.799L10.793 17.193C10.633 17.353 10.498 17.488 10.188 17.488L10.401 14.435L15.961 9.412C16.203 9.199 15.907 9.079 15.588 9.291L8.71702 13.617L5.75502 12.693C5.11202 12.489 5.09802 12.05 5.89102 11.74Z" - fill={color} - /> - </svg> - ); -}; - -TelegramIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/icons/socials/TwitterIcon.tsx b/explorer/src/icons/socials/TwitterIcon.tsx deleted file mode 100644 index 6f0b545f56..0000000000 --- a/explorer/src/icons/socials/TwitterIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { useTheme } from '@mui/material/styles'; - -interface TwitterIconProps { - size?: number | string; - color?: string; -} - -export const TwitterIcon: FCWithChildren<TwitterIconProps> = ({ size, color: colorProp }) => { - const theme = useTheme(); - const color = colorProp || theme.palette.text.primary; - return ( - <svg width={size} height={size} viewBox="0 0 24 24" fill="none"> - <g clipPath="url(#clip0_1223_2294)"> - <path - d="M12.4 0C5.77362 0 0.400024 5.3736 0.400024 12C0.400024 18.6264 5.77362 24 12.4 24C19.0264 24 24.4 18.6264 24.4 12C24.4 5.3736 19.0264 0 12.4 0ZM17.8791 9.35632C17.8844 9.47443 17.887 9.59308 17.887 9.71228C17.887 13.3519 15.1166 17.5488 10.0502 17.549H10.0504H10.0502C8.49475 17.549 7.0473 17.0931 5.82837 16.3118C6.04388 16.3372 6.26324 16.3499 6.48535 16.3499C7.77588 16.3499 8.9635 15.9097 9.90631 15.1708C8.70056 15.1485 7.68396 14.3522 7.33313 13.2578C7.50104 13.29 7.67371 13.3076 7.85077 13.3076C8.10217 13.3076 8.3457 13.2737 8.57715 13.2105C7.31683 12.9582 6.36743 11.8444 6.36743 10.5106C6.36743 10.4982 6.36743 10.487 6.3678 10.4755C6.73895 10.6818 7.16339 10.806 7.6153 10.8199C6.87573 10.3264 6.38959 9.48285 6.38959 8.52722C6.38959 8.02258 6.526 7.5498 6.76257 7.14276C8.12085 8.80939 10.1508 9.90546 12.4399 10.0206C12.3927 9.81885 12.3683 9.60864 12.3683 9.39258C12.3683 7.87207 13.6019 6.63849 15.123 6.63849C15.9153 6.63849 16.6309 6.97339 17.1335 7.50879C17.761 7.38501 18.3502 7.15576 18.8825 6.84027C18.6765 7.48315 18.24 8.02258 17.6713 8.36371C18.2285 8.29706 18.7595 8.14929 19.2529 7.92993C18.8843 8.48236 18.4169 8.96759 17.8791 9.35632V9.35632Z" - fill={color} - /> - </g> - <defs> - <clipPath id="clip0_1223_2294"> - <rect width="24" height="24" transform="translate(0.400024)" /> - </clipPath> - </defs> - </svg> - ); -}; - -TwitterIcon.defaultProps = { - size: 24, - color: undefined, -}; diff --git a/explorer/src/index.html b/explorer/src/index.html deleted file mode 100644 index dd5a4761ee..0000000000 --- a/explorer/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="utf-8" /> - <title>Nym Network Explorer - - - - -
- - - diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx deleted file mode 100644 index 0b76be7ab1..0000000000 --- a/explorer/src/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { ErrorBoundary } from 'react-error-boundary'; -import { MainContextProvider } from './context/main'; -import { NetworkExplorerThemeProvider } from './theme'; -import { ErrorBoundaryContent } from './errors/ErrorBoundaryContent'; -import CosmosKitProvider from './context/cosmos-kit'; -import '@interchain-ui/react/styles'; -import { App } from './App'; -import { WalletProvider } from './context/wallet'; -import './styles.css'; - -const elem = document.getElementById('app'); - -if (elem) { - const root = createRoot(elem); - root.render( - - - - - - - - - - - - - , - ); -} diff --git a/explorer/src/pages/404/index.tsx b/explorer/src/pages/404/index.tsx deleted file mode 100644 index 02f4247fc5..0000000000 --- a/explorer/src/pages/404/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react'; -import { Box, Button, Grid, Paper, Typography } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { useNavigate } from 'react-router-dom'; -import { NymLogo } from '@nymproject/react/logo/NymLogo'; -import { useMainContext } from '../../context/main'; - -export const Page404 = () => { - const navigate = useNavigate(); - const { mode } = useMainContext(); - const theme = useTheme(); - return ( - - - - - - Oh No! - It looks like you might be lost. - - Please try the link again or navigate back to{' '} - - - - - - - ); -}; diff --git a/explorer/src/pages/Delegations/index.tsx b/explorer/src/pages/Delegations/index.tsx deleted file mode 100644 index 32e178f2fc..0000000000 --- a/explorer/src/pages/Delegations/index.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useEffect } from 'react'; -import { Alert, AlertTitle, Box, Button, Card, Chip, IconButton, Tooltip, Typography } from '@mui/material'; -import { Link, useNavigate } from 'react-router-dom'; -import { DelegationModal, DelegationModalProps, Title, UniversalDataGrid } from '@src/components'; -import { useWalletContext } from '@src/context/wallet'; -import { GridColDef } from '@mui/x-data-grid'; -import { unymToNym } from '@src/utils/currency'; -import { - DelegationWithRewards, - DelegationsProvider, - PendingEvent, - useDelegationsContext, -} from '@src/context/delegations'; -import { urls } from '@src/utils'; -import { useClipboard } from 'use-clipboard-copy'; -import { Close } from '@mui/icons-material'; - -const mapToDelegationsRow = (delegation: DelegationWithRewards, index: number) => ({ - identity: delegation.identityKey, - mix_id: delegation.mix_id, - amount: `${unymToNym(delegation.amount.amount)} NYM`, - rewards: `${unymToNym(delegation.rewards)} NYM`, - id: index, - pending: delegation.pending, -}); - -const Banner = ({ onClose }: { onClose: () => void }) => { - const { copy } = useClipboard(); - - return ( - - - - } - > - Mobile Delegations Beta - - - This is a beta release for mobile delegations If you have any feedback or feature suggestions contact us at - support@nymte.ch - - - - - ); -}; - -const DelegationsPage = () => { - const [confirmationModalProps, setConfirmationModalProps] = React.useState(); - const [isLoading, setIsLoading] = React.useState(false); - const [showBanner, setShowBanner] = React.useState(true); - - const { isWalletConnected } = useWalletContext(); - const { handleGetDelegations, handleUndelegate, delegations } = useDelegationsContext(); - const navigate = useNavigate(); - - useEffect(() => { - let timeoutId: NodeJS.Timeout; - - const fetchDelegations = async () => { - setIsLoading(true); - try { - await handleGetDelegations(); - } catch (error) { - setConfirmationModalProps({ - status: 'error', - message: "Couldn't fetch delegations. Please try again later.", - }); - } finally { - setIsLoading(false); - - timeoutId = setTimeout(() => { - fetchDelegations(); - }, 60_000); - } - }; - - fetchDelegations(); - - return () => { - clearTimeout(timeoutId); - }; - }, [handleGetDelegations]); - - const getTooltipTitle = (pending: PendingEvent) => { - if (pending?.kind === 'undelegate') { - return 'You have an undelegation pending'; - } - - if (pending?.kind === 'delegate') { - return `You have a delegation pending worth ${unymToNym(pending.amount.amount)} NYM`; - } - - return undefined; - }; - - const onUndelegate = async (mixId: number) => { - setConfirmationModalProps({ status: 'loading' }); - - try { - const tx = await handleUndelegate(mixId); - - if (tx) { - setConfirmationModalProps({ - status: 'success', - message: 'Undelegation can take up to one hour to process', - transactions: [ - { url: `${urls('MAINNET').blockExplorer}/transaction/${tx.transactionHash}`, hash: tx.transactionHash }, - ], - }); - } - } catch (error) { - if (error instanceof Error) { - setConfirmationModalProps({ status: 'error', message: error.message }); - } - } - }; - - const columns: GridColDef[] = [ - { - field: 'identity', - headerName: 'Identity Key', - width: 400, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'mix_id', - headerName: 'Mix ID', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'amount', - headerName: 'Amount', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'rewards', - headerName: 'Rewards', - width: 150, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'left', - }, - { - field: 'undelegate', - headerName: '', - minWidth: 150, - flex: 1, - disableColumnMenu: true, - disableReorder: true, - sortable: false, - headerAlign: 'right', - renderCell: (params) => { - const { pending } = params.row; - - return ( - - {pending ? ( - e.stopPropagation()} - PopperProps={{}} - > - - - ) : ( - - )} - - ); - }, - }, - ]; - - const handleRowClick = (params: any) => { - navigate(`/network-components/mixnode/${params.row.mix_id}`); - }; - - return ( - - {confirmationModalProps && ( - { - if (confirmationModalProps.status === 'success') { - await handleGetDelegations(); - } - setConfirmationModalProps(undefined); - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - {showBanner && setShowBanner(false)} />} - - - <Button variant="contained" color="primary" component={Link} to="/network-components/mixnodes"> - Delegate - </Button> - </Box> - {!isWalletConnected ? ( - <Box> - <Typography mb={2} variant="h6"> - Connect your wallet to view your delegations. - </Typography> - </Box> - ) : null} - - <Card - sx={{ - mt: 2, - padding: 2, - height: '100%', - }} - > - <UniversalDataGrid - onRowClick={handleRowClick} - rows={delegations?.map(mapToDelegationsRow) || []} - columns={columns} - loading={isLoading} - /> - </Card> - </Box> - ); -}; - -export const Delegations = () => ( - <DelegationsProvider> - <DelegationsPage /> - </DelegationsProvider> -); diff --git a/explorer/src/pages/GatewayDetail/index.tsx b/explorer/src/pages/GatewayDetail/index.tsx deleted file mode 100644 index 3382947512..0000000000 --- a/explorer/src/pages/GatewayDetail/index.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { GatewayBond } from '../../typeDefs/explorer-api'; -import { ColumnsType, DetailTable } from '../../components/DetailTable'; -import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways'; -import { ComponentError } from '../../components/ComponentError'; -import { ContentCard } from '../../components/ContentCard'; -import { TwoColSmallTable } from '../../components/TwoColSmallTable'; -import { UptimeChart } from '../../components/UptimeChart'; -import { GatewayContextProvider, useGatewayContext } from '../../context/gateway'; -import { useMainContext } from '../../context/main'; -import { Title } from '../../components/Title'; - -const columns: ColumnsType[] = [ - { - field: 'identity_key', - title: 'Identity Key', - headerAlign: 'left', - width: 230, - }, - { - field: 'bond', - title: 'Bond', - headerAlign: 'left', - }, - { - field: 'node_performance', - title: 'Routing Score', - headerAlign: 'left', - tooltipInfo: - "Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test", - }, - { - field: 'avgUptime', - title: 'Avg. Score', - headerAlign: 'left', - tooltipInfo: "Gateway's average routing score in the last 24 hours", - }, - { - field: 'host', - title: 'IP', - headerAlign: 'left', - width: 99, - }, - { - field: 'location', - title: 'Location', - headerAlign: 'left', - }, - { - field: 'owner', - title: 'Owner', - headerAlign: 'left', - }, - { - field: 'version', - title: 'Version', - headerAlign: 'left', - }, -]; - -/** - * Shows gateway details - */ -const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayBond | undefined }) => { - const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>(); - const [status, setStatus] = React.useState<number[] | undefined>(); - const { uptimeReport, uptimeStory } = useGatewayContext(); - - React.useEffect(() => { - if (uptimeReport?.data && selectedGateway) { - setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)); - } - }, [uptimeReport, selectedGateway]); - - React.useEffect(() => { - if (enrichGateway) { - setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]); - } - }, [enrichGateway]); - - return ( - <Box component="main"> - <Title text="Gateway Detail" /> - - <Grid container> - <Grid item xs={12}> - <DetailTable - columnsData={columns} - tableName="Gateway detail table" - rows={enrichGateway ? [enrichGateway] : []} - /> - </Grid> - </Grid> - - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Gateway Status"> - <TwoColSmallTable - loading={false} - keys={['Mix port', 'Client WS API Port']} - values={status.map((each) => each)} - icons={status.map((elem) => !!elem)} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageGatewayDetailGuard: FCWithChildren = () => { - const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>(); - const { gateways } = useMainContext(); - const { id } = useParams<{ id: string | undefined }>(); - - React.useEffect(() => { - if (gateways?.data) { - setSelectedGateway(gateways.data.find((g) => g.gateway.identity_key === id)); - } - }, [gateways, id]); - - if (gateways?.isLoading) { - return <CircularProgress />; - } - - if (gateways?.error) { - // eslint-disable-next-line no-console - console.error(gateways?.error); - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ); - } - - // loaded, but not found - if (gateways && !gateways.isLoading && !gateways.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Gateway not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ); - } - - return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />; -}; - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -export const PageGatewayDetail: FCWithChildren = () => { - const { id } = useParams<{ id: string | undefined }>(); - - if (!id) { - return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; - } - - return ( - <GatewayContextProvider gatewayIdentityKey={id}> - <PageGatewayDetailGuard /> - </GatewayContextProvider> - ); -}; diff --git a/explorer/src/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx deleted file mode 100644 index c04b7eaf40..0000000000 --- a/explorer/src/pages/Gateways/index.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import * as React from 'react'; -import { Box, Card, Grid, Stack } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { SelectChangeEvent } from '@mui/material/Select'; -import { diff, gte, rcompare } from 'semver'; -import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip'; -import { useMainContext } from '../../context/main'; -import { gatewayToGridRow } from '../../components/Gateways'; -import { GatewayResponse } from '../../typeDefs/explorer-api'; -import { TableToolbar } from '../../components/TableToolbar'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { unymToNym } from '../../utils/currency'; -import { Tooltip } from '../../components/Tooltip'; -import { NYM_BIG_DIPPER } from '../../api/constants'; -import { splice } from '../../utils'; -import { VersionDisplaySelector, VersionSelectOptions } from '../../components/Gateways/VersionDisplaySelector'; -import StyledLink from '../../components/StyledLink'; - -export const PageGateways: FCWithChildren = () => { - const { gateways } = useMainContext(); - const [filteredGateways, setFilteredGateways] = React.useState<GatewayResponse>([]); - const [pageSize, setPageSize] = React.useState<string>('50'); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - const [versionFilter, setVersionFilter] = React.useState<VersionSelectOptions>(VersionSelectOptions.all); - - const theme = useTheme(); - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - const highestVersion = React.useMemo(() => { - if (gateways?.data) { - const versions = gateways.data.reduce((a: string[], b) => [...a, b.gateway.version], []); - const [lastestVersion] = versions.sort(rcompare); - return lastestVersion; - } - // fallback value - return '2.0.0'; - }, [gateways]); - - const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version); - return versionDiff === 'patch' || versionDiff === null; - }); - if (filtered) return filtered; - return []; - }, [gateways]); - - const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { - const versionDiff = diff(highestVersion, gw.gateway.version); - return versionDiff === 'major' || versionDiff === 'minor'; - }); - if (filtered) return filtered; - return []; - }, [gateways]); - - const filteredByVersion = React.useMemo(() => { - switch (versionFilter) { - case VersionSelectOptions.latestVersion: - return filterByLatestVersions; - case VersionSelectOptions.olderVersions: - return filterByOlderVersions; - case VersionSelectOptions.all: - return gateways?.data || []; - default: - return []; - } - }, [versionFilter, gateways]); - - React.useEffect(() => { - if (searchTerm === '') { - setFilteredGateways(filteredByVersion); - } else { - const filtered = filteredByVersion.filter((g) => { - if ( - g.gateway.location.toLowerCase().includes(searchTerm) || - g.gateway.identity_key.toLocaleLowerCase().includes(searchTerm) || - g.owner.toLowerCase().includes(searchTerm) - ) { - return g; - } - return null; - }); - - if (filtered) { - setFilteredGateways(filtered); - } - } - }, [searchTerm, gateways?.data, versionFilter]); - - const columns: GridColDef[] = [ - { - field: 'identity_key', - renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, - headerClassName: 'MuiDataGrid-header-override', - width: 400, - disableColumnMenu: true, - headerAlign: 'center', - renderCell: (params: GridRenderCellParams) => ( - <Stack direction="row" gap={1}> - <CopyToClipboard smallIcons value={params.value} tooltip={`Copy identity key ${params.value} to clipboard`} /> - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`}>{params.value}</StyledLink> - </Stack> - ), - }, - { - field: 'node_performance', - align: 'center', - renderHeader: () => ( - <> - <InfoTooltip - id="gateways-list-routing-score" - title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test" - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - <CustomColumnHeading headingTitle="Routing Score" /> - </> - ), - width: 120, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="pledge-amount"> - {`${params.value}%`} - </StyledLink> - ), - }, - { - field: 'version', - align: 'center', - renderHeader: () => <CustomColumnHeading headingTitle="Version" />, - width: 150, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="version"> - {params.value} - </StyledLink> - ), - sortComparator: (a, b) => { - if (gte(a, b)) return 1; - return -1; - }, - }, - { - field: 'location', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Box - onClick={() => handleSearch(params.value as string)} - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip text={params.value} id="gateway-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {params.value} - </Box> - </Tooltip> - </Box> - ), - }, - { - field: 'host', - renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="host"> - {params.value} - </StyledLink> - ), - }, - { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`${NYM_BIG_DIPPER}/account/${params.value}`} target="_blank" data-testid="owner"> - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'bond', - width: 150, - disableColumnMenu: true, - type: 'number', - renderHeader: () => <CustomColumnHeading headingTitle="Bond" />, - headerClassName: 'MuiDataGrid-header-override', - headerAlign: 'left', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink to={`/network-components/gateway/${params.row.identity_key}`} data-testid="pledge-amount"> - {`${unymToNym(params.value, 6)}`} - </StyledLink> - ), - }, - ]; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - if (gateways?.data) { - return ( - <> - <Box mb={2}> - <Title text="Gateways" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - childrenBefore={ - <VersionDisplaySelector - handleChange={(option) => setVersionFilter(option)} - selected={versionFilter} - /> - } - /> - <UniversalDataGrid - pagination - rows={gatewayToGridRow(filteredGateways)} - columns={columns} - pageSize={pageSize} - /> - </Card> - </Grid> - </Grid> - </> - ); - } - return null; -}; diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx deleted file mode 100644 index 5ceaa36867..0000000000 --- a/explorer/src/pages/MixnodeDetail/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertTitle, Box, CircularProgress, Grid, Typography } from '@mui/material'; -import { useParams } from 'react-router-dom'; -import { ColumnsType, DetailTable } from '../../components/DetailTable'; -import { BondBreakdownTable } from '../../components/MixNodes/BondBreakdown'; -import { DelegatorsInfoTable, EconomicsInfoColumns, EconomicsInfoRows } from '../../components/MixNodes/Economics'; -import { ComponentError } from '../../components/ComponentError'; -import { ContentCard } from '../../components/ContentCard'; -import { TwoColSmallTable } from '../../components/TwoColSmallTable'; -import { UptimeChart } from '../../components/UptimeChart'; -import { WorldMap } from '../../components/WorldMap'; -import { MixNodeDetailSection } from '../../components/MixNodes/DetailSection'; -import { MixnodeContextProvider, useMixnodeContext } from '../../context/mixnode'; -import { Title } from '../../components/Title'; -import { useIsMobile } from '../../hooks/useIsMobile'; - -const columns: ColumnsType[] = [ - { - field: 'owner', - title: 'Owner', - width: '15%', - }, - { - field: 'identity_key', - title: 'Identity Key', - width: '15%', - }, - - { - field: 'bond', - title: 'Stake', - width: '12.5%', - }, - { - field: 'stake_saturation', - title: 'Stake Saturation', - width: '12.5%', - tooltipInfo: - 'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set.', - }, - { - field: 'self_percentage', - width: '10%', - title: 'Bond %', - tooltipInfo: "Percentage of the operator's bond to the total stake on the node", - }, - - { - field: 'host', - width: '10%', - title: 'Host', - }, - { - field: 'location', - title: 'Location', - }, - - { - field: 'layer', - title: 'Layer', - }, -]; - -/** - * Shows mix node details - */ -const PageMixnodeDetailWithState: FCWithChildren = () => { - const { mixNode, mixNodeRow, description, stats, status, uptimeStory, uniqDelegations } = useMixnodeContext(); - const isMobile = useIsMobile(); - return ( - <Box component="main"> - <Title text="Mixnode Detail" /> - <Grid container spacing={2} mt={1} mb={6}> - <Grid item xs={12}> - {mixNodeRow && description?.data && ( - <MixNodeDetailSection mixNodeRow={mixNodeRow} mixnodeDescription={description.data} /> - )} - {mixNodeRow?.blacklisted && ( - <Typography textAlign={isMobile ? 'left' : 'right'} fontSize="smaller" sx={{ color: 'error.main' }}> - This node is having a poor performance - </Typography> - )} - </Grid> - </Grid> - <Grid container> - <Grid item xs={12}> - <DetailTable columnsData={columns} tableName="Mixnode detail table" rows={mixNodeRow ? [mixNodeRow] : []} /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <DelegatorsInfoTable - columnsData={EconomicsInfoColumns} - tableName="Delegators info table" - rows={[EconomicsInfoRows()]} - /> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12}> - <ContentCard title={`Stake Breakdown (${uniqDelegations?.data?.length} delegators)`}> - <BondBreakdownTable /> - </ContentCard> - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - <ContentCard title="Mixnode Stats"> - {stats && ( - <> - {stats.error && <ComponentError text="There was a problem retrieving this nodes stats." />} - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since startup" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_startup || 0, - stats?.data?.packets_sent_since_startup || 0, - stats?.data?.packets_explicitly_dropped_since_startup || 0, - ]} - /> - <TwoColSmallTable - loading={stats.isLoading} - error={stats?.error?.message} - title="Since last update" - keys={['Received', 'Sent', 'Explicitly dropped']} - values={[ - stats?.data?.packets_received_since_last_update || 0, - stats?.data?.packets_sent_since_last_update || 0, - stats?.data?.packets_explicitly_dropped_since_last_update || 0, - ]} - marginBottom - /> - </> - )} - {!stats && <Typography>No stats information</Typography>} - </ContentCard> - </Grid> - <Grid item xs={12} md={8}> - {uptimeStory && ( - <ContentCard title="Routing Score"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} - <UptimeChart - loading={uptimeStory.isLoading} - xLabel="Date" - yLabel="Daily average" - uptimeStory={uptimeStory} - /> - </ContentCard> - )} - </Grid> - </Grid> - <Grid container spacing={2} mt={0}> - <Grid item xs={12} md={4}> - {status && ( - <ContentCard title="Mixnode Status"> - {status.error && <ComponentError text="There was a problem retrieving port information" />} - <TwoColSmallTable - loading={status.isLoading} - error={status?.error?.message} - keys={['Mix port', 'Verloc port', 'HTTP port']} - values={[1789, 1790, 8000].map((each) => each)} - icons={(status?.data?.ports && Object.values(status.data.ports)) || [false, false, false]} - /> - </ContentCard> - )} - </Grid> - <Grid item xs={12} md={8}> - {mixNode && ( - <ContentCard title="Location"> - {mixNode?.error && <ComponentError text="There was a problem retrieving this mixnode location" />} - {mixNode?.data?.location?.latitude && mixNode?.data?.location?.longitude && ( - <WorldMap - loading={mixNode.isLoading} - userLocation={[mixNode.data.location.longitude, mixNode.data.location.latitude]} - /> - )} - </ContentCard> - )} - </Grid> - </Grid> - </Box> - ); -}; - -/** - * Guard component to handle loading and not found states - */ -const PageMixnodeDetailGuard: FCWithChildren = () => { - const { mixNode } = useMixnodeContext(); - const { id } = useParams<{ id: string | undefined }>(); - - if (mixNode?.isLoading) { - return <CircularProgress />; - } - - if (mixNode?.error) { - // eslint-disable-next-line no-console - console.error(mixNode?.error); - return ( - <Alert severity="error"> - Oh no! Could not load mixnode <code>{id || ''}</code> - </Alert> - ); - } - - // loaded, but not found - if (mixNode && !mixNode.isLoading && !mixNode.data) { - return ( - <Alert severity="warning"> - <AlertTitle>Mixnode not found</AlertTitle> - Sorry, we could not find a mixnode with id <code>{id || ''}</code> - </Alert> - ); - } - - return <PageMixnodeDetailWithState />; -}; - -/** - * Wrapper component that adds the mixnode content based on the `id` in the address URL - */ -export const PageMixnodeDetail: FCWithChildren = () => { - const { id } = useParams<{ id: string | undefined }>(); - - if (!id) { - return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; - } - - return ( - <MixnodeContextProvider mixId={id}> - <PageMixnodeDetailGuard /> - </MixnodeContextProvider> - ); -}; diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx deleted file mode 100644 index 79a88f7053..0000000000 --- a/explorer/src/pages/Mixnodes/index.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import * as React from 'react'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { Stack, Card, Grid, Box, Button } from '@mui/material'; -import { useParams, useNavigate, Link } from 'react-router-dom'; -import { SelectChangeEvent } from '@mui/material/Select'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { useMainContext } from '@src/context/main'; -import { - DelegateIconButton, - DelegationModal, - DelegationModalProps, - DelegateModal, - CustomColumnHeading, - StyledLink, - Title, - UniversalDataGrid, - TableToolbar, - Tooltip, - MixNodeStatusDropdown, - mixnodeToGridRow, -} from '@src/components'; -import { MixNodeResponse, MixnodeStatusWithAll, toMixnodeStatus } from '@src/typeDefs/explorer-api'; -import { NYM_BIG_DIPPER } from '@src/api/constants'; -import { currencyToString } from '@src/utils/currency'; -import { splice } from '@src/utils'; -import { useGetMixNodeStatusColor, useIsMobile } from '@src/hooks'; -import { useWalletContext } from '@src/context/wallet'; -import { DelegationsProvider } from '@src/context/delegations'; - -export const PageMixnodes: FCWithChildren = () => { - const { mixnodes, fetchMixnodes } = useMainContext(); - const [filteredMixnodes, setFilteredMixnodes] = React.useState<MixNodeResponse>([]); - const [pageSize, setPageSize] = React.useState<string>('10'); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - const [itemSelectedForDelegation, setItemSelectedForDelegation] = React.useState<{ - mixId: number; - identityKey: string; - }>(); - const [confirmationModalProps, setConfirmationModalProps] = React.useState<DelegationModalProps | undefined>(); - const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); - - const navigate = useNavigate(); - const { isWalletConnected } = useWalletContext(); - const isMobile = useIsMobile(); - - const handleNewDelegation = (delegationModalProps: DelegationModalProps) => { - setItemSelectedForDelegation(undefined); - setConfirmationModalProps(delegationModalProps); - }; - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - React.useEffect(() => { - if (searchTerm === '' && mixnodes?.data) { - setFilteredMixnodes(mixnodes?.data); - } else { - const filtered = mixnodes?.data?.filter((m) => { - if ( - m.location?.country_name.toLowerCase().includes(searchTerm) || - m.mix_node.identity_key.toLocaleLowerCase().includes(searchTerm) || - m.owner.toLowerCase().includes(searchTerm) - ) { - return m; - } - return null; - }); - if (filtered) { - setFilteredMixnodes(filtered); - } - } - }, [searchTerm, mixnodes?.data, mixnodes?.isLoading]); - - React.useEffect(() => { - // when the status changes, get the mixnodes - fetchMixnodes(toMixnodeStatus(status)); - }, [status]); - - const handleMixnodeStatusChanged = (newStatus?: MixnodeStatusWithAll) => { - navigate( - newStatus && newStatus !== MixnodeStatusWithAll.all - ? `/network-components/mixnodes/${newStatus}` - : '/network-components/mixnodes', - ); - }; - - const handleOnDelegate = ({ identityKey, mixId }: { identityKey: string; mixId: number }) => { - if (!isWalletConnected) { - setConfirmationModalProps({ - status: 'info', - message: 'Please connect your wallet to delegate', - }); - } else { - setItemSelectedForDelegation({ identityKey, mixId }); - } - }; - - const columns: GridColDef[] = [ - { - field: 'delegate', - disableColumnMenu: true, - disableReorder: true, - sortable: false, - width: isMobile ? 25 : 100, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => null, - renderCell: (params: GridRenderCellParams) => ( - <DelegateIconButton - size="small" - onDelegate={() => handleOnDelegate({ identityKey: params.row.identity_key, mixId: params.row.mix_id })} - /> - ), - }, - { - field: 'identity_key', - width: 325, - headerName: 'Identity Key', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, - renderCell: (params: GridRenderCellParams) => ( - <Stack direction="row" alignItems="center" gap={1}> - <CopyToClipboard - sx={{ mr: 0.5, color: 'grey.400' }} - smallIcons - value={params.value} - tooltip={`Copy identity key ${params.value} to clipboard`} - /> - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - dataTestId="identity-link" - > - {splice(7, 29, params.value)} - </StyledLink> - </Stack> - ), - }, - { - field: 'mix_id', - width: 85, - align: 'center', - hide: true, - headerName: 'Mix ID', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.value}`} - color={useGetMixNodeStatusColor(params.row.status)} - data-testid="mix-id" - > - {params.value} - </StyledLink> - ), - }, - - { - field: 'bond', - width: 150, - align: 'left', - type: 'number', - disableColumnMenu: true, - headerName: 'Stake', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => <CustomColumnHeading headingTitle="Stake" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - > - {currencyToString({ amount: params.value })} - </StyledLink> - ), - }, - { - field: 'stake_saturation', - width: 185, - align: 'center', - headerName: 'Stake Saturation', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Stake Saturation" - tooltipInfo="Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is 940k NYMs, computed as S/K where S is target amount of tokens staked in the network and K is the number of nodes in the reward set." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value} %`}</StyledLink> - ), - }, - { - field: 'pledge_amount', - width: 150, - align: 'left', - type: 'number', - headerName: 'Bond', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - > - {currencyToString({ amount: params.value })} - </StyledLink> - ), - }, - { - field: 'profit_percentage', - width: 145, - align: 'center', - headerName: 'Profit Margin', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Profit Margin" - tooltipInfo="Percentage of the delegators rewards that the operator takes as fee before rewards are distributed to the delegators." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value}%`}</StyledLink> - ), - }, - { - field: 'operating_cost', - width: 170, - align: 'center', - headerName: 'Operating Cost', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Operating Cost" - tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value} NYM`}</StyledLink> - ), - }, - { - field: 'node_performance', - width: 165, - align: 'center', - headerName: 'Routing Score', - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Routing Score" - tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test." - /> - ), - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/mixnode/${params.row.mix_id}`} - color={useGetMixNodeStatusColor(params.row.status)} - >{`${params.value}%`}</StyledLink> - ), - }, - { - field: 'owner', - width: 120, - headerName: 'Owner', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`${NYM_BIG_DIPPER}/account/${params.value}`} - color={useGetMixNodeStatusColor(params.row.status)} - target="_blank" - data-testid="big-dipper-link" - > - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'location', - width: 150, - headerName: 'Location', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - renderCell: (params: GridRenderCellParams) => ( - <Tooltip text={params.value} id="mixnode-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - cursor: 'pointer', - color: useGetMixNodeStatusColor(params.row.status), - }} - onClick={() => handleSearch(params.value)} - > - {params.value} - </Box> - </Tooltip> - ), - }, - { - field: 'host', - width: 130, - headerName: 'Host', - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - disableColumnMenu: true, - renderHeader: () => <CustomColumnHeading headingTitle="Host" />, - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - color={useGetMixNodeStatusColor(params.row.status)} - to={`/network-components/mixnode/${params.row.mix_id}`} - > - {params.value} - </StyledLink> - ), - }, - ]; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - return ( - <DelegationsProvider> - <Box mb={2}> - <Title text="Mixnodes" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - height: '100%', - }} - > - <TableToolbar - childrenBefore={ - <MixNodeStatusDropdown sx={{ mr: 2 }} status={status} onSelectionChanged={handleMixnodeStatusChanged} /> - } - childrenAfter={ - isWalletConnected && ( - <Button fullWidth size="large" variant="outlined" color="primary" component={Link} to="/delegations"> - Delegations - </Button> - ) - } - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - withFilters - /> - <UniversalDataGrid - pagination - loading={Boolean(mixnodes?.isLoading)} - rows={mixnodeToGridRow(filteredMixnodes)} - columns={columns} - pageSize={pageSize} - /> - </Card> - </Grid> - </Grid> - - {itemSelectedForDelegation && ( - <DelegateModal - onClose={() => { - setItemSelectedForDelegation(undefined); - }} - header="Delegate" - buttonText="Delegate stake" - denom="nym" - onOk={(delegationModalProps: DelegationModalProps) => handleNewDelegation(delegationModalProps)} - identityKey={itemSelectedForDelegation.identityKey} - mixId={itemSelectedForDelegation.mixId} - /> - )} - - {confirmationModalProps && ( - <DelegationModal - {...confirmationModalProps} - open={Boolean(confirmationModalProps)} - onClose={async () => { - setConfirmationModalProps(undefined); - if (confirmationModalProps.status === 'success') { - navigate('/delegations'); - } - }} - sx={{ - width: { - xs: '90%', - sm: 600, - }, - }} - /> - )} - </DelegationsProvider> - ); -}; diff --git a/explorer/src/pages/MixnodesMap/index.tsx b/explorer/src/pages/MixnodesMap/index.tsx deleted file mode 100644 index a7c0048a11..0000000000 --- a/explorer/src/pages/MixnodesMap/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react'; -import { Alert, Box, CircularProgress, Grid, SelectChangeEvent, Typography } from '@mui/material'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { ContentCard } from '../../components/ContentCard'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; -import { TableToolbar } from '../../components/TableToolbar'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { WorldMap } from '../../components/WorldMap'; -import { useMainContext } from '../../context/main'; -import { CountryDataRowType, countryDataToGridRow } from '../../utils'; - -export const PageMixnodesMap: FCWithChildren = () => { - const { countryData } = useMainContext(); - const [pageSize, setPageSize] = React.useState<string>('10'); - const [formattedCountries, setFormattedCountries] = React.useState<CountryDataRowType[]>([]); - const [searchTerm, setSearchTerm] = React.useState<string>(''); - - const handleSearch = (str: string) => { - setSearchTerm(str.toLowerCase()); - }; - - const handlePageSize = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - const columns: GridColDef[] = [ - { - field: 'countryName', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => <Typography data-testid="country-name">{params.value}</Typography>, - }, - { - field: 'nodes', - renderHeader: () => <CustomColumnHeading headingTitle="Number of Nodes" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Typography data-testid="number-of-nodes">{params.value}</Typography> - ), - }, - { - field: 'percentage', - renderHeader: () => <CustomColumnHeading headingTitle="Percentage %" />, - flex: 1, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => <Typography data-testid="percentage">{params.value}</Typography>, - }, - ]; - - React.useEffect(() => { - if (countryData?.data && searchTerm === '') { - setFormattedCountries(countryDataToGridRow(Object.values(countryData.data))); - } else if (countryData?.data !== undefined && searchTerm !== '') { - const formatted = countryDataToGridRow(Object.values(countryData?.data)); - const filtered = formatted.filter( - (m) => m?.countryName?.toLowerCase().includes(searchTerm) || m?.ISO3?.toLowerCase().includes(searchTerm), - ); - if (filtered) { - setFormattedCountries(filtered); - } - } - }, [searchTerm, countryData?.data]); - - if (countryData?.isLoading) { - return <CircularProgress />; - } - - if (countryData?.data && !countryData.isLoading) { - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item data-testid="mixnodes-globe"> - <Title text="Mixnodes Around the Globe" /> - </Grid> - <Grid item> - <Grid container spacing={2}> - <Grid item xs={12}> - <ContentCard title="Distribution of nodes"> - <WorldMap loading={false} countryData={countryData} /> - <Box sx={{ marginTop: 2 }} /> - <TableToolbar - onChangeSearch={handleSearch} - onChangePageSize={handlePageSize} - pageSize={pageSize} - searchTerm={searchTerm} - /> - <UniversalDataGrid - pagination - loading={countryData?.isLoading} - columns={columns} - rows={formattedCountries} - pageSize={pageSize} - /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ); - } - - if (countryData?.error) { - return <Alert severity="error">{countryData.error.message}</Alert>; - } - - return null; -}; diff --git a/explorer/src/pages/Overview/index.tsx b/explorer/src/pages/Overview/index.tsx deleted file mode 100644 index 9bc2ebade7..0000000000 --- a/explorer/src/pages/Overview/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as React from 'react'; -import { Box, Grid, Link, Typography } from '@mui/material'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { useTheme } from '@mui/material/styles'; -import { useNavigate } from 'react-router-dom'; -import { PeopleAlt } from '@mui/icons-material'; -import { WorldMap } from '../../components/WorldMap'; -import { useMainContext } from '../../context/main'; -import { formatNumber } from '../../utils'; -import { BIG_DIPPER } from '../../api/constants'; -import { ValidatorsSVG } from '../../icons/ValidatorsSVG'; -import { GatewaysSVG } from '../../icons/GatewaysSVG'; -import { MixnodesSVG } from '../../icons/MixnodesSVG'; -import { Title } from '../../components/Title'; -import { ContentCard } from '../../components/ContentCard'; -import { StatsCard } from '../../components/StatsCard'; -import { Icons } from '../../components/Icons'; - -export const PageOverview: FCWithChildren = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const { summaryOverview, gateways, validators, block, countryData, serviceProviders } = useMainContext(); - return ( - <Box component="main" sx={{ flexGrow: 1 }}> - <Grid> - <Grid item paddingBottom={3}> - <Title text="Overview" /> - </Grid> - <Grid item> - <Grid container spacing={3}> - {summaryOverview && ( - <> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes')} - title="Mixnodes" - icon={<MixnodesSVG />} - count={summaryOverview.data?.mixnodes.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes/active')} - title="Active nodes" - icon={<Icons.Mixnodes.Status.Active />} - color={theme.palette.nym.networkExplorer.mixnodes.status.active} - count={summaryOverview.data?.mixnodes.activeset.active} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/mixnodes/standby')} - title="Standby nodes" - color={theme.palette.nym.networkExplorer.mixnodes.status.standby} - icon={<Icons.Mixnodes.Status.Standby />} - count={summaryOverview.data?.mixnodes.activeset.standby} - errorMsg={summaryOverview?.error} - /> - </Grid> - </> - )} - {gateways && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/gateways')} - title="Gateways" - count={gateways?.data?.length || ''} - errorMsg={gateways?.error} - icon={<GatewaysSVG />} - /> - </Grid> - )} - {serviceProviders && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => navigate('/network-components/service-providers')} - title="Service providers" - icon={<PeopleAlt />} - count={serviceProviders.data?.length} - errorMsg={summaryOverview?.error} - /> - </Grid> - )} - {validators && ( - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => window.open(`${BIG_DIPPER}/validators`)} - title="Validators" - count={validators?.data?.count || ''} - errorMsg={validators?.error} - icon={<ValidatorsSVG />} - /> - </Grid> - )} - {block?.data && ( - <Grid item xs={12}> - <Link - href={`${BIG_DIPPER}/blocks`} - target="_blank" - rel="noreferrer" - underline="none" - color="inherit" - marginY={2} - paddingX={3} - paddingY={0.25} - fontSize={14} - fontWeight={600} - display="flex" - alignItems="center" - > - <Typography fontWeight="inherit" fontSize="inherit"> - Current block height is {formatNumber(block.data)} - </Typography> - <OpenInNewIcon fontWeight="inherit" fontSize="inherit" sx={{ ml: 0.5 }} /> - </Link> - </Grid> - )} - <Grid item xs={12}> - <ContentCard title="Distribution of nodes around the world"> - <WorldMap loading={false} countryData={countryData} /> - </ContentCard> - </Grid> - </Grid> - </Grid> - </Grid> - </Box> - ); -}; diff --git a/explorer/src/pages/ServiceProviders/index.tsx b/explorer/src/pages/ServiceProviders/index.tsx deleted file mode 100644 index 618aa72924..0000000000 --- a/explorer/src/pages/ServiceProviders/index.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import { Box, Button, Card, FormControl, Grid, ListItem, Menu, SelectChangeEvent, Typography } from '@mui/material'; -import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { TableToolbar } from '../../components/TableToolbar'; -import { Title } from '../../components/Title'; -import { UniversalDataGrid } from '../../components/Universal-DataGrid'; -import { useMainContext } from '../../context/main'; -import { CustomColumnHeading } from '../../components/CustomColumnHeading'; - -const columns: GridColDef[] = [ - { - headerName: 'Client ID', - field: 'address', - disableColumnMenu: true, - flex: 3, - }, - { - headerName: 'Type', - field: 'service_type', - disableColumnMenu: true, - flex: 1, - }, - { - headerName: 'Routing score', - field: 'routing_score', - disableColumnMenu: true, - flex: 2, - sortingOrder: ['asc', 'desc'], - sortComparator: (a?: string, b?: string) => { - if (!a) return -1; // Place undefined values at the end - if (!b) return 1; // Place undefined values at the end - - const aToNum = parseInt(a, 10); - const bToNum = parseInt(b, 10); - - if (aToNum > bToNum) return 1; - - return -1; // Sort numbers in ascending order - }, - renderCell: (params: GridRenderCellParams) => (!params.value ? '-' : params.value), - renderHeader: () => ( - <CustomColumnHeading - headingTitle="Routing score" - tooltipInfo="Routing score is only displayed for the service providers that had a successful ping within the last two hours" - /> - ), - }, -]; - -const SupportedApps = () => { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); - const open = Boolean(anchorEl); - const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - const anchorRef = React.useRef<HTMLButtonElement>(null); - - return ( - <FormControl size="small"> - <Button - ref={anchorRef} - onClick={handleClick} - size="large" - variant="outlined" - color="inherit" - sx={{ mr: 2, textTransform: 'capitalize' }} - > - Supported Apps - </Button> - <Menu anchorEl={anchorEl} open={open} onClose={handleClose}> - <ListItem>Keybase</ListItem> - <ListItem>Telegram</ListItem> - <ListItem>Electrum</ListItem> - <ListItem>Blockstream Green</ListItem> - </Menu> - </FormControl> - ); -}; - -export const ServiceProviders = () => { - const { serviceProviders } = useMainContext(); - const [pageSize, setPageSize] = React.useState('10'); - - const handleOnPageSizeChange = (event: SelectChangeEvent<string>) => { - setPageSize(event.target.value); - }; - - return ( - <> - <Box mb={2}> - <Title text="Service Providers" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - }} - > - {serviceProviders?.data ? ( - <> - <TableToolbar - onChangePageSize={handleOnPageSizeChange} - pageSize={pageSize} - childrenBefore={<SupportedApps />} - /> - <UniversalDataGrid - pagination - rows={serviceProviders.data} - columns={columns} - pageSize={pageSize} - initialState={{ - sorting: { - sortModel: [ - { - field: 'routing_score', - sort: 'desc', - }, - ], - }, - }} - /> - </> - ) : ( - <Typography>No service providers to display</Typography> - )} - </Card> - </Grid> - </Grid> - </> - ); -}; diff --git a/explorer/src/routes/index.tsx b/explorer/src/routes/index.tsx deleted file mode 100644 index 67ef8b4621..0000000000 --- a/explorer/src/routes/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { Routes as ReactRouterRoutes, Route } from 'react-router-dom'; -import { Delegations } from '@src/pages/Delegations'; -import { PageOverview } from '../pages/Overview'; -import { PageMixnodesMap } from '../pages/MixnodesMap'; -import { Page404 } from '../pages/404'; -import { NetworkComponentsRoutes } from './network-components'; - -export const Routes: FCWithChildren = () => ( - <ReactRouterRoutes> - <Route path="/" element={<PageOverview />} /> - <Route path="/network-components/*" element={<NetworkComponentsRoutes />} /> - <Route path="/nodemap" element={<PageMixnodesMap />} /> - <Route path="/delegations" element={<Delegations />} /> - <Route path="*" element={<Page404 />} /> - </ReactRouterRoutes> -); diff --git a/explorer/src/routes/network-components.tsx b/explorer/src/routes/network-components.tsx deleted file mode 100644 index 266a75aa2f..0000000000 --- a/explorer/src/routes/network-components.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom'; -import { BIG_DIPPER } from '../api/constants'; -import { PageGateways } from '../pages/Gateways'; -import { PageGatewayDetail } from '../pages/GatewayDetail'; -import { PageMixnodeDetail } from '../pages/MixnodeDetail'; -import { PageMixnodes } from '../pages/Mixnodes'; -import { ServiceProviders } from '../pages/ServiceProviders'; - -const ValidatorRoute: FCWithChildren = () => { - const navigate = useNavigate(); - window.open(`${BIG_DIPPER}/validators`); - navigate(-1); - return null; -}; - -export const NetworkComponentsRoutes: FCWithChildren = () => ( - <ReactRouterRoutes> - <Route path="mixnodes/:status" element={<PageMixnodes />} /> - <Route path="mixnodes" element={<PageMixnodes />} /> - <Route path="mixnode/:id" element={<PageMixnodeDetail />} /> - <Route path="gateways" element={<PageGateways />} /> - <Route path="gateway/:id" element={<PageGatewayDetail />} /> - <Route path="validators" element={<ValidatorRoute />} /> - <Route path="service-providers" element={<ServiceProviders />} /> - </ReactRouterRoutes> -); diff --git a/explorer/src/stories/Introduction.stories.mdx b/explorer/src/stories/Introduction.stories.mdx deleted file mode 100644 index dafab13de0..0000000000 --- a/explorer/src/stories/Introduction.stories.mdx +++ /dev/null @@ -1,7 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - -<Meta title="Introduction" /> - -# Nym Network Explorer Storybook - -This is the Storybook for the Nym Network Explorer. diff --git a/explorer/src/styles.css b/explorer/src/styles.css deleted file mode 100644 index 818211ccea..0000000000 --- a/explorer/src/styles.css +++ /dev/null @@ -1,30 +0,0 @@ -/* last resort for styles that cannot be handled by Material UI and Emotion JS */ - -/* TODO - this override should take place in either -the theme declaration in index.tsx or the style prop -in <DataGrid /> */ - -.MuiDataGrid-columnSeparator { - visibility: hidden; -} - -/* TODO - this should be managed somehow in MUI DataGrid -but documentation doesnt offer a way to do it. Possibly only -included in Data Grid Pro package */ -.MuiDataGrid-header-override { - height: 55px; - min-height: 55px; -} - -/* Again, no offered way to add sx to this specific div to kill the padding -which puts it out of sync with other (sx styled) cells */ -div div.MuiDataGrid-root .MuiDataGrid-columnHeaderTitleContainer { - padding-left: 0; -} - -@media screen and (max-width: 900px) { - .MuiDrawer-paperAnchorLeft { - min-width: 100vw; - margin-top: 58px; - } -} diff --git a/explorer/src/tests/Nav.test.tsx b/explorer/src/tests/Nav.test.tsx deleted file mode 100644 index c3e5e949d0..0000000000 --- a/explorer/src/tests/Nav.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { render } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { Nav } from '../components/Nav'; - -describe('Nav', () => { - beforeEach(() => { - render(<Nav />); - }); - it('should render without exploding', () => { - const { container } = render(<Nav />); - expect(container.firstChild).toBeInTheDocument(); - }); -}); diff --git a/explorer/src/tests/WorldMap.test.tsx b/explorer/src/tests/WorldMap.test.tsx deleted file mode 100644 index 1a5b37d25d..0000000000 --- a/explorer/src/tests/WorldMap.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { WorldMap } from '../components/WorldMap'; - -describe('WorldMap', () => { - beforeEach(() => { - render(<WorldMap loading={false} />); - }); - it('should render without exploding', () => { - const { container } = render(<WorldMap loading={false} />); - expect(container.firstChild).toBeInTheDocument(); - }); - it('should render the expected container/child element', () => { - expect(screen.getByTestId('worldMap__container')).toBeInTheDocument(); - }); - it('should render the title', () => { - expect(screen.getByText('mix-nodes around the globe')).toBeTruthy(); - }); - it('should render the map/SVG', () => { - expect(screen.getByTestId('svg')).toBeInTheDocument(); - }); - it('should render map at correct size/dims', () => { - const expectedWidth = '1000'; - const expectedHeight = '800'; - expect(screen.getByTestId('svg')).toHaveAttribute('width', expectedWidth); - expect(screen.getByTestId('svg')).toHaveAttribute('height', expectedHeight); - }); -}); diff --git a/explorer/src/tests/todo.test.ts b/explorer/src/tests/todo.test.ts deleted file mode 100644 index 0580a22ccc..0000000000 --- a/explorer/src/tests/todo.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -describe('testing jest config', () => { - test('jest works with typescript', () => { - const foo = () => 42; - - expect(foo()).toBe(42); - }); -}); diff --git a/explorer/src/theme/index.tsx b/explorer/src/theme/index.tsx deleted file mode 100644 index 3542bd9c9a..0000000000 --- a/explorer/src/theme/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as React from 'react'; -import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme'; -import { useMainContext } from '../context/main'; - -export const NetworkExplorerThemeProvider: FCWithChildren = ({ children }) => { - const { mode } = useMainContext(); - return <NymNetworkExplorerThemeProvider mode={mode}>{children}</NymNetworkExplorerThemeProvider>; -}; diff --git a/explorer/src/theme/mui-theme.d.ts b/explorer/src/theme/mui-theme.d.ts deleted file mode 100644 index fc1f856f91..0000000000 --- a/explorer/src/theme/mui-theme.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme {} - interface ThemeOptions extends Partial<NymTheme> {} - interface Palette extends NymPaletteWithExtensions {} - interface PaletteOptions extends NymPaletteWithExtensionsOptions {} -} diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts deleted file mode 100644 index 9319c33952..0000000000 --- a/explorer/src/typeDefs/explorer-api.ts +++ /dev/null @@ -1,277 +0,0 @@ -/* eslint-disable camelcase */ - -export interface ClientConfig { - url: string; - version: string; -} - -export interface SummaryOverviewResponse { - mixnodes: { - count: number; - activeset: { - active: number; - standby: number; - inactive: number; - }; - }; - gateways: { - count: number; - }; - validators: { - count: number; - }; -} - -export interface MixNode { - host: string; - mix_port: number; - http_api_port: number; - verloc_port: number; - sphinx_key: string; - identity_key: string; - version: string; - location: string; -} - -export interface Gateway { - host: string; - mix_port: number; - clients_port: number; - location: string; - sphinx_key: string; - identity_key: string; - version: string; -} - -export interface Amount { - denom: string; - amount: number; -} - -export enum MixnodeStatus { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set -} - -export enum MixnodeStatusWithAll { - active = 'active', // in both the active set and the rewarded set - standby = 'standby', // only in the rewarded set - inactive = 'inactive', // in neither the rewarded set nor the active set - all = 'all', // any status -} - -export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | undefined => { - if (!status || status === MixnodeStatusWithAll.all) { - return undefined; - } - return status as unknown as MixnodeStatus; -}; - -export interface MixNodeResponseItem { - mix_id: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - layer: string; - status: MixnodeStatus; - location: { - country_name: string; - latitude?: number; - longitude?: number; - three_letter_iso_country_code: string; - two_letter_iso_country_code: string; - }; - mix_node: MixNode; - avg_uptime: number; - node_performance: NodePerformance; - stake_saturation: number; - uncapped_saturation: number; - operating_cost: Amount; - profit_margin_percent: string; - blacklisted: boolean; -} - -export type MixNodeResponse = MixNodeResponseItem[]; - -export interface MixNodeReportResponse { - identity: string; - owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; -} - -export interface StatsResponse { - update_time: Date; - previous_update_time: Date; - packets_received_since_startup: number; - packets_sent_since_startup: number; - packets_explicitly_dropped_since_startup: number; - packets_received_since_last_update: number; - packets_sent_since_last_update: number; - packets_explicitly_dropped_since_last_update: number; -} - -export interface NodePerformance { - most_recent: string; - last_hour: string; - last_24h: string; -} - -export type MixNodeHistoryResponse = StatsResponse; - -export interface GatewayBond { - block_height: number; - pledge_amount: Amount; - total_delegation: Amount; - owner: string; - gateway: Gateway; - node_performance: NodePerformance; - location?: Location; -} - -export interface GatewayBondAnnotated { - gateway_bond: GatewayBond; - node_performance: NodePerformance; -} - -export interface Location { - two_letter_iso_country_code: string; - three_letter_iso_country_code: string; - country_name: string; - latitude?: number; - longitude?: number; -} - -export interface LocatedGateway { - pledge_amount: Amount; - owner: string; - block_height: number; - gateway: Gateway; - proxy?: string; - location?: Location; -} - -export type GatewayResponse = GatewayBond[]; - -export interface GatewayReportResponse { - identity: string; - owner: string; - most_recent: number; - last_hour: number; - last_day: number; -} - -export type GatewayHistoryResponse = StatsResponse; - -export interface MixNodeDescriptionResponse { - name: string; - description: string; - link: string; - location: string; -} - -export type MixNodeStatsResponse = StatsResponse; - -export interface Validator { - address: string; - proposer_priority: string; - pub_key: { - type: string; - value: string; - }; -} -export interface ValidatorsResponse { - block_height: number; - count: string; - total: string; - validators: Validator[]; -} - -export type CountryData = { - ISO3: string; - nodes: number; -}; - -export type Delegation = { - owner: string; - amount: Amount; - block_height: number; -}; - -export type DelegationUniq = { - owner: string; - amount: Amount; -}; - -export type DelegationsResponse = Delegation[]; - -export type UniqDelegationsResponse = DelegationUniq[]; - -export interface CountryDataResponse { - [threeLetterCountryCode: string]: CountryData; -} - -export type BlockType = number; -export type BlockResponse = BlockType; - -export interface ApiState<RESPONSE> { - isLoading: boolean; - data?: RESPONSE; - error?: Error; -} - -export type StatusResponse = { - pending: boolean; - ports: { - 1789: boolean; - 1790: boolean; - 8000: boolean; - }; -}; - -export type UptimeTime = { - date: string; - uptime: number; -}; - -export type UptimeStoryResponse = { - history: UptimeTime[]; - identity: string; - owner: string; -}; - -export type MixNodeEconomicDynamicsStatsResponse = { - stake_saturation: number; - uncapped_saturation: number; - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - active_set_inclusion_probability: 'High' | 'Good' | 'Low'; - reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; - estimated_total_node_reward: number; - estimated_operator_reward: number; - estimated_delegators_reward: number; - current_interval_uptime: number; -}; - -export type Environment = 'mainnet' | 'sandbox' | 'qa'; - -export type ServiceProviderType = 'Network Requester'; - -export type DirectoryServiceProvider = { - id: string; - description: string; - address: string; - gateway: string; - routing_score: string | null; - service_type: ServiceProviderType; -}; - -export type DirectoryService = { - id: string; - description: string; - items: DirectoryServiceProvider[]; -}; diff --git a/explorer/src/typeDefs/filters.ts b/explorer/src/typeDefs/filters.ts deleted file mode 100644 index 53de60cb87..0000000000 --- a/explorer/src/typeDefs/filters.ts +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Mark } from '@mui/base'; - -export enum EnumFilterKey { - profitMargin = 'profitMargin', - stakeSaturation = 'stakeSaturation', - routingScore = 'routingScore', -} - -export type TFilterItem = { - label: string; - id: EnumFilterKey; - value: number[]; - isSmooth?: boolean; - marks: Mark[]; - min?: number; - max?: number; - scale?: (value: number) => number; - tooltipInfo?: string; -}; - -export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/explorer/src/typeDefs/network.ts b/explorer/src/typeDefs/network.ts deleted file mode 100644 index f8615cd992..0000000000 --- a/explorer/src/typeDefs/network.ts +++ /dev/null @@ -1 +0,0 @@ -export type Network = 'QA' | 'SANDBOX' | 'MAINNET'; diff --git a/explorer/src/typeDefs/tables.ts b/explorer/src/typeDefs/tables.ts deleted file mode 100644 index 1ab2ffead4..0000000000 --- a/explorer/src/typeDefs/tables.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type TableHeading = { - id: string; - numeric: boolean; - disablePadding: boolean; - label: string; -}; - -export type TableHeadingsType = TableHeading[]; diff --git a/explorer/src/typings/FC.d.ts b/explorer/src/typings/FC.d.ts deleted file mode 100644 index 08ebdfa298..0000000000 --- a/explorer/src/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>; diff --git a/explorer/src/typings/jpeg.d.ts b/explorer/src/typings/jpeg.d.ts deleted file mode 100644 index af2ed72913..0000000000 --- a/explorer/src/typings/jpeg.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} - -declare module '*.jpg' { - const value: any; - export default value; -} diff --git a/explorer/src/typings/json.d.ts b/explorer/src/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/explorer/src/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/explorer/src/typings/png.d.ts b/explorer/src/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/explorer/src/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/explorer/src/typings/react-identicons.d.ts b/explorer/src/typings/react-identicons.d.ts deleted file mode 100644 index 6c460eff99..0000000000 --- a/explorer/src/typings/react-identicons.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'react-identicons' { - import * as React from 'react'; - - interface IdenticonProps { - string: string; - size?: number; - padding?: number; - bg?: string; - fg?: string; - palette?: string[]; - count?: number; - // getColor: Function; - } - - declare function Identicon(props: IdenticonProps): React.ReactElement<IdenticonProps>; - - export default Identicon; -} diff --git a/explorer/src/typings/react-tooltip.d.ts b/explorer/src/typings/react-tooltip.d.ts deleted file mode 100644 index 98cb6d592d..0000000000 --- a/explorer/src/typings/react-tooltip.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-tooltip'; diff --git a/explorer/src/typings/svg.d.ts b/explorer/src/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/explorer/src/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/explorer/src/utils/currency.ts b/explorer/src/utils/currency.ts deleted file mode 100644 index ad6ed1f165..0000000000 --- a/explorer/src/utils/currency.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { printableCoin } from '@nymproject/nym-validator-client'; -import Big from 'big.js'; -import { DecCoin, isValidRawCoin } from '@nymproject/types'; - -const DENOM = process.env.CURRENCY_DENOM || 'unym'; -const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; - -export const toDisplay = (val: string | number | Big, dp = 4) => { - let displayValue; - try { - displayValue = Big(val).toFixed(dp); - } catch (e: any) { - console.warn(`${displayValue} not a valid decimal number: ${e}`); - } - return displayValue; -}; - -export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string; dp?: number; denom?: string }) => { - if (!dp) { - printableCoin({ - amount, - denom, - }); - } - - const [printableAmount, printableDenom] = printableCoin({ - amount, - denom, - }).split(/\s+/); - - return `${toDisplay(printableAmount, dp)} ${printableDenom}`; -}; - -export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => - printableCoin({ - amount, - denom, - }); - -/** - * Converts a decimal number to a pretty representation - * with fixed decimal places. - * - * @param val - a decimal number of string form - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns A prettyfied decimal number - */ - -/** - * Converts a decimal number of μNYM (micro NYM) to NYM. - * - * @param unym - a decimal number of μNYM - * @param dp - number of decimal places (4 by default ie. 0.0000) - * @returns The corresponding decimal number in NYM - */ -export const unymToNym = (unym: string | number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return nym; -}; - -export const validateAmount = async ( - majorAmountAsString: DecCoin['amount'], - minimumAmountAsString: DecCoin['amount'], -): Promise<boolean> => { - // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc - if (!Number(majorAmountAsString)) { - return false; - } - - if (!isValidRawCoin(majorAmountAsString)) { - return false; - } - - const majorValueFloat = parseInt(majorAmountAsString, Number(10)); - - return majorValueFloat >= parseInt(minimumAmountAsString, Number(10)); -}; - -/** - * Takes a DecCoin and prettify its amount to a representation - * with fixed decimal places. - * - * @param coin - a DecCoin - * @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000) - * @returns A DecCoin with prettified amount - */ -export const decCoinToDisplay = (coin: DecCoin, dp = 4) => { - const displayCoin = { ...coin }; - try { - displayCoin.amount = Big(coin.amount).toFixed(dp); - } catch (e: any) { - console.warn(`${coin.amount} not a valid decimal number: ${e}`); - } - return displayCoin; -}; diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts deleted file mode 100644 index 91b082c5ca..0000000000 --- a/explorer/src/utils/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* eslint-disable camelcase */ -import { MutableRefObject } from 'react'; -import { Theme } from '@mui/material/styles'; -import { registerLocale, getName } from 'i18n-iso-countries'; -import Big from 'big.js'; -import { CountryData } from '../typeDefs/explorer-api'; -import { EconomicsRowsType } from '../components/MixNodes/Economics/types'; -import { Network } from '../typeDefs/network'; - -registerLocale(require('i18n-iso-countries/langs/en.json')); - -export function formatNumber(num: number): string { - return new Intl.NumberFormat().format(num); -} - -export function scrollToRef(ref: MutableRefObject<HTMLDivElement | undefined>): void { - if (ref?.current) ref.current.scrollIntoView(); -} - -export type CountryDataRowType = { - id: number; - ISO3: string; - nodes: number; - countryName: string; - percentage: string; -}; - -export function countryDataToGridRow(countriesData: CountryData[]): CountryDataRowType[] { - const totalNodes = countriesData.reduce((acc, obj) => acc + obj.nodes, 0); - const formatted = countriesData.map((each: CountryData, index: number) => { - const updatedCountryRecord: CountryDataRowType = { - ...each, - id: index, - countryName: getName(each.ISO3, 'en', { select: 'alias' }), - percentage: ((each.nodes * 100) / totalNodes).toFixed(1), - }; - return updatedCountryRecord; - }); - - const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1)); - return sorted; -} - -export const splice = (start: number, deleteCount: number, address?: string): string => { - if (address) { - const array = address.split(''); - array.splice(start, deleteCount, '...'); - return array.join(''); - } - return ''; -}; - -export const trimAddress = (address = '', trimBy = 6) => `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`; - -/** - * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). - * - * @param value - the percentage to convert - * @returns A stringified integer - */ -export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); -export const toPercentInteger = (value: string) => Math.round(Number(value) * 100); - -export const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { - const progressBarValue = value?.progressBarValue || 0; - const fieldValue = value.value; - - if (progressBarValue > 100) { - return theme.palette.warning.main; - } - if (field === 'selectionChance') { - // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow - switch (fieldValue) { - case 'High': - case 'VeryHigh': - return theme.palette.nym.networkExplorer.selectionChance.overModerate; - case 'Good': - case 'Moderate': - return theme.palette.nym.networkExplorer.selectionChance.moderate; - case 'Low': - case 'VeryLow': - return theme.palette.nym.networkExplorer.selectionChance.underModerate; - default: - return theme.palette.nym.wallet.fee; - } - } - return theme.palette.nym.wallet.fee; -}; - -export const isGreaterThan = (a: number, b: number) => a > b; - -export const isLessThan = (a: number, b: number) => a < b; - -/** - * - * Checks if the user's balance is enough to pay the fee - * @param balance - The user's current balance - * @param fee - The fee for the tx - * @param tx - The amount of the tx - * @returns boolean - * - */ - -export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - try { - return Big(balance).gte(Big(fee).plus(Big(tx))); - } catch (e) { - console.log(e); - return false; - } -}; - -export const urls = (networkName?: Network) => - networkName === 'MAINNET' - ? { - mixnetExplorer: 'https://mixnet.explorers.guru/', - blockExplorer: 'https://blocks.nymtech.net', - networkExplorer: 'https://explorer.nymtech.net', - } - : { - blockExplorer: `https://${networkName}-blocks.nymtech.net`, - networkExplorer: `https://${networkName}-explorer.nymtech.net`, - }; diff --git a/explorer/src/x-data-grid/LICENSE b/explorer/src/x-data-grid/LICENSE deleted file mode 100644 index 126bc57eaa..0000000000 --- a/explorer/src/x-data-grid/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Material-UI SAS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/explorer/src/x-data-grid/README.md b/explorer/src/x-data-grid/README.md deleted file mode 100644 index c163d7ceeb..0000000000 --- a/explorer/src/x-data-grid/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# @mui/x-data-grid - -This package is the community edition of the data grid component. -It's part of Material-UI X, an open core extension of Material-UI, with advanced components. - -## Installation - -Install the package in your project directory with: - -```sh -// with npm -npm install @mui/x-data-grid - -// with yarn -yarn add @mui/x-data-grid -``` - -This component has two peer dependencies that you will need to install as well. - -```json -"peerDependencies": { - "@material-ui/core": "^4.12.0 || ^5.0.0-beta.0", - "react": "^17.0.0" -}, -``` - -## Documentation - -[The documentation](https://material-ui.com/components/data-grid/) diff --git a/explorer/src/x-data-grid/package.json b/explorer/src/x-data-grid/package.json deleted file mode 100644 index 46225fa760..0000000000 --- a/explorer/src/x-data-grid/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "_from": "@mui/x-data-grid", - "_id": "@mui/x-data-grid@4.0.0", - "_inBundle": false, - "_integrity": "sha512-BYn7uLx5tJbMarcWltjjVArWNBdC22/2xOpq3Azhltbb3TRx3h2RLUeKwZI685xmGHmzvtu6QqaoYqQgFe/h+g==", - "_location": "/@mui/x-data-grid", - "_phantomChildren": {}, - "_requested": { - "type": "tag", - "registry": true, - "raw": "@mui/x-data-grid", - "name": "@mui/x-data-grid", - "escapedName": "@mui%2fx-data-grid", - "scope": "@mui", - "rawSpec": "", - "saveSpec": null, - "fetchSpec": "latest" - }, - "_requiredBy": [ - "#USER", - "/" - ], - "_resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-4.0.0.tgz", - "_shasum": "e9e9c33a8b86e85872c48f30f4a8de72ee819153", - "_spec": "@mui/x-data-grid", - "_where": "/Users/adrianthompson/Documents/nym/explorer", - "author": { - "name": "MUI Team" - }, - "bugs": { - "url": "https://github.com/mui-org/material-ui-x/issues" - }, - "bundleDependencies": false, - "dependencies": { - "@material-ui/utils": "^5.0.0-beta.4", - "clsx": "^1.1.1", - "prop-types": "^15.7.2", - "reselect": "^4.0.0" - }, - "deprecated": false, - "description": "The community edition of the data grid component (Material-UI X).", - "engines": { - "node": ">=12.0.0" - }, - "files": [ - "dist/*" - ], - "gitHead": "940d6238fe499f027ee27e818d714f79ca40e9a5", - "homepage": "https://material-ui.com/components/data-grid/", - "keywords": [ - "react", - "react-component", - "material-ui", - "mui", - "react-table", - "table", - "datatable", - "data-table", - "datagrid", - "data-grid" - ], - "license": "MIT", - "main": "dist/index-cjs.js", - "module": "dist/index-esm.js", - "name": "@mui/x-data-grid", - "peerDependencies": { - "@material-ui/core": "^4.12.0 || ^5.0.0-beta.0", - "@mui/styles": "^4.11.4 || ^5.0.0-beta.0", - "react": "^17.0.0" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/mui-org/material-ui-x.git", - "directory": "packages/grid/data-grid" - }, - "scripts": { - "build": "cd ../ && rollup --config rollup.data-grid.config.js", - "typescript": "tsc -p tsconfig.json" - }, - "setupFiles": [ - "<rootDir>/src/setupTests.js" - ], - "sideEffects": false, - "types": "dist/data-grid.d.ts", - "version": "4.0.0" -} \ No newline at end of file diff --git a/explorer/tsconfig.json b/explorer/tsconfig.json deleted file mode 100644 index 093340052d..0000000000 --- a/explorer/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../ts-packages/tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx", - "outDir": "./dist", - "module": "CommonJS", - "baseUrl": ".", - "paths": { - "@src/*": ["./src/*"], - "@assets/*": ["../assets/*"] - } - }, - "include": ["./src/**/*.ts", "./src/**/*.tsx"], - "exclude": ["node_modules", "build", "dist"] -} diff --git a/explorer/webpack.common.js b/explorer/webpack.common.js deleted file mode 100644 index 7bb9cb9032..0000000000 --- a/explorer/webpack.common.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -const { mergeWithRules } = require('webpack-merge'); -const { webpackCommon } = require('@nymproject/webpack'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(webpackCommon(__dirname), { - entry: path.resolve(__dirname, 'src/index.tsx'), - output: { - path: path.resolve(__dirname, 'dist'), - publicPath: '/', - }, - resolve: { - fallback: { - fs: false, - tls: false, - path: false, - http: false, - https: false, - stream: false, - crypto: false, - net: false, - zlib: false, - buffer: require.resolve('buffer'), - }, - }, -}); diff --git a/explorer/webpack.config.js b/explorer/webpack.config.js deleted file mode 100644 index 550951cb52..0000000000 --- a/explorer/webpack.config.js +++ /dev/null @@ -1,76 +0,0 @@ -const { mergeWithRules } = require('webpack-merge'); -const webpack = require('webpack'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const ReactRefreshTypeScript = require('react-refresh-typescript'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'development', - devtool: 'inline-source-map', - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - options: { - getCustomTransformers: () => ({ - before: [ReactRefreshTypeScript()], - }), - // `ts-loader` does not work with HMR unless `transpileOnly` is used. - // If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative. - transpileOnly: true, - }, - }, - ], - }, - plugins: [ - new ReactRefreshWebpackPlugin(), - - // this can be included automatically by the dev server, however build mode fails if missing - new webpack.HotModuleReplacementPlugin(), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - ], - - target: 'web', - - devServer: { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - historyApiFallback: true, - }, - - // recommended for faster rebuild - optimization: { - runtimeChunk: true, - removeAvailableModules: false, - removeEmptyChunks: false, - splitChunks: false, - }, - - cache: { - type: 'filesystem', - buildDependencies: { - // restart on config change - config: ['./webpack.config.js'], - }, - }, -}); diff --git a/explorer/webpack.prod.js b/explorer/webpack.prod.js deleted file mode 100644 index 259c32a4af..0000000000 --- a/explorer/webpack.prod.js +++ /dev/null @@ -1,52 +0,0 @@ -const webpack = require('webpack'); -const { mergeWithRules } = require('webpack-merge'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'production', - - // TODO: no source maps, add back - devtool: false, - - module: { - rules: [ - { - test: /\.m?js/, - resolve: { - fullySpecified: false, - }, - }, - { - test: /\.css$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - ], - }, - optimization: { - minimizer: [`...`, new CssMinimizerPlugin()], - splitChunks: { - chunks: 'all', - }, - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css', - }), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - }), - ], - output: { - pathinfo: false, - filename: '[name].[contenthash].js', - }, -}); diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 076f1304f8..13d80f7a14 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -11,7 +11,7 @@ authors = [ ] description = "Implementation of the Nym Mixnet Gateway" edition = "2021" -rust-version = "1.76" +rust-version = "1.77" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -20,13 +20,16 @@ path = "src/lib.rs" [dependencies] anyhow = { workspace = true } +bincode = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } bs58 = { workspace = true } dashmap = { workspace = true } +fastrand = { workspace = true } futures = { workspace = true } ipnetwork = { workspace = true } rand = { workspace = true } +serde = { workspace = true, features = ["derive"] } sha2 = { workspace = true } thiserror = { workspace = true } time = { workspace = true } @@ -44,13 +47,13 @@ tracing = { workspace = true } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true } + # internal -nym-authenticator = { path = "../service-providers/authenticator" } nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-credential-verification = { path = "../common/credential-verification" } -nym-crypto = { path = "../common/crypto" } +nym-crypto = { path = "../common/crypto", features = ["sphinx"] } nym-gateway-storage = { path = "../common/gateway-storage" } nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" } nym-gateway-requests = { path = "../common/gateway-requests" } @@ -69,15 +72,19 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } nym-node-metrics = { path = "../nym-node/nym-node-metrics" } nym-wireguard = { path = "../common/wireguard" } +nym-wireguard-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } +nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-client-core = { path = "../common/client-core", features = ["cli"] } +nym-id = { path = "../common/nym-id" } +nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } + + defguard_wireguard_rs = { workspace = true } -[build-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = [ - "runtime-tokio-rustls", - "sqlite", - "macros", - "migrate", -] } +[dev-dependencies] +nym-gateway-storage = { path = "../common/gateway-storage", features = ["mock"] } +nym-wireguard = { path = "../common/wireguard", features = ["mock"] } +mock_instant = "0.6.0" +time = { workspace = true } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 474d2cbf64..849f658a26 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use nym_authenticator::error::AuthenticatorError; +use crate::node::internal_service_providers::authenticator::error::AuthenticatorError; use nym_gateway_stats_storage::error::StatsStorageError; use nym_gateway_storage::error::GatewayStorageError; use nym_ip_packet_router::error::IpPacketRouterError; @@ -69,10 +69,7 @@ pub enum GatewayError { }, #[error("there was an issue with the local authenticator: {source}")] - AuthenticatorFailure { - #[from] - source: AuthenticatorError, - }, + AuthenticatorFailure { source: Box<AuthenticatorError> }, #[error("failed to startup local {typ}")] ServiceProviderStartupFailure { typ: &'static str }, @@ -138,3 +135,11 @@ impl From<ClientCoreError> for GatewayError { } } } + +impl From<AuthenticatorError> for GatewayError { + fn from(error: AuthenticatorError) -> Self { + GatewayError::AuthenticatorFailure { + source: Box::new(error), + } + } +} diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index abac82f783..0664d95761 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -10,3 +10,5 @@ pub mod node; pub use error::GatewayError; pub use node::GatewayTasksBuilder; + +pub use node::internal_service_providers::authenticator as nym_authenticator; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index eeff95d907..a215affdf9 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -22,7 +22,7 @@ enum ActiveClient { Remote(RemoteClientData), /// Handle to a locally (inside the same process) running client. - Embedded(LocalEmbeddedClientHandle), + Embedded(Box<LocalEmbeddedClientHandle>), } impl ActiveClient { @@ -163,7 +163,7 @@ impl ActiveClientsStore { /// Inserts a handle to the embedded client pub fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) { let key = local_client_handle.client_destination(); - let entry = ActiveClient::Embedded(local_client_handle); + let entry = ActiveClient::Embedded(Box::new(local_client_handle)); if self.inner.insert(key, entry).is_some() { // this is literally impossible since we're starting the local embedded client before // even spawning the websocket listener task diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 16c5a42faa..a152092ef7 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -18,16 +18,17 @@ use nym_credential_verification::CredentialVerifier; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ClientBandwidth, }; -use nym_credentials_interface::TicketType; use nym_gateway_requests::{ types::{BinaryRequest, ServerResponse}, ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, SimpleGatewayRequestsError, }; use nym_gateway_storage::error::GatewayStorageError; +use nym_gateway_storage::traits::BandwidthGatewayStorage; +use nym_gateway_storage::traits::SharedKeyGatewayStorage; use nym_node_metrics::events::MetricsEvent; use nym_sphinx::forwarding::packet::MixPacket; -use nym_statistics_common::gateways::GatewaySessionEvent; +use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType}; use nym_task::TaskClient; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; @@ -191,7 +192,7 @@ impl<R, S> AuthenticatedHandler<R, S> { let handler = AuthenticatedHandler { bandwidth_storage_manager: BandwidthStorageManager::new( - fresh.shared_state.storage.clone(), + Box::new(fresh.shared_state.storage.clone()), ClientBandwidth::new(bandwidth.into()), client.id, fresh.shared_state.cfg.bandwidth, @@ -258,7 +259,7 @@ impl<R, S> AuthenticatedHandler<R, S> { &self.client.shared_keys, iv, )?; - let maybe_ticket_type = TicketType::try_from_encoded(credential.data.payment.t_type); + let mut verifier = CredentialVerifier::new( credential, self.inner.shared_state.ecash_verifier.clone(), @@ -271,14 +272,6 @@ impl<R, S> AuthenticatedHandler<R, S> { .inspect_err(|verification_failure| debug!("{verification_failure}"))?; trace!("available total bandwidth: {available_total}"); - if let Ok(ticket_type) = maybe_ticket_type { - self.inner.shared_state.metrics_sender.report_unchecked( - GatewaySessionEvent::new_ecash_ticket(self.client.address, ticket_type), - ); - } else { - error!("Somehow verified a ticket with an unknown ticket type"); - } - Ok(ServerResponse::Bandwidth { available_total }) } @@ -323,7 +316,8 @@ impl<R, S> AuthenticatedHandler<R, S> { } Ok(request) => match request { // currently only a single type exists - BinaryRequest::ForwardSphinx { packet } => { + BinaryRequest::ForwardSphinx { packet } + | BinaryRequest::ForwardSphinxV2 { packet } => { self.handle_forward_sphinx(packet).await.into_ws_message() } _ => RequestHandlingError::UnknownBinaryRequest.into_error_message(), @@ -334,7 +328,7 @@ impl<R, S> AuthenticatedHandler<R, S> { async fn handle_forget_me( &mut self, client: bool, - stats: bool, + _stats: bool, ) -> Result<ServerResponse, RequestHandlingError> { if client { self.inner() @@ -343,12 +337,20 @@ impl<R, S> AuthenticatedHandler<R, S> { .handle_forget_me(self.client.address) .await?; } - if stats { - self.send_metrics(GatewaySessionEvent::new_session_delete(self.client.address)); - } Ok(SensitiveServerResponse::ForgetMeAck {}.encrypt(&self.client.shared_keys)?) } + async fn handle_remember_me( + &self, + session_type: SessionType, + ) -> Result<ServerResponse, RequestHandlingError> { + self.send_metrics(GatewaySessionEvent::new_session_remember( + session_type, + self.client.address, + )); + Ok(SensitiveServerResponse::RememberMeAck {}.encrypt(&self.client.shared_keys)?) + } + async fn handle_key_upgrade( &mut self, hkdf_salt: Vec<u8>, @@ -393,6 +395,9 @@ impl<R, S> AuthenticatedHandler<R, S> { derived_key_digest, } => self.handle_key_upgrade(hkdf_salt, derived_key_digest).await, ClientRequest::ForgetMe { client, stats } => self.handle_forget_me(client, stats).await, + ClientRequest::RememberMe { session_type } => { + self.handle_remember_me(session_type).await + } _ => Err(RequestHandlingError::UnknownEncryptedTextRequest), } } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 23d9604814..2edbf16b4c 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -27,6 +27,9 @@ use nym_gateway_requests::{ INITIAL_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; +use nym_gateway_storage::traits::BandwidthGatewayStorage; +use nym_gateway_storage::traits::InboxGatewayStorage; +use nym_gateway_storage::traits::SharedKeyGatewayStorage; use nym_node_metrics::events::MetricsEvent; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; @@ -83,7 +86,7 @@ pub(crate) enum InitialAuthenticationError { UnexpectedMessageType { typ: String }, #[error("Experienced connection error: {0}")] - ConnectionError(#[from] WsError), + ConnectionError(Box<WsError>), #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] IncompatibleProtocol { client: Option<u8>, current: u8 }, @@ -91,7 +94,7 @@ pub(crate) enum InitialAuthenticationError { #[error("failed to send authentication response: {source}")] ResponseSendFailure { #[source] - source: WsError, + source: Box<WsError>, }, #[error("possibly received a sphinx packet without prior authentication. Request is going to be ignored")] @@ -103,7 +106,7 @@ pub(crate) enum InitialAuthenticationError { #[error("failed to obtain message from websocket stream: {source}")] FailedToReadMessage { #[source] - source: WsError, + source: Box<WsError>, }, #[error("timed out while waiting for initial data")] @@ -113,6 +116,12 @@ pub(crate) enum InitialAuthenticationError { EmptyClientDetails, } +impl From<WsError> for InitialAuthenticationError { + fn from(error: WsError) -> Self { + InitialAuthenticationError::ConnectionError(Box::new(error)) + } +} + pub(crate) struct FreshHandler<R, S> { rng: R, pub(crate) shared_state: CommonHandlerState, @@ -163,7 +172,7 @@ impl<R, S> FreshHandler<R, S> { SocketStream::RawTcp(conn) => { // TODO: perhaps in the future, rather than panic here (and uncleanly shut tcp stream) // return a result with an error? - let ws_stream = tokio_tungstenite::accept_async(conn).await?; + let ws_stream = Box::new(tokio_tungstenite::accept_async(conn).await?); SocketStream::UpgradedWebSocket(ws_stream) } other => other, @@ -342,7 +351,7 @@ impl<R, S> FreshHandler<R, S> { // push them to the client if let Err(err) = self.push_packets_to_client(shared_keys, messages).await { warn!("We failed to send stored messages to fresh client - {err}",); - return Err(InitialAuthenticationError::ConnectionError(err)); + return Err(InitialAuthenticationError::ConnectionError(Box::new(err))); } else { // if it was successful - remove them from the store self.shared_state.storage.remove_messages(ids).await?; @@ -413,6 +422,13 @@ impl<R, S> FreshHandler<R, S> { return Ok(INITIAL_PROTOCOL_VERSION); }; + // ##### + // On backwards compat: + // Currently it is the case that gateways will understand all previous protocol versions + // and will downgrade accordingly, but this will now always be the case. + // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected + // ##### + // a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses if client_protocol_version == 1 { return Ok(1); @@ -428,6 +444,11 @@ impl<R, S> FreshHandler<R, S> { return Ok(3); } + // a v5 gateway will understand v4 requests (key-rotation) + if client_protocol_version == 4 { + return Ok(4); + } + // we can't handle clients with higher protocol than ours // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) if client_protocol_version <= CURRENT_PROTOCOL_VERSION { @@ -880,7 +901,9 @@ impl<R, S> FreshHandler<R, S> { .await { debug!("failed to send authentication response: {source}"); - return Err(InitialAuthenticationError::ResponseSendFailure { source }); + return Err(InitialAuthenticationError::ResponseSendFailure { + source: Box::new(source), + }); } let Some(client_details) = auth_result.client_details else { @@ -963,7 +986,9 @@ impl<R, S> FreshHandler<R, S> { Ok(Some(Ok(msg))) => msg, Ok(Some(Err(source))) => { debug!("failed to obtain message from websocket stream! stopping connection handler: {source}"); - return Err(InitialAuthenticationError::FailedToReadMessage { source }); + return Err(InitialAuthenticationError::FailedToReadMessage { + source: Box::new(source), + }); } Ok(None) => return Err(InitialAuthenticationError::ClosedConnection), Err(_timeout) => return Err(InitialAuthenticationError::Timeout), diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index 38026fa8be..0ce52eec00 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -31,7 +31,7 @@ const INITIAL_MESSAGE_TIMEOUT: Duration = Duration::from_millis(10_000); pub(crate) enum SocketStream<S> { RawTcp(S), - UpgradedWebSocket(WebSocketStream<S>), + UpgradedWebSocket(Box<WebSocketStream<S>>), Invalid, } diff --git a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs new file mode 100644 index 0000000000..1a1e542368 --- /dev/null +++ b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs @@ -0,0 +1,87 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use nym_network_defaults::{ + WG_METADATA_PORT, WG_TUNNEL_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, + WG_TUN_DEVICE_NETMASK_V4, WG_TUN_DEVICE_NETMASK_V6, +}; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + +pub use nym_client_core::config::Config as BaseClientConfig; +pub use persistence::AuthenticatorPaths; + +pub mod persistence; + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct Config { + #[serde(flatten)] + pub base: BaseClientConfig, + + #[serde(default)] + pub authenticator: Authenticator, + + pub storage_paths: AuthenticatorPaths, +} + +impl Config { + pub fn validate(&self) -> bool { + // no other sections have explicit requirements (yet) + self.base.validate() + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct Authenticator { + /// Socket address this node will use for binding its wireguard interface. + /// default: `0.0.0.0:51822` + pub bind_address: SocketAddr, + + /// Private IP address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IP address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub tunnel_announced_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, +} + +impl Default for Authenticator { + fn default() -> Self { + Self { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_TUNNEL_PORT), + private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, + private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, + tunnel_announced_port: WG_TUNNEL_PORT, + private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, + private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, + } + } +} + +impl From<Authenticator> for nym_wireguard_types::Config { + fn from(value: Authenticator) -> Self { + nym_wireguard_types::Config { + bind_address: value.bind_address, + private_ipv4: value.private_ipv4, + private_ipv6: value.private_ipv6, + announced_tunnel_port: value.tunnel_announced_port, + announced_metadata_port: WG_METADATA_PORT, + private_network_prefix_v4: value.private_network_prefix_v4, + private_network_prefix_v6: value.private_network_prefix_v6, + } + } +} diff --git a/service-providers/authenticator/src/config/persistence.rs b/gateway/src/node/internal_service_providers/authenticator/config/persistence.rs similarity index 100% rename from service-providers/authenticator/src/config/persistence.rs rename to gateway/src/node/internal_service_providers/authenticator/config/persistence.rs diff --git a/service-providers/authenticator/src/error.rs b/gateway/src/node/internal_service_providers/authenticator/error.rs similarity index 84% rename from service-providers/authenticator/src/error.rs rename to gateway/src/node/internal_service_providers/authenticator/error.rs index 59a1597147..ac9b4fd261 100644 --- a/service-providers/authenticator/src/error.rs +++ b/gateway/src/node/internal_service_providers/authenticator/error.rs @@ -17,6 +17,9 @@ pub enum AuthenticatorError { #[error("{0}")] CredentialVerificationError(#[from] nym_credential_verification::Error), + #[error("invalid credential type")] + InvalidCredentialType, + #[error("the entity wrapping the network requester has disconnected")] DisconnectedParent, @@ -24,7 +27,7 @@ pub enum AuthenticatorError { ShortPacket, #[error("failed to connect to mixnet: {source}")] - FailedToConnectToMixnet { source: nym_sdk::Error }, + FailedToConnectToMixnet { source: Box<nym_sdk::Error> }, #[error("failed to deserialize tagged packet: {source}")] FailedToDeserializeTaggedPacket { source: bincode::Error }, @@ -33,13 +36,13 @@ pub enum AuthenticatorError { FailedToLoadConfig(String), #[error("failed to send packet to mixnet: {source}")] - FailedToSendPacketToMixnet { source: nym_sdk::Error }, + FailedToSendPacketToMixnet { source: Box<nym_sdk::Error> }, #[error("failed to serialize response packet: {source}")] FailedToSerializeResponsePacket { source: Box<bincode::ErrorKind> }, #[error("failed to setup mixnet client: {source}")] - FailedToSetupMixnetClient { source: nym_sdk::Error }, + FailedToSetupMixnetClient { source: Box<nym_sdk::Error> }, #[error("{0}")] GatewayStorageError(#[from] nym_gateway_storage::error::GatewayStorageError), @@ -77,15 +80,6 @@ pub enum AuthenticatorError { #[error("peers can't be interacted with anymore")] PeerInteractionStopped, - #[error("operation is not supported")] - UnsupportedOperation, - - #[error("operation unavailable for older client")] - OldClient, - - #[error("storage should have the requested bandwidht entry")] - MissingClientBandwidthEntry, - #[error("unknown version number")] UnknownVersion, @@ -103,6 +97,7 @@ pub enum AuthenticatorError { #[error("{0}")] RecipientFormatting(#[from] nym_sdk::mixnet::RecipientFormattingError), -} -pub type Result<T> = std::result::Result<T, AuthenticatorError>; + #[error("no credential received")] + NoCredentialReceived, +} diff --git a/service-providers/authenticator/src/mixnet_client.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs similarity index 77% rename from service-providers/authenticator/src/mixnet_client.rs rename to gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs index 2222ffa93c..65d9b58e7a 100644 --- a/service-providers/authenticator/src/mixnet_client.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs @@ -5,7 +5,9 @@ use nym_client_core::{config::disk_persistence::CommonClientPaths, TopologyProvi use nym_sdk::{GatewayTransceiver, NymNetworkDetails}; use nym_task::TaskClient; -use crate::{config::BaseClientConfig, error::AuthenticatorError}; +use crate::node::internal_service_providers::authenticator::{ + config::BaseClientConfig, error::AuthenticatorError, +}; // Helper function to create the mixnet client. // This is NOT in the SDK since we don't want to expose any of the client-core config types. @@ -26,7 +28,9 @@ pub async fn create_mixnet_client( let mut client_builder = nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths) .await - .map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { source: err })? + .map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { + source: Box::new(err), + })? .network_details(NymNetworkDetails::new_from_env()) .debug_config(debug_config) .custom_shutdown(shutdown) @@ -41,12 +45,16 @@ pub async fn create_mixnet_client( client_builder = client_builder.custom_topology_provider(topology_provider); } - let mixnet_client = client_builder - .build() - .map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { source: err })?; + let mixnet_client = + client_builder + .build() + .map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { + source: Box::new(err), + })?; - mixnet_client - .connect_to_mixnet() - .await - .map_err(|err| AuthenticatorError::FailedToConnectToMixnet { source: err }) + mixnet_client.connect_to_mixnet().await.map_err(|err| { + AuthenticatorError::FailedToConnectToMixnet { + source: Box::new(err), + } + }) } diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs similarity index 83% rename from service-providers/authenticator/src/mixnet_listener.rs rename to gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs index d88a1185c5..c389055696 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -7,8 +7,10 @@ use std::{ time::{Duration, SystemTime}, }; -use crate::{config::Config, error::*}; -use crate::{error::AuthenticatorError, peer_manager::PeerManager}; +use crate::node::internal_service_providers::authenticator::{ + config::Config, error::AuthenticatorError, peer_manager::PeerManager, + seen_credential_cache::SeenCredentialCache, +}; use defguard_wireguard_rs::net::IpAddrMask; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::StreamExt; @@ -23,13 +25,15 @@ use nym_authenticator_requests::{ }, v1, v2, v3, v4, v5, CURRENT_VERSION, }; +use nym_credential_verification::ecash::traits::EcashManager; use nym_credential_verification::{ - bandwidth_storage_manager::BandwidthStorageManager, ecash::EcashManager, - BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, + bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, + ClientBandwidth, CredentialVerifier, }; -use nym_credentials_interface::CredentialSpendingData; +use nym_credentials_interface::{CredentialSpendingData, TicketType}; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; +use nym_gateway_storage::models::PersistedBandwidth; use nym_sdk::mixnet::{ AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane, }; @@ -42,7 +46,7 @@ use rand::{prelude::IteratorRandom, thread_rng}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; -type AuthenticatorHandleResult = Result<(Vec<u8>, Option<Recipient>)>; +type AuthenticatorHandleResult = Result<(Vec<u8>, Option<Recipient>), AuthenticatorError>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute pub(crate) struct RegistredAndFree { @@ -74,9 +78,11 @@ pub(crate) struct MixnetListener { pub(crate) peer_manager: PeerManager, - pub(crate) ecash_verifier: Option<Arc<EcashManager>>, + pub(crate) ecash_verifier: Arc<dyn EcashManager + Send + Sync>, pub(crate) timeout_check_interval: IntervalStream, + + pub(crate) seen_credential_cache: SeenCredentialCache, } impl MixnetListener { @@ -86,7 +92,7 @@ impl MixnetListener { wireguard_gateway_data: WireguardGatewayData, mixnet_client: nym_sdk::mixnet::MixnetClient, task_handle: TaskHandle, - ecash_verifier: Option<Arc<EcashManager>>, + ecash_verifier: Arc<dyn EcashManager + Send + Sync>, ) -> Self { let timeout_check_interval = IntervalStream::new(tokio::time::interval(DEFAULT_REGISTRATION_TIMEOUT_CHECK)); @@ -98,6 +104,7 @@ impl MixnetListener { peer_manager: PeerManager::new(wireguard_gateway_data), ecash_verifier, timeout_check_interval, + seen_credential_cache: SeenCredentialCache::new(), } } @@ -105,7 +112,7 @@ impl MixnetListener { self.peer_manager.wireguard_gateway_data.keypair() } - async fn remove_stale_registrations(&self) -> Result<()> { + async fn remove_stale_registrations(&self) -> Result<(), AuthenticatorError> { let mut registred_and_free = self.registred_and_free.write().await; let registred_values: Vec<_> = registred_and_free .registration_in_progres @@ -125,7 +132,7 @@ impl MixnetListener { registred_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); - log::debug!( + tracing::debug!( "Removed stale registration of {}", reg.gateway_data.pub_key() ); @@ -141,7 +148,7 @@ impl MixnetListener { registred_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); - log::debug!( + tracing::debug!( "Removed stale registration of {}", reg.gateway_data.pub_key() ); @@ -284,7 +291,7 @@ impl MixnetListener { v1::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -297,7 +304,7 @@ impl MixnetListener { v2::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -310,7 +317,7 @@ impl MixnetListener { v3::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -323,7 +330,7 @@ impl MixnetListener { v4::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ips: (allowed_ipv4, allowed_ipv6).into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -336,7 +343,7 @@ impl MixnetListener { v5::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ips: (allowed_ipv4, allowed_ipv6).into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, request_id, ) @@ -367,7 +374,7 @@ impl MixnetListener { let registration_data = RegistrationData { nonce, gateway_data: gateway_data.clone(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }; registred_and_free .registration_in_progres @@ -497,38 +504,37 @@ impl MixnetListener { 128, )); - // If gateway does ecash verification and client sends a credential, we do the additional - // credential verification. Later this will become mandatory. - if let (Some(ecash_verifier), Some(credential)) = - (self.ecash_verifier.clone(), final_message.credential()) + let Some(credential) = final_message.credential() else { + return Err(AuthenticatorError::NoCredentialReceived); + }; + let client_id = self + .ecash_verifier + .storage() + .insert_wireguard_peer( + &peer, + TicketType::try_from_encoded(credential.payment.t_type) + .map_err(|_| AuthenticatorError::InvalidCredentialType)? + .into(), + ) + .await?; + if let Err(e) = + credential_verification(self.ecash_verifier.clone(), credential, client_id).await { - let client_id = ecash_verifier + self.ecash_verifier .storage() - .insert_wireguard_peer(&peer, true) - .await? - .ok_or(AuthenticatorError::InternalError( - "peer with ticket shouldn't have been used before without a ticket".to_string(), - ))?; - if let Err(e) = - Self::credential_verification(ecash_verifier.clone(), credential, client_id).await - { - ecash_verifier - .storage() - .remove_wireguard_peer(&peer.public_key.to_string()) - .await?; - return Err(e); - } - let public_key = peer.public_key.to_string(); - if let Err(e) = self.peer_manager.add_peer(peer, Some(client_id)).await { - ecash_verifier - .storage() - .remove_wireguard_peer(&public_key) - .await?; - return Err(e); - } - } else { - self.peer_manager.add_peer(peer, None).await?; + .remove_wireguard_peer(&peer.public_key.to_string()) + .await?; + return Err(e); } + let public_key = peer.public_key.to_string(); + if let Err(e) = self.peer_manager.add_peer(peer).await { + self.ecash_verifier + .storage() + .remove_wireguard_peer(&public_key) + .await?; + return Err(e); + } + registred_and_free .registration_in_progres .remove(&final_message.pub_key()); @@ -593,37 +599,6 @@ impl MixnetListener { Ok((bytes, reply_to)) } - async fn credential_verification( - ecash_verifier: Arc<EcashManager>, - credential: CredentialSpendingData, - client_id: i64, - ) -> Result<i64> { - ecash_verifier - .storage() - .create_bandwidth_entry(client_id) - .await?; - let bandwidth = ecash_verifier - .storage() - .get_available_bandwidth(client_id) - .await? - .ok_or(AuthenticatorError::InternalError( - "bandwidth entry should have just been created".to_string(), - ))?; - let client_bandwidth = ClientBandwidth::new(bandwidth.into()); - let mut verifier = CredentialVerifier::new( - CredentialSpendingRequest::new(credential), - ecash_verifier.clone(), - BandwidthStorageManager::new( - ecash_verifier.storage().clone(), - client_bandwidth, - client_id, - BandwidthFlushingBehaviourConfig::default(), - true, - ), - ); - Ok(verifier.verify().await?) - } - async fn on_query_bandwidth_request( &mut self, msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>, @@ -631,12 +606,12 @@ impl MixnetListener { request_id: u64, reply_to: Option<Recipient>, ) -> AuthenticatorHandleResult { - let bandwidth_data = self.peer_manager.query_bandwidth(msg).await?; + let available_bandwidth = self.peer_manager.query_bandwidth(msg.pub_key()).await?; let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => { v1::response::AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data.map(|data| v1::registration::RemainingBandwidthData { - available_bandwidth: data.available_bandwidth as u64, + Some(v1::registration::RemainingBandwidthData { + available_bandwidth: available_bandwidth as u64, suspended: false, }), reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, @@ -649,8 +624,8 @@ impl MixnetListener { } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data.map(|data| v2::registration::RemainingBandwidthData { - available_bandwidth: data.available_bandwidth, + Some(v2::registration::RemainingBandwidthData { + available_bandwidth, }), reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -662,8 +637,8 @@ impl MixnetListener { } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data.map(|data| v3::registration::RemainingBandwidthData { - available_bandwidth: data.available_bandwidth, + Some(v3::registration::RemainingBandwidthData { + available_bandwidth, }), reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -675,8 +650,8 @@ impl MixnetListener { } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data.map(|data| v4::registration::RemainingBandwidthData { - available_bandwidth: data.available_bandwidth, + Some(v4::registration::RemainingBandwidthData { + available_bandwidth, }), reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -688,8 +663,8 @@ impl MixnetListener { } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data.map(|data| v5::registration::RemainingBandwidthData { - available_bandwidth: data.available_bandwidth, + Some(v5::registration::RemainingBandwidthData { + available_bandwidth, }), request_id, ) @@ -710,37 +685,19 @@ impl MixnetListener { request_id: u64, reply_to: Option<Recipient>, ) -> AuthenticatorHandleResult { - let Some(ecash_verifier) = self.ecash_verifier.clone() else { - return Err(AuthenticatorError::UnsupportedOperation); + let available_bandwidth = if self.received_retry(msg.as_ref()) { + // don't process the credential and just return the current bandwidth + self.peer_manager.query_bandwidth(msg.pub_key()).await? + } else { + let mut verifier = self + .peer_manager + .query_verifier_by_key(msg.pub_key(), msg.credential()) + .await?; + let available_bandwidth = verifier.verify().await?; + self.seen_credential_cache + .insert_credential(msg.credential(), msg.pub_key()); + available_bandwidth }; - let client_id = ecash_verifier - .storage() - .get_wireguard_peer(&msg.pub_key().to_string()) - .await? - .ok_or(AuthenticatorError::MissingClientBandwidthEntry)? - .client_id - .ok_or(AuthenticatorError::OldClient)?; - let bandwidth = ecash_verifier - .storage() - .get_available_bandwidth(client_id) - .await? - .ok_or(AuthenticatorError::InternalError( - "bandwidth entry should have just been created".to_string(), - ))?; - - let client_bandwidth = ClientBandwidth::new(bandwidth.into()); - let mut verifier = CredentialVerifier::new( - CredentialSpendingRequest::new(msg.credential()), - ecash_verifier.clone(), - BandwidthStorageManager::new( - ecash_verifier.storage().clone(), - client_bandwidth, - client_id, - BandwidthFlushingBehaviourConfig::default(), - true, - ), - ); - let available_bandwidth = verifier.verify().await?; let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( @@ -777,11 +734,23 @@ impl MixnetListener { Ok((bytes, reply_to)) } + fn received_retry(&self, msg: &(dyn TopUpMessage + Send + Sync + 'static)) -> bool { + if let Some(peer_pub_key) = self + .seen_credential_cache + .get_peer_pub_key(&msg.credential()) + { + // check if the same peer sent the same credential twice, probably because of a retry + peer_pub_key == msg.pub_key() + } else { + false + } + } + async fn on_reconstructed_message( &mut self, reconstructed: ReconstructedMessage, ) -> AuthenticatorHandleResult { - log::debug!( + tracing::debug!( "Received message with sender_tag: {:?}", reconstructed.sender_tag ); @@ -834,27 +803,29 @@ impl MixnetListener { response: Vec<u8>, recipient: Option<Recipient>, sender_tag: Option<AnonymousSenderTag>, - ) -> Result<()> { + ) -> Result<(), AuthenticatorError> { let input_message = create_input_message(recipient, sender_tag, response)?; - self.mixnet_client - .send(input_message) - .await - .map_err(|err| AuthenticatorError::FailedToSendPacketToMixnet { source: err }) + self.mixnet_client.send(input_message).await.map_err(|err| { + AuthenticatorError::FailedToSendPacketToMixnet { + source: Box::new(err), + } + }) } - pub(crate) async fn run(mut self) -> Result<()> { - log::info!("Using authenticator version {}", CURRENT_VERSION); + pub(crate) async fn run(mut self) -> Result<(), AuthenticatorError> { + tracing::info!("Using authenticator version {CURRENT_VERSION}"); let mut task_client = self.task_handle.fork("main_loop"); while !task_client.is_shutdown() { tokio::select! { _ = task_client.recv() => { - log::debug!("Authenticator [main loop]: received shutdown"); + tracing::debug!("Authenticator [main loop]: received shutdown"); }, _ = self.timeout_check_interval.next() => { if let Err(e) = self.remove_stale_registrations().await { - log::error!("Could not clear stale registrations. The registration process might get jammed soon - {:?}", e); + tracing::error!("Could not clear stale registrations. The registration process might get jammed soon - {e:?}"); } + self.seen_credential_cache.remove_stale(); } msg = self.mixnet_client.next() => { if let Some(msg) = msg { @@ -862,28 +833,69 @@ impl MixnetListener { match self.on_reconstructed_message(msg).await { Ok((response, recipient)) => { if let Err(err) = self.handle_response(response, recipient, sender_tag).await { - log::error!("Mixnet listener failed to handle response: {err}"); + tracing::error!("Mixnet listener failed to handle response: {err}"); } } Err(err) => { - log::error!("Error handling reconstructed mixnet message: {err}"); + tracing::error!("Error handling reconstructed mixnet message: {err}"); } }; } else { - log::trace!("Authenticator [main loop]: stopping since channel closed"); + tracing::trace!("Authenticator [main loop]: stopping since channel closed"); break; }; }, } } - log::debug!("Authenticator: stopping"); + tracing::debug!("Authenticator: stopping"); Ok(()) } } -fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result<AuthenticatorRequest> { +pub async fn credential_storage_preparation( + ecash_verifier: Arc<dyn EcashManager + Send + Sync>, + client_id: i64, +) -> Result<PersistedBandwidth, AuthenticatorError> { + ecash_verifier + .storage() + .create_bandwidth_entry(client_id) + .await?; + let bandwidth = ecash_verifier + .storage() + .get_available_bandwidth(client_id) + .await? + .ok_or(AuthenticatorError::InternalError( + "bandwidth entry should have just been created".to_string(), + ))?; + Ok(bandwidth) +} + +async fn credential_verification( + ecash_verifier: Arc<dyn EcashManager + Send + Sync>, + credential: CredentialSpendingData, + client_id: i64, +) -> Result<i64, AuthenticatorError> { + let bandwidth = credential_storage_preparation(ecash_verifier.clone(), client_id).await?; + let client_bandwidth = ClientBandwidth::new(bandwidth.into()); + let mut verifier = CredentialVerifier::new( + CredentialSpendingRequest::new(credential), + ecash_verifier.clone(), + BandwidthStorageManager::new( + ecash_verifier.storage(), + client_bandwidth, + client_id, + BandwidthFlushingBehaviourConfig::default(), + true, + ), + ); + Ok(verifier.verify().await?) +} + +fn deserialize_request( + reconstructed: &ReconstructedMessage, +) -> Result<AuthenticatorRequest, AuthenticatorError> { let request_version = *reconstructed .message .first_chunk::<2>() @@ -940,7 +952,7 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result<Authentic } } [version, _] => { - log::info!("Received packet with invalid version: v{version}"); + tracing::info!("Received packet with invalid version: v{version}"); Err(AuthenticatorError::InvalidPacketVersion(version)) } } @@ -950,11 +962,11 @@ fn create_input_message( nym_address: Option<Recipient>, reply_to_tag: Option<AnonymousSenderTag>, response_packet: Vec<u8>, -) -> Result<InputMessage> { +) -> Result<InputMessage, AuthenticatorError> { let lane = TransmissionLane::General; let packet_type = None; if let Some(reply_to_tag) = reply_to_tag { - log::debug!("Creating message using SURB"); + tracing::debug!("Creating message using SURB"); Ok(InputMessage::new_reply( reply_to_tag, response_packet, @@ -962,7 +974,7 @@ fn create_input_message( packet_type, )) } else if let Some(nym_address) = nym_address { - log::debug!("Creating message using nym_address"); + tracing::debug!("Creating message using nym_address"); Ok(InputMessage::new_regular( nym_address, response_packet, @@ -970,7 +982,7 @@ fn create_input_message( packet_type, )) } else { - log::error!("No nym-address or sender tag provided"); + tracing::error!("No nym-address or sender tag provided"); Err(AuthenticatorError::MissingReplyToForOldClient) } } diff --git a/service-providers/authenticator/src/authenticator.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs similarity index 83% rename from service-providers/authenticator/src/authenticator.rs rename to gateway/src/node/internal_service_providers/authenticator/mod.rs index 268d96c1ef..12772b6a30 100644 --- a/service-providers/authenticator/src/authenticator.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -1,8 +1,7 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime}; - +use crate::node::internal_service_providers::authenticator::error::AuthenticatorError; use futures::channel::oneshot; use ipnetwork::IpNetwork; use nym_client_core::{HardcodedTopologyProvider, TopologyProvider}; @@ -10,8 +9,16 @@ use nym_credential_verification::ecash::EcashManager; use nym_sdk::{mixnet::Recipient, GatewayTransceiver}; use nym_task::{TaskClient, TaskHandle}; use nym_wireguard::WireguardGatewayData; +use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime}; -use crate::{config::Config, error::AuthenticatorError}; +pub mod config; +pub mod error; +pub mod mixnet_client; +pub mod mixnet_listener; +mod peer_manager; +mod seen_credential_cache; + +pub use config::Config; pub struct OnStartData { // to add more fields as required @@ -26,12 +33,12 @@ impl OnStartData { pub struct Authenticator { #[allow(unused)] - config: Config, + config: crate::node::internal_service_providers::authenticator::Config, wait_for_gateway: bool, custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>, custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>, wireguard_gateway_data: WireguardGatewayData, - ecash_verifier: Option<Arc<EcashManager>>, + ecash_verifier: Arc<EcashManager>, used_private_network_ips: Vec<IpAddr>, shutdown: Option<TaskClient>, on_start: Option<oneshot::Sender<OnStartData>>, @@ -39,16 +46,17 @@ pub struct Authenticator { impl Authenticator { pub fn new( - config: Config, + config: crate::node::internal_service_providers::authenticator::Config, wireguard_gateway_data: WireguardGatewayData, used_private_network_ips: Vec<IpAddr>, + ecash_verifier: Arc<EcashManager>, ) -> Self { Self { config, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, - ecash_verifier: None, + ecash_verifier, wireguard_gateway_data, used_private_network_ips, shutdown: None, @@ -56,13 +64,6 @@ impl Authenticator { } } - #[must_use] - #[allow(unused)] - pub fn with_ecash_verifier(mut self, ecash_verifier: Arc<EcashManager>) -> Self { - self.ecash_verifier = Some(ecash_verifier); - self - } - #[must_use] #[allow(unused)] pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { @@ -125,7 +126,7 @@ impl Authenticator { let task_handle: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); // Connect to the mixnet - let mixnet_client = crate::mixnet_client::create_mixnet_client( + let mixnet_client = crate::node::internal_service_providers::authenticator::mixnet_client::create_mixnet_client( &self.config.base, task_handle .get_handle() @@ -135,7 +136,7 @@ impl Authenticator { self.wait_for_gateway, &self.config.storage_paths.common_paths, ) - .await?; + .await?; let self_address = *mixnet_client.nym_address(); @@ -156,7 +157,7 @@ impl Authenticator { } }) .collect(); - let mixnet_listener = crate::mixnet_listener::MixnetListener::new( + let mixnet_listener = crate::node::internal_service_providers::authenticator::mixnet_listener::MixnetListener::new( self.config, free_private_network_ips, self.wireguard_gateway_data, @@ -165,8 +166,8 @@ impl Authenticator { self.ecash_verifier, ); - log::info!("The address of this client is: {self_address}"); - log::info!("All systems go. Press CTRL-C to stop the server."); + tracing::info!("The address of this client is: {self_address}"); + tracing::info!("All systems go. Press CTRL-C to stop the server."); if let Some(on_start) = self.on_start { if on_start.send(OnStartData::new(self_address)).is_err() { diff --git a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs new file mode 100644 index 0000000000..fce8cf7174 --- /dev/null +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -0,0 +1,469 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::internal_service_providers::authenticator::error::AuthenticatorError; +use defguard_wireguard_rs::{host::Peer, key::Key}; +use futures::channel::oneshot; +use nym_credential_verification::{ClientBandwidth, TicketVerifier}; +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; +use nym_wireguard_types::PeerPublicKey; + +pub struct PeerManager { + pub(crate) wireguard_gateway_data: WireguardGatewayData, +} + +impl PeerManager { + pub fn new(wireguard_gateway_data: WireguardGatewayData) -> Self { + PeerManager { + wireguard_gateway_data, + } + } + pub async fn add_peer(&mut self, peer: Peer) -> Result<(), AuthenticatorError> { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::AddPeer { peer, response_tx }; + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .await + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| AuthenticatorError::InternalError("no response for add peer".to_string()))? + .map_err(|err| { + AuthenticatorError::InternalError(format!( + "adding peer could not be performed: {err:?}" + )) + }) + } + + pub async fn _remove_peer(&mut self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { + let key = Key::new(pub_key.to_bytes()); + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::RemovePeer { key, response_tx }; + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .await + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| { + AuthenticatorError::InternalError("no response for remove peer".to_string()) + })? + .map_err(|err| { + AuthenticatorError::InternalError(format!( + "removing peer could not be performed: {err:?}" + )) + }) + } + + pub async fn query_peer( + &mut self, + public_key: PeerPublicKey, + ) -> Result<Option<Peer>, AuthenticatorError> { + let key = Key::new(public_key.to_bytes()); + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::QueryPeer { key, response_tx }; + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .await + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| { + AuthenticatorError::InternalError("no response for query peer".to_string()) + })? + .map_err(|err| { + AuthenticatorError::InternalError(format!( + "querying peer could not be performed: {err:?}" + )) + }) + } + + pub async fn query_bandwidth( + &mut self, + public_key: PeerPublicKey, + ) -> Result<i64, AuthenticatorError> { + let client_bandwidth = self.query_client_bandwidth(public_key).await?; + Ok(client_bandwidth.available().await) + } + + pub async fn query_client_bandwidth( + &mut self, + key: PeerPublicKey, + ) -> Result<ClientBandwidth, AuthenticatorError> { + let key = Key::new(key.to_bytes()); + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetClientBandwidthByKey { key, response_tx }; + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .await + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| { + AuthenticatorError::InternalError( + "no response for query client bandwidth".to_string(), + ) + })? + .map_err(|err| { + AuthenticatorError::InternalError(format!( + "querying client bandwidth could not be performed: {err:?}" + )) + }) + } + + pub async fn query_verifier_by_key( + &mut self, + key: PeerPublicKey, + credential: CredentialSpendingData, + ) -> Result<Box<dyn TicketVerifier + Send + Sync>, AuthenticatorError> { + let key = Key::new(key.to_bytes()); + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetVerifierByKey { + key, + credential: Box::new(credential), + response_tx, + }; + self.wireguard_gateway_data + .peer_tx() + .send(msg) + .await + .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| { + AuthenticatorError::InternalError("no response for query verifier".to_string()) + })? + .map_err(|err| { + AuthenticatorError::InternalError(format!( + "querying verifier could not be performed: {err:?}" + )) + }) + } +} + +#[cfg(test)] +mod tests { + use std::{str::FromStr, sync::Arc}; + + use nym_credential_verification::{ + bandwidth_storage_manager::BandwidthStorageManager, ecash::MockEcashManager, + }; + use nym_credentials_interface::Bandwidth; + use nym_crypto::asymmetric::x25519::KeyPair; + use nym_gateway_storage::traits::{mock::MockGatewayStorage, BandwidthGatewayStorage}; + use nym_wireguard::peer_controller::{start_controller, stop_controller}; + use rand::rngs::OsRng; + use time::{Duration, OffsetDateTime}; + use tokio::sync::RwLock; + + use crate::nym_authenticator::{ + config::Authenticator, mixnet_listener::credential_storage_preparation, + }; + + use super::*; + + const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + #[tokio::test] + async fn add_peer() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + let peer = Peer::default(); + let ecash_manager = MockEcashManager::new(Box::new(storage.clone())); + + assert!(peer_manager.add_peer(peer.clone()).await.is_err()); + + let client_id = storage + .insert_wireguard_peer(&peer, FromStr::from_str("entry_wireguard").unwrap()) + .await + .unwrap(); + assert!(peer_manager.add_peer(peer.clone()).await.is_err()); + + credential_storage_preparation(Arc::new(ecash_manager), client_id) + .await + .unwrap(); + peer_manager.add_peer(peer.clone()).await.unwrap(); + + stop_controller(task_manager).await; + } + + async fn helper_add_peer( + storage: &Arc<RwLock<MockGatewayStorage>>, + peer_manager: &mut PeerManager, + ) -> i64 { + let peer = Peer::default(); + let ecash_manager = MockEcashManager::new(Box::new(storage.clone())); + let client_id = storage + .insert_wireguard_peer(&peer, FromStr::from_str("entry_wireguard").unwrap()) + .await + .unwrap(); + credential_storage_preparation(Arc::new(ecash_manager), client_id) + .await + .unwrap(); + peer_manager.add_peer(peer.clone()).await.unwrap(); + + client_id + } + + #[tokio::test] + async fn remove_peer() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let key = Key::default(); + let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + + helper_add_peer(&storage, &mut peer_manager).await; + peer_manager._remove_peer(public_key).await.unwrap(); + + stop_controller(task_manager).await; + } + + #[tokio::test] + async fn query_peer() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let key = Key::default(); + let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + + assert!(peer_manager.query_peer(public_key).await.unwrap().is_none()); + + helper_add_peer(&storage, &mut peer_manager).await; + let peer = peer_manager.query_peer(public_key).await.unwrap().unwrap(); + assert_eq!(peer.public_key, key); + + stop_controller(task_manager).await; + } + + #[tokio::test] + async fn query_bandwidth() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let key = Key::default(); + let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + + assert!(peer_manager.query_bandwidth(public_key).await.is_err()); + + helper_add_peer(&storage, &mut peer_manager).await; + let available_bandwidth = peer_manager.query_bandwidth(public_key).await.unwrap(); + assert_eq!(available_bandwidth, 0); + + stop_controller(task_manager).await; + } + + #[tokio::test] + async fn query_client_bandwidth() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let key = Key::default(); + let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + + assert!(peer_manager + .query_client_bandwidth(public_key) + .await + .is_err()); + + helper_add_peer(&storage, &mut peer_manager).await; + let available_bandwidth = peer_manager + .query_client_bandwidth(public_key) + .await + .unwrap() + .available() + .await; + assert_eq!(available_bandwidth, 0); + + stop_controller(task_manager).await; + } + + #[tokio::test] + async fn query_verifier() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let key = Key::default(); + let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + + assert!(peer_manager + .query_verifier_by_key(public_key, credential.clone()) + .await + .is_err()); + + helper_add_peer(&storage, &mut peer_manager).await; + peer_manager + .query_verifier_by_key(public_key, credential) + .await + .unwrap(); + + stop_controller(task_manager).await; + } + + #[tokio::test] + async fn increase_decrease_bandwidth() { + let (wireguard_data, request_rx) = WireguardGatewayData::new( + Authenticator::default().into(), + Arc::new(KeyPair::new(&mut OsRng)), + ); + let mut peer_manager = PeerManager::new(wireguard_data); + let key = Key::default(); + let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap(); + let top_up = 42; + let consume = 4; + let (storage, task_manager) = start_controller( + peer_manager.wireguard_gateway_data.peer_tx().clone(), + request_rx, + ); + + let client_id = helper_add_peer(&storage, &mut peer_manager).await; + let client_bandwidth = peer_manager + .query_client_bandwidth(public_key) + .await + .unwrap(); + + let mut bw_manager = BandwidthStorageManager::new( + Box::new(storage), + client_bandwidth.clone(), + client_id, + Default::default(), + true, + ); + bw_manager + .increase_bandwidth( + Bandwidth::new_unchecked(top_up as u64), + OffsetDateTime::now_utc() + .checked_add(Duration::minutes(1)) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(client_bandwidth.available().await, top_up); + assert_eq!( + peer_manager.query_bandwidth(public_key).await.unwrap(), + top_up + ); + + bw_manager.try_use_bandwidth(consume).await.unwrap(); + let remaining = top_up - consume; + assert_eq!(client_bandwidth.available().await, remaining); + assert_eq!( + peer_manager.query_bandwidth(public_key).await.unwrap(), + remaining + ); + + stop_controller(task_manager).await; + } +} diff --git a/gateway/src/node/internal_service_providers/authenticator/seen_credential_cache.rs b/gateway/src/node/internal_service_providers/authenticator/seen_credential_cache.rs new file mode 100644 index 0000000000..0c2253f7c5 --- /dev/null +++ b/gateway/src/node/internal_service_providers/authenticator/seen_credential_cache.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +use mock_instant::thread_local::SystemTime; +#[cfg(not(test))] +use std::time::SystemTime; +use std::{collections::HashMap, time::Duration}; + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; + +const SEEN_CREDENTIAL_CACHE_TIME: Duration = Duration::from_secs(60 * 60); // 1 hour + +#[derive(Eq, Hash, PartialEq)] +struct TimestampedPeerPubKey { + peer_pub_key: PeerPublicKey, + timestamp: SystemTime, +} + +pub(crate) struct SeenCredentialCache { + cached_credentials: HashMap<String, TimestampedPeerPubKey>, +} + +impl SeenCredentialCache { + pub(crate) fn new() -> Self { + SeenCredentialCache { + cached_credentials: HashMap::new(), + } + } + + pub(crate) fn insert_credential( + &mut self, + credential: CredentialSpendingData, + peer_pub_key: PeerPublicKey, + ) { + let value = TimestampedPeerPubKey { + peer_pub_key, + timestamp: SystemTime::now(), + }; + self.cached_credentials + .insert(credential.serial_number_b58(), value); + } + + pub(crate) fn get_peer_pub_key( + &self, + credential: &CredentialSpendingData, + ) -> Option<PeerPublicKey> { + self.cached_credentials + .get(&credential.serial_number_b58()) + .map(|value| value.peer_pub_key) + } + + pub(crate) fn remove_stale(&mut self) { + let now = SystemTime::now(); + self.cached_credentials.retain(|_, value| { + let Ok(cache_time) = now.duration_since(value.timestamp) else { + tracing::warn!("Got decreasing consecutive system timestamps"); + return false; + }; + cache_time < SEEN_CREDENTIAL_CACHE_TIME + }); + } +} + +#[cfg(test)] +mod test { + use mock_instant::thread_local::MockClock; + use std::str::FromStr; + + use super::*; + + const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + const PUB_KEY: &str = "yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ="; + + #[test] + fn insertion() { + let mut cache = SeenCredentialCache::new(); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap(); + + assert!(cache.get_peer_pub_key(&credential).is_none()); + + cache.insert_credential(credential.clone(), peer_pub_key); + let cached_peer_pub_key = cache.get_peer_pub_key(&credential).unwrap(); + assert_eq!(cached_peer_pub_key, peer_pub_key); + } + + #[test] + fn staleness() { + let mut cache = SeenCredentialCache::new(); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap(); + + cache.insert_credential(credential.clone(), peer_pub_key); + cache.remove_stale(); + assert!(cache.get_peer_pub_key(&credential).is_some()); + + MockClock::advance_system_time(SEEN_CREDENTIAL_CACHE_TIME * 2); + + cache.remove_stale(); + assert!(cache.get_peer_pub_key(&credential).is_none()); + } + + #[test] + fn invalid_time() { + assert!(MockClock::is_thread_local()); + assert!(SystemTime::now().is_thread_local()); + + let mut cache = SeenCredentialCache::new(); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap(); + + // set some value for time + MockClock::set_system_time(Duration::from_secs(10)); + cache.insert_credential(credential.clone(), peer_pub_key); + + // then set the time in the past + MockClock::set_system_time(Duration::ZERO); + cache.remove_stale(); + + // invalid time should remove the credential, just in case + assert!(cache.get_peer_pub_key(&credential).is_none()); + } +} diff --git a/gateway/src/node/internal_service_providers.rs b/gateway/src/node/internal_service_providers/mod.rs similarity index 94% rename from gateway/src/node/internal_service_providers.rs rename to gateway/src/node/internal_service_providers/mod.rs index b26d9f562f..cd4cd051b9 100644 --- a/gateway/src/node/internal_service_providers.rs +++ b/gateway/src/node/internal_service_providers/mod.rs @@ -5,10 +5,10 @@ use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, use crate::node::client_handling::websocket::message_receiver::{ MixMessageReceiver, MixMessageSender, }; +use crate::node::internal_service_providers::authenticator::Authenticator; use crate::GatewayError; use async_trait::async_trait; use futures::channel::{mpsc, oneshot}; -use nym_authenticator::Authenticator; use nym_crypto::asymmetric::ed25519; use nym_ip_packet_router::error::IpPacketRouterError; use nym_ip_packet_router::IpPacketRouter; @@ -22,6 +22,8 @@ use std::fmt::Display; use tokio::task::JoinHandle; use tracing::error; +pub mod authenticator; + pub trait LocalRecipient { fn address(&self) -> Recipient; } @@ -38,7 +40,7 @@ impl LocalRecipient for nym_ip_packet_router::OnStartData { } } -impl LocalRecipient for nym_authenticator::OnStartData { +impl LocalRecipient for authenticator::OnStartData { fn address(&self) -> Recipient { self.address } @@ -78,8 +80,8 @@ impl RunnableServiceProvider for IpPacketRouter { #[async_trait] impl RunnableServiceProvider for Authenticator { const NAME: &'static str = "authenticator"; - type OnStartData = nym_authenticator::OnStartData; - type Error = nym_authenticator::error::AuthenticatorError; + type OnStartData = authenticator::OnStartData; + type Error = authenticator::error::AuthenticatorError; async fn run_service_provider(self) -> Result<(), Self::Error> { self.run_service_provider().await @@ -128,6 +130,8 @@ where } }); + // TODO: if something is blocking during SP startup, the below will wait forever + // we need to introduce additional timeouts here. let on_start_data = self .on_start_rx .await diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index c13df9f241..a89a68cb23 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -5,21 +5,21 @@ use crate::config::Config; use crate::error::GatewayError; use crate::node::client_handling::websocket; use crate::node::internal_service_providers::{ - ExitServiceProviders, ServiceProviderBeingBuilt, SpMessageRouterBuilder, + authenticator, ExitServiceProviders, ServiceProviderBeingBuilt, SpMessageRouterBuilder, }; +use crate::node::stale_data_cleaner::StaleMessagesCleaner; use futures::channel::oneshot; -use nym_authenticator::Authenticator; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; use nym_crypto::asymmetric::ed25519; -use nym_gateway_storage::models::WireguardPeer; use nym_ip_packet_router::IpPacketRouter; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::NRServiceProviderBuilder; use nym_node_metrics::events::MetricEventsSender; -use nym_task::TaskClient; +use nym_node_metrics::NymNodeMetrics; +use nym_task::{ShutdownToken, TaskClient}; use nym_topology::TopologyProvider; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; @@ -32,14 +32,17 @@ use tracing::*; use zeroize::Zeroizing; pub(crate) mod client_handling; -mod internal_service_providers; +pub(crate) mod internal_service_providers; mod stale_data_cleaner; -use crate::node::stale_data_cleaner::StaleMessagesCleaner; +use crate::node::internal_service_providers::authenticator::Authenticator; pub use client_handling::active_clients::ActiveClientsStore; pub use nym_gateway_stats_storage::PersistentStatsStorage; -pub use nym_gateway_storage::{error::GatewayStorageError, GatewayStorage}; -use nym_node_metrics::NymNodeMetrics; +pub use nym_gateway_storage::{ + error::GatewayStorageError, + traits::{BandwidthGatewayStorage, InboxGatewayStorage}, + GatewayStorage, +}; pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; #[derive(Debug, Clone)] @@ -58,7 +61,7 @@ pub struct LocalIpPacketRouterOpts { #[derive(Debug, Clone)] pub struct LocalAuthenticatorOpts { - pub config: nym_authenticator::Config, + pub config: authenticator::Config, pub custom_mixnet_path: Option<PathBuf>, } @@ -88,12 +91,14 @@ pub struct GatewayTasksBuilder { mnemonic: Arc<Zeroizing<bip39::Mnemonic>>, - shutdown: TaskClient, + legacy_task_client: TaskClient, + + shutdown_token: ShutdownToken, // populated and cached as necessary ecash_manager: Option<Arc<EcashManager>>, - wireguard_peers: Option<Vec<WireguardPeer>>, + wireguard_peers: Option<Vec<defguard_wireguard_rs::host::Peer>>, wireguard_networks: Option<Vec<IpAddr>>, } @@ -102,7 +107,7 @@ impl Drop for GatewayTasksBuilder { fn drop(&mut self) { // disarm the shutdown as it was already used to construct relevant tasks and we don't want the builder // to cause shutdown - self.shutdown.disarm(); + self.legacy_task_client.disarm(); } } @@ -116,7 +121,8 @@ impl GatewayTasksBuilder { metrics_sender: MetricEventsSender, metrics: NymNodeMetrics, mnemonic: Arc<Zeroizing<bip39::Mnemonic>>, - shutdown: TaskClient, + legacy_task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> GatewayTasksBuilder { GatewayTasksBuilder { config, @@ -130,7 +136,8 @@ impl GatewayTasksBuilder { metrics_sender, metrics, mnemonic, - shutdown, + legacy_task_client, + shutdown_token, ecash_manager: None, wireguard_peers: None, wireguard_networks: None, @@ -225,7 +232,7 @@ impl GatewayTasksBuilder { handler_config, nyxd_client, self.identity_keypair.public_key().to_bytes(), - self.shutdown.fork("ecash_manager"), + self.legacy_task_client.fork("ecash_manager"), self.storage.clone(), ) .await?, @@ -267,7 +274,7 @@ impl GatewayTasksBuilder { self.config.gateway.websocket_bind_address, self.config.debug.maximum_open_connections, shared_state, - self.shutdown.fork("websocket"), + self.legacy_task_client.fork("websocket"), )) } @@ -283,13 +290,14 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("network_requester_message_router"), + self.legacy_task_client + .fork("network_requester_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); let (on_start_tx, on_start_rx) = oneshot::channel(); let mut nr_builder = NRServiceProviderBuilder::new(nr_opts.config.clone()) - .with_shutdown(self.shutdown.fork("network_requester_sp")) + .with_shutdown(self.legacy_task_client.fork("network_requester_sp")) .with_custom_gateway_transceiver(transceiver) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) @@ -318,13 +326,13 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("ipr_message_router"), + self.legacy_task_client.fork("ipr_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); let (on_start_tx, on_start_rx) = oneshot::channel(); let mut ip_packet_router = IpPacketRouter::new(ip_opts.config.clone()) - .with_shutdown(self.shutdown.fork("ipr_sp")) + .with_shutdown(self.legacy_task_client.fork("ipr_sp")) .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) @@ -357,12 +365,12 @@ impl GatewayTasksBuilder { async fn build_wireguard_peers_and_networks( &self, - ) -> Result<(Vec<WireguardPeer>, Vec<IpAddr>), GatewayError> { + ) -> Result<(Vec<defguard_wireguard_rs::host::Peer>, Vec<IpAddr>), GatewayError> { let mut used_private_network_ips = vec![]; let mut all_peers = vec![]; for wireguard_peer in self.storage.get_all_wireguard_peers().await?.into_iter() { let mut peer = defguard_wireguard_rs::host::Peer::try_from(wireguard_peer.clone())?; - let Some(peer) = peer.allowed_ips.pop() else { + let Some(allowed_ip) = peer.allowed_ips.pop() else { let peer_identity = &peer.public_key; warn!("Peer {peer_identity} has empty allowed ips. It will be removed",); self.storage @@ -370,8 +378,8 @@ impl GatewayTasksBuilder { .await?; continue; }; - used_private_network_ips.push(peer.ip); - all_peers.push(wireguard_peer); + used_private_network_ips.push(allowed_ip.ip); + all_peers.push(peer); } Ok((all_peers, used_private_network_ips)) @@ -379,7 +387,9 @@ impl GatewayTasksBuilder { // only used under linux #[allow(dead_code)] - async fn get_wireguard_peers(&mut self) -> Result<Vec<WireguardPeer>, GatewayError> { + async fn get_wireguard_peers( + &mut self, + ) -> Result<Vec<defguard_wireguard_rs::host::Peer>, GatewayError> { if let Some(cached) = self.wireguard_peers.take() { return Ok(cached); } @@ -422,7 +432,7 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("authenticator_message_router"), + self.legacy_task_client.fork("authenticator_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); @@ -432,10 +442,10 @@ impl GatewayTasksBuilder { opts.config.clone(), wireguard_data.inner.clone(), used_private_network_ips, + ecash_manager, ) - .with_ecash_verifier(ecash_manager) .with_custom_gateway_transceiver(transceiver) - .with_shutdown(self.shutdown.fork("authenticator_sp")) + .with_shutdown(self.legacy_task_client.fork("authenticator_sp")) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) .with_custom_topology_provider(topology_provider) @@ -455,7 +465,7 @@ impl GatewayTasksBuilder { pub fn build_stale_messages_cleaner(&self) -> StaleMessagesCleaner { StaleMessagesCleaner::new( &self.storage, - self.shutdown.fork("stale_messages_cleaner"), + self.legacy_task_client.fork("stale_messages_cleaner"), self.config.debug.stale_messages_max_age, self.config.debug.stale_messages_cleaner_run_interval, ) @@ -466,13 +476,17 @@ impl GatewayTasksBuilder { &mut self, ) -> Result<Arc<nym_wireguard::WgApiWrapper>, Box<dyn std::error::Error + Send + Sync>> { let _ = self.metrics.clone(); + let _ = self.shutdown_token.clone(); unimplemented!("wireguard is not supported on this platform") } #[cfg(target_os = "linux")] pub async fn try_start_wireguard( &mut self, - ) -> Result<Arc<nym_wireguard::WgApiWrapper>, Box<dyn std::error::Error + Send + Sync>> { + ) -> Result< + nym_wireguard_private_metadata_server::ShutdownHandles, + Box<dyn std::error::Error + Send + Sync>, + > { let all_peers = self.get_wireguard_peers().await?; let Some(wireguard_data) = self.wireguard_data.take() else { @@ -481,14 +495,49 @@ impl GatewayTasksBuilder { ); }; + let Some(ecash_manager) = self.ecash_manager.clone() else { + return Err( + GatewayError::InternalWireguardError("ecash manager not set".to_string()).into(), + ); + }; + + let router = nym_wireguard_private_metadata_server::RouterBuilder::with_default_routes(); + let router = router.with_state(nym_wireguard_private_metadata_server::AppState::new( + nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( + wireguard_data.inner.peer_tx().clone(), + ), + )); + + let bind_address = std::net::SocketAddr::new( + wireguard_data.inner.config().private_ipv4.into(), + wireguard_data.inner.config().announced_metadata_port, + ); + let wg_handle = nym_wireguard::start_wireguard( - self.storage.clone(), + ecash_manager, self.metrics.clone(), all_peers, - self.shutdown.fork("wireguard"), + self.legacy_task_client.fork("wireguard"), wireguard_data, ) .await?; - Ok(wg_handle) + + let server = router.build_server(&bind_address).await?; + let cancel_token: tokio_util::sync::CancellationToken = (*self.shutdown_token).clone(); + let axum_shutdown_receiver = cancel_token.clone().cancelled_owned(); + let server_handle = tokio::spawn(async move { + { + info!("Started Wireguard Axum HTTP V2 server on {bind_address}"); + server.run(axum_shutdown_receiver).await + } + }); + + let shutdown_handles = nym_wireguard_private_metadata_server::ShutdownHandles::new( + server_handle, + wg_handle, + cancel_token, + ); + + Ok(shutdown_handles) } } diff --git a/integrations/bity/Cargo.toml b/integrations/bity/Cargo.toml deleted file mode 100644 index 6c96600f82..0000000000 --- a/integrations/bity/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "nym-bity-integration" -version = "0.1.0" -edition = "2021" -rust-version = "1.56" -license.workspace = true - -[dependencies] -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -k256 = { workspace = true, features = ["ecdsa", "sha256"] } -eyre = { workspace = true } - -cosmrs = { workspace = true } - -nym-cli-commands = { path = "../../common/commands" } -nym-validator-client = { path = "../../common/client-libs/validator-client" } - -[dev-dependencies] -anyhow = { workspace = true } diff --git a/integrations/bity/README.md b/integrations/bity/README.md deleted file mode 100644 index c06f59148e..0000000000 --- a/integrations/bity/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Buy NYM with Bity - -This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases. - -## Signing - -The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields: - -``` -account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf -message: "This is the order message from Bity" -order signature: -{ - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "public_key": { - "@type": "/cosmos.crypto.secp256k1.PubKey", - "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" - }, - "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" -} -``` - -The `signature` field of the order contains a JSON representation of: - -- the Cosmos address of the signer (`account_id`) -- the Cosmos public key -- a hex string digest of the Bity order message signed by the user - -Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered. - -## Verification - -Verification has been wrapped up into taking a single struct that can be parsed from JSON: - -``` -{ - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "message": "This is the order message from Bity", - "signature": << ORDER SIGNATURE JSON GOES HERE >> -} -``` - -The following will be checked: - -- the `account_id` supplied matches: - - the account id derived from the public key - - the account id field in the order signature JSON -- the account id is for Nym mainnet -- the signature is for the message -- all data structures parse correctly - - nested structs - - account ids - - Cosmos public keys \ No newline at end of file diff --git a/integrations/bity/src/lib.rs b/integrations/bity/src/lib.rs deleted file mode 100644 index ee99cc01bb..0000000000 --- a/integrations/bity/src/lib.rs +++ /dev/null @@ -1,220 +0,0 @@ -pub mod order; -pub mod sign; -pub mod verify; - -#[cfg(test)] -mod tests { - use crate::order::{Order, OrderSignature}; - use crate::{sign, verify}; - use cosmrs::AccountId; - use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; - use std::str::FromStr; - - fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> { - let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove"; - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?); - - let accounts = wallet.try_derive_accounts()?; - - let message = "This is the order message from Bity"; - let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?; - - Ok((signature, message.to_string())) - } - - #[test] - fn integration_happy_path() -> anyhow::Result<()> { - let (signature, message) = get_order("n")?; - - let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap(); - assert_eq!(account_id.to_string(), signature.account_id.to_string()); - - println!("Order signature:"); - println!("{}", ::serde_json::to_string_pretty(&signature)?); - - Ok(verify::verify_order(Order { - account_id, - signature, - message, - })?) - } - - #[test] - fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> { - let (signature, message) = get_order("nymt")?; - - println!("Order signature:"); - println!("{}", ::serde_json::to_string_pretty(&signature)?); - - let res = verify::verify_order(Order { - account_id: signature.clone().account_id, - signature, - message, - }); - - println!("Expecting error, got: {:?}", res); - - assert!(res.is_err()); - - Ok(()) - } - - #[test] - fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> { - let (signature, message) = get_order("atom")?; - - println!("Order signature:"); - println!("{}", ::serde_json::to_string_pretty(&signature)?); - - let res = verify::verify_order(Order { - account_id: signature.clone().account_id, - signature, - message, - }); - - println!("Expecting error, got: {:?}", res); - - assert!(res.is_err()); - - Ok(()) - } - - #[test] - fn integration_change_account_id() -> anyhow::Result<()> { - let (signature, message) = get_order("n")?; - - let OrderSignature { - signature_as_hex, - public_key, - account_id: _, - } = signature; - - // use a different account id to the one that signed the order - let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap(); - - let res = verify::verify_order(Order { - account_id: account_id.clone(), - signature: OrderSignature { - account_id, - signature_as_hex, - public_key, - }, - message, - }); - - println!("Expecting error, got: {:?}", res); - - assert!(res.is_err()); - - Ok(()) - } - - #[test] - fn integration_change_signature() -> anyhow::Result<()> { - let (signature, message) = get_order("n")?; - - let OrderSignature { - signature_as_hex: _, - public_key, - account_id, - } = signature; - - let res = verify::verify_order(Order { - account_id: account_id.clone(), - signature: OrderSignature { - account_id, - signature_as_hex: "this is not the signature you were looking for".to_string(), - public_key, - }, - message, - }); - - println!("Expecting error, got: {:?}", res); - - assert!(res.is_err()); - - Ok(()) - } - - #[test] - fn integration_with_json_happy_path() -> anyhow::Result<()> { - let json_order = r#"{ - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "message": "This is the order message from Bity", - "signature": { - "public_key": { - "@type": "/cosmos.crypto.secp256k1.PubKey", - "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" - }, - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" - } -}"#; - let order: Order = serde_json::from_str(json_order)?; - - Ok(verify::verify_order(order)?) - } - - #[test] - fn integration_with_json_bad_signature() -> anyhow::Result<()> { - let json_order = r#"{ - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "message": "A different message to the one signed", - "signature": { - "public_key": { - "@type": "/cosmos.crypto.secp256k1.PubKey", - "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" - }, - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" - } -}"#; - let order: Order = serde_json::from_str(json_order)?; - let res = verify::verify_order(order); - println!("Expecting error, got: {:?}", res); - assert!(res.is_err()); - Ok(()) - } - - #[test] - fn integration_with_json_bad_account_id() -> anyhow::Result<()> { - let json_order = r#"{ - "account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", - "message": "This is the order message from Bity", - "signature": { - "public_key": { - "@type": "/cosmos.crypto.secp256k1.PubKey", - "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" - }, - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" - } -}"#; - let order: Order = serde_json::from_str(json_order)?; - let res = verify::verify_order(order); - println!("Expecting error, got: {:?}", res); - assert!(res.is_err()); - Ok(()) - } - - #[test] - fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> { - let json_order = r#"{ - "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "message": "This is the order message from Bity", - "signature": { - "public_key": { - "@type": "/cosmos.crypto.secp256k1.PubKey", - "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" - }, - "account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", - "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" - } -}"#; - let order: Order = serde_json::from_str(json_order)?; - let res = verify::verify_order(order); - println!("Expecting error, got: {:?}", res); - assert!(res.is_err()); - Ok(()) - } -} diff --git a/integrations/bity/src/order.rs b/integrations/bity/src/order.rs deleted file mode 100644 index d5a4a24009..0000000000 --- a/integrations/bity/src/order.rs +++ /dev/null @@ -1,17 +0,0 @@ -use cosmrs::crypto::PublicKey; -use cosmrs::AccountId; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct OrderSignature { - pub public_key: PublicKey, - pub account_id: AccountId, - pub signature_as_hex: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Order { - pub account_id: AccountId, - pub message: String, - pub signature: OrderSignature, -} diff --git a/integrations/bity/src/sign/mod.rs b/integrations/bity/src/sign/mod.rs deleted file mode 100644 index f258164499..0000000000 --- a/integrations/bity/src/sign/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::order::OrderSignature; -use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; -use nym_validator_client::signing::signer::OfflineSigner; -use nym_validator_client::signing::AccountData; - -/// Signs an order message to purchase Nym with Bity -pub fn sign_order( - wallet: &DirectSecp256k1HdWallet, - signer: &AccountData, - message: String, -) -> Result<OrderSignature, NyxdError> { - Ok(OrderSignature { - account_id: signer.address().clone(), - public_key: signer.public_key(), - signature_as_hex: wallet - .sign_raw_with_account(signer, message.into_bytes())? - .to_string(), - }) -} diff --git a/integrations/bity/src/verify/mod.rs b/integrations/bity/src/verify/mod.rs deleted file mode 100644 index 4fb4d77147..0000000000 --- a/integrations/bity/src/verify/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -use thiserror::Error; - -use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key; - -use crate::order::Order; - -#[derive(Error, Debug)] -pub enum VerifyOrderError { - #[error("{source}")] - K256Error { - #[from] - source: k256::ecdsa::Error, - }, - #[error("{source}")] - ErrorReport { - #[from] - source: eyre::Report, - }, - #[error("Account id does not match public key")] - AccountIdDoesNotMatchPubKey, - #[error("Unsupported key type. Only secp256k1 is currently supported.")] - UnsupportedKeyType, - #[error("Signature error - {0}")] - SignatureError(k256::ecdsa::signature::Error), - #[error("Account id is not a Nyx mainnet account")] - AccountIdPrefixIncorrect, -} - -/// Verifies an order -pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> { - let account_id = order.signature.public_key.account_id("n")?; - - if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" { - return Err(VerifyOrderError::AccountIdPrefixIncorrect); - } - - // the account id in the order must match the account id derived from the public key - if account_id != order.signature.account_id { - return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey); - } - - // the user provided account id in the order must match the derived account id - if account_id != order.account_id || account_id != order.signature.account_id { - return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey); - } - - if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL { - return Err(VerifyOrderError::UnsupportedKeyType); - } - - match secp256k1_verify_with_public_key( - &order.signature.public_key.to_bytes(), - order.signature.signature_as_hex, - order.message, - ) { - Ok(()) => Ok(()), - Err(e) => Err(VerifyOrderError::SignatureError(e)), - } -} diff --git a/lefthook.yml b/lefthook.yml index 11b130f92a..0687e78972 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -47,3 +47,7 @@ pre-commit: glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,css}" run: yarn biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} stage_fixed: true + rust-lint: + glob: "*.rs" + run: cargo fmt + stage_fixed: true diff --git a/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json b/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json index 9c0c64d664..03474ab35e 100644 --- a/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json +++ b/nym-api/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json @@ -6,7 +6,7 @@ { "name": "epoch_id: u32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serialised_signatures", @@ -16,7 +16,7 @@ { "name": "serialization_revision: u8", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json b/nym-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json index c8fbd21911..068bc54b4b 100644 --- a/nym-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json +++ b/nym-api/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json @@ -6,7 +6,7 @@ { "name": "epoch_id: u32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serialised_key", @@ -16,7 +16,7 @@ { "name": "serialization_revision: u8", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json b/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json index ba84fb0761..4352545af3 100644 --- a/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json +++ b/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json @@ -6,12 +6,12 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "mix_id", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "identity_key", diff --git a/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json b/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json index 156de4067f..3af24abb79 100644 --- a/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json +++ b/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json @@ -6,7 +6,7 @@ { "name": "count", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json b/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json index 9ad6f6f437..161dfae48d 100644 --- a/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json +++ b/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json @@ -6,7 +6,7 @@ { "name": "reliability: f32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Null" } ], "parameters": { diff --git a/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json b/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json index 2a2b8976de..131935337d 100644 --- a/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json +++ b/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json @@ -6,12 +6,12 @@ { "name": "timestamp", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "reliability: u8", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json b/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json index d1252da321..21a1f70f90 100644 --- a/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json +++ b/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json @@ -6,7 +6,7 @@ { "name": "epoch_id: u32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serialised_signatures", diff --git a/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json b/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json index 72aabf9250..3bfabd2a3a 100644 --- a/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json +++ b/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json @@ -6,7 +6,7 @@ { "name": "count", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json b/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json index cc74ae3e4b..6dd984d42a 100644 --- a/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json +++ b/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json @@ -11,7 +11,7 @@ { "name": "uptime", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-2ee8137d6a332c3cdfe720e91ec4d8e2fb6b4bce50bbc922ab663d062a9cff7c.json b/nym-api/.sqlx/query-2ee8137d6a332c3cdfe720e91ec4d8e2fb6b4bce50bbc922ab663d062a9cff7c.json index 25fe34aeab..76a7022119 100644 --- a/nym-api/.sqlx/query-2ee8137d6a332c3cdfe720e91ec4d8e2fb6b4bce50bbc922ab663d062a9cff7c.json +++ b/nym-api/.sqlx/query-2ee8137d6a332c3cdfe720e91ec4d8e2fb6b4bce50bbc922ab663d062a9cff7c.json @@ -16,7 +16,7 @@ { "name": "count: u32", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json b/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json index 67dc8f42f0..b15eda5f1c 100644 --- a/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json +++ b/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json b/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json index 0f02120676..c0f352a88f 100644 --- a/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json +++ b/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json @@ -6,12 +6,12 @@ { "name": "timestamp", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "reliability: u8", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-38bb0c1c7875d00ab451ad67cb1aed19cc52cfa474a9d48cc2c30d9baba51e3e.json b/nym-api/.sqlx/query-38bb0c1c7875d00ab451ad67cb1aed19cc52cfa474a9d48cc2c30d9baba51e3e.json index 9e41399227..4eddcf35d0 100644 --- a/nym-api/.sqlx/query-38bb0c1c7875d00ab451ad67cb1aed19cc52cfa474a9d48cc2c30d9baba51e3e.json +++ b/nym-api/.sqlx/query-38bb0c1c7875d00ab451ad67cb1aed19cc52cfa474a9d48cc2c30d9baba51e3e.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-4263e1fc5a3b44af8d7ae68f3082f47cef07820c9e8b77ca8e0d93a7924c57ee.json b/nym-api/.sqlx/query-4263e1fc5a3b44af8d7ae68f3082f47cef07820c9e8b77ca8e0d93a7924c57ee.json index ad32a60788..337a93bce0 100644 --- a/nym-api/.sqlx/query-4263e1fc5a3b44af8d7ae68f3082f47cef07820c9e8b77ca8e0d93a7924c57ee.json +++ b/nym-api/.sqlx/query-4263e1fc5a3b44af8d7ae68f3082f47cef07820c9e8b77ca8e0d93a7924c57ee.json @@ -11,7 +11,7 @@ { "name": "count: u32", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json b/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json index 6d6254d3ba..b4e999cf21 100644 --- a/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json +++ b/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json b/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json index c99e1af9d9..fbafeb2e7a 100644 --- a/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json +++ b/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json @@ -6,12 +6,12 @@ { "name": "absolute_epoch_id: u32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "eligible_mixnodes: u32", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json b/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json index 9ff0d9cea2..29e5080a6d 100644 --- a/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json +++ b/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json @@ -6,14 +6,14 @@ { "name": "exists", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { "Right": 0 }, "nullable": [ - null + false ] }, "hash": "5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa" diff --git a/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json b/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json index 0f79148335..8d03e6b94b 100644 --- a/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json +++ b/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json @@ -6,7 +6,7 @@ { "name": "epoch_id: u32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serialised_signatures", diff --git a/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json b/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json index 35be81ffcb..903ee12ebf 100644 --- a/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json +++ b/nym-api/.sqlx/query-676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302.json @@ -6,12 +6,12 @@ { "name": "node_id: NodeId", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "value: f32", "ordinal": 1, - "type_info": "Int" + "type_info": "Null" } ], "parameters": { @@ -19,7 +19,7 @@ }, "nullable": [ false, - true + null ] }, "hash": "676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302" diff --git a/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json b/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json index e063997beb..d4f30c2354 100644 --- a/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json +++ b/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json @@ -6,7 +6,7 @@ { "name": "db_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "identity_key", @@ -16,37 +16,37 @@ { "name": "reliability: u8", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "timestamp!", "ordinal": 3, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "gateway_id!", "ordinal": 4, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "layer1_mix_id!", "ordinal": 5, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "layer2_mix_id!", "ordinal": 6, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "layer3_mix_id!", "ordinal": 7, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "monitor_run_id!", "ordinal": 8, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json b/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json index 61c9dd9cd5..03571f54f4 100644 --- a/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json +++ b/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-71c761aaf86fe395a3ec05704bf5e3bd84c594904011bbec53edb1523b1cc5c0.json b/nym-api/.sqlx/query-71c761aaf86fe395a3ec05704bf5e3bd84c594904011bbec53edb1523b1cc5c0.json index 1ba015d239..54948464e3 100644 --- a/nym-api/.sqlx/query-71c761aaf86fe395a3ec05704bf5e3bd84c594904011bbec53edb1523b1cc5c0.json +++ b/nym-api/.sqlx/query-71c761aaf86fe395a3ec05704bf5e3bd84c594904011bbec53edb1523b1cc5c0.json @@ -6,7 +6,7 @@ { "name": "deposit_id: DepositId", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "merkle_leaf", @@ -16,7 +16,7 @@ { "name": "merkle_index: u32", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json b/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json index e02d48f56e..eb0d21f2b3 100644 --- a/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json +++ b/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json @@ -11,7 +11,7 @@ { "name": "uptime", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json b/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json index aa72f67202..6c62e5e972 100644 --- a/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json +++ b/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json @@ -6,12 +6,12 @@ { "name": "timestamp", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "reliability: u8", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json b/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json index ecdcbeb97b..6f9964d8db 100644 --- a/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json +++ b/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json @@ -6,12 +6,12 @@ { "name": "timestamp", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "reliability: u8", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json b/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json index 6ef9cdf7d2..c746ec816d 100644 --- a/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json +++ b/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json @@ -6,7 +6,7 @@ { "name": "count", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-989b86c24c404f1ec7e0b962586a601b8e3d3ee03162b5319afb8359efab3c85.json b/nym-api/.sqlx/query-989b86c24c404f1ec7e0b962586a601b8e3d3ee03162b5319afb8359efab3c85.json index b137a08a91..d0800749ab 100644 --- a/nym-api/.sqlx/query-989b86c24c404f1ec7e0b962586a601b8e3d3ee03162b5319afb8359efab3c85.json +++ b/nym-api/.sqlx/query-989b86c24c404f1ec7e0b962586a601b8e3d3ee03162b5319afb8359efab3c85.json @@ -11,12 +11,12 @@ { "name": "node_id: NodeId", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "id", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json b/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json index 648001157d..7323886a9f 100644 --- a/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json +++ b/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json @@ -11,7 +11,7 @@ { "name": "uptime!", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-a77f7222a6fb9e7851b3bb71ef39685fde165003c552094b02f6b90dd947519b.json b/nym-api/.sqlx/query-a77f7222a6fb9e7851b3bb71ef39685fde165003c552094b02f6b90dd947519b.json index e96e5fefdd..7cefe8532b 100644 --- a/nym-api/.sqlx/query-a77f7222a6fb9e7851b3bb71ef39685fde165003c552094b02f6b90dd947519b.json +++ b/nym-api/.sqlx/query-a77f7222a6fb9e7851b3bb71ef39685fde165003c552094b02f6b90dd947519b.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json b/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json index 0e5185d864..13fbea4732 100644 --- a/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json +++ b/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json b/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json index 58eb2b5b63..96de815353 100644 --- a/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json +++ b/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json @@ -6,7 +6,7 @@ { "name": "reliability: f32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Null" } ], "parameters": { diff --git a/nym-api/.sqlx/query-b5cc0ec39c7474f73ebb903ff8269fd88f5b0ae3766034b29aeda8248b948d0e.json b/nym-api/.sqlx/query-b5cc0ec39c7474f73ebb903ff8269fd88f5b0ae3766034b29aeda8248b948d0e.json index 7c703d7f63..b06fa7221c 100644 --- a/nym-api/.sqlx/query-b5cc0ec39c7474f73ebb903ff8269fd88f5b0ae3766034b29aeda8248b948d0e.json +++ b/nym-api/.sqlx/query-b5cc0ec39c7474f73ebb903ff8269fd88f5b0ae3766034b29aeda8248b948d0e.json @@ -11,7 +11,7 @@ { "name": "count: u32", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json b/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json index c7305db1cd..ec53ccb5e4 100644 --- a/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json +++ b/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json @@ -6,7 +6,7 @@ { "name": "node_id: NodeId", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json b/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json index eb8e65e763..e2aad4f6a6 100644 --- a/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json +++ b/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json @@ -11,12 +11,12 @@ { "name": "mix_id: NodeId", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "id", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json b/nym-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json index e2f05399ee..7c01c8d1fa 100644 --- a/nym-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json +++ b/nym-api/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json @@ -6,7 +6,7 @@ { "name": "epoch_id: u32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "serialised_signatures", @@ -16,7 +16,7 @@ { "name": "serialization_revision: u8", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json b/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json index 0c6d2f6072..c4b25591f3 100644 --- a/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json +++ b/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json @@ -6,12 +6,12 @@ { "name": "mix_id: NodeId", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "value: f32", "ordinal": 1, - "type_info": "Int64" + "type_info": "Null" } ], "parameters": { @@ -19,7 +19,7 @@ }, "nullable": [ false, - true + null ] }, "hash": "c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166" diff --git a/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json b/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json index e1e680e7bf..62513a4d83 100644 --- a/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json +++ b/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-cdc99f6f23d058577bd587be49e5b80b949735e2f0fcc45bfcbd7c0d96e1c973.json b/nym-api/.sqlx/query-cdc99f6f23d058577bd587be49e5b80b949735e2f0fcc45bfcbd7c0d96e1c973.json index a6301fb174..2d6131e218 100644 --- a/nym-api/.sqlx/query-cdc99f6f23d058577bd587be49e5b80b949735e2f0fcc45bfcbd7c0d96e1c973.json +++ b/nym-api/.sqlx/query-cdc99f6f23d058577bd587be49e5b80b949735e2f0fcc45bfcbd7c0d96e1c973.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json b/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json index a31cc32c92..25adfd0a84 100644 --- a/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json +++ b/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json @@ -6,7 +6,7 @@ { "name": "reliability: f32", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json b/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json index 3a8afcec25..a47d876f4d 100644 --- a/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json +++ b/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json @@ -6,7 +6,7 @@ { "name": "count", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json b/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json index 8d314d07f8..c972805000 100644 --- a/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json +++ b/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json @@ -6,14 +6,14 @@ { "name": "exists", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { "Right": 1 }, "nullable": [ - null + false ] }, "hash": "e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e" diff --git a/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json b/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json index 661d52138e..9822780c3c 100644 --- a/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json +++ b/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json @@ -6,7 +6,7 @@ { "name": "count", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json b/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json index 987eee80a8..b391eb7cc1 100644 --- a/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json +++ b/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json @@ -11,7 +11,7 @@ { "name": "uptime!", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json b/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json index 3ef0dd10a6..55cfdd3125 100644 --- a/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json +++ b/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json @@ -6,12 +6,12 @@ { "name": "db_id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "mix_id!", "ordinal": 1, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "identity_key", @@ -21,37 +21,37 @@ { "name": "reliability: u8", "ordinal": 3, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "timestamp!", "ordinal": 4, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "gateway_id!", "ordinal": 5, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "layer1_mix_id!", "ordinal": 6, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "layer2_mix_id!", "ordinal": 7, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "layer3_mix_id!", "ordinal": 8, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "monitor_run_id!", "ordinal": 9, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4a8856b45e..87a8eb9ed2 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.56" +version = "1.1.64" authors.workspace = true edition = "2021" rust-version.workspace = true @@ -16,17 +16,13 @@ async-trait = { workspace = true } bs58 = { workspace = true } bip39 = { workspace = true } bincode.workspace = true +console-subscriber = { workspace = true, optional = true } # nym-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" cfg-if = { workspace = true } clap = { workspace = true, features = ["cargo", "derive", "env"] } -console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" dashmap = { workspace = true } -dirs = { workspace = true } futures = { workspace = true } -itertools = { workspace = true } humantime-serde = { workspace = true } -k256 = { workspace = true, features = [ - "ecdsa-core", -] } # needed for the Verifier trait; pull whatever version is used by other dependencies +moka = { workspace = true } pin-project = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } @@ -50,7 +46,6 @@ tendermint = { workspace = true } ts-rs = { workspace = true, optional = true } anyhow = { workspace = true } -getset = { workspace = true } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", @@ -65,30 +60,19 @@ zeroize = { workspace = true } # for axum server axum = { workspace = true, features = ["tokio"] } -axum-extra = { workspace = true, features = ["typed-header"] } tower-http = { workspace = true, features = ["cors", "trace", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd"] } utoipa = { workspace = true, features = ["axum_extras", "time"] } utoipauto = { workspace = true } utoipa-swagger-ui = { workspace = true, features = ["axum"] } tracing = { workspace = true } -## ephemera-specific -#actix-web = "4" -#array-bytes = "6.0.0" -#chrono = { version = "0.4.24", default-features = false, features = ["clock"] } -#futures-util = "0.3.25" -#serde_derive = "1.0.149" -#uuid = { version = "1.3.0", features = ["serde", "v4"] } - ## internal -#ephemera = { path = "../ephemera" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" } nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" } nym-credentials-interface = { path = "../common/credentials-interface" } -#nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } nym-config = { path = "../common/config" } cosmwasm-std = { workspace = true } nym-credential-storage = { path = "../common/credential-storage", features = [ @@ -101,11 +85,8 @@ cw3 = { workspace = true } cw4 = { workspace = true } nym-dkg = { path = "../common/dkg", features = ["cw-types"] } nym-gateway-client = { path = "../common/client-libs/gateway-client" } -nym-inclusion-probability = { path = "../common/inclusion-probability" } nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } -nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" } nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float", "utoipa"] } -nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } @@ -116,11 +97,12 @@ nym-bin-common = { path = "../common/bin-common", features = ["output_format", " nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-types = { path = "../common/types" } -nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } +nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa", "output", "middleware"] } nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" } nym-statistics-common = { path = "../common/statistics" } -chrono.workspace = true +nym-ecash-signer-check = { path = "../common/ecash-signer-check" } + [features] no-reward = [] @@ -128,6 +110,7 @@ v2-performance = [] generate-ts = ["ts-rs"] [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", @@ -143,7 +126,8 @@ cw3 = { workspace = true } cw-utils = { workspace = true } rand_chacha = { workspace = true } sha2 = { workspace = true } -dotenv = "0.15" +dotenvy = { workspace = true } +test-with = { workspace = true, default-features = false } [lints] workspace = true diff --git a/nym-api/build.rs b/nym-api/build.rs index a2c0377698..09184d677d 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -1,22 +1,33 @@ +use anyhow::Context; use sqlx::{Connection, FromRow, SqliteConnection}; use std::env; -// it's fine if compilation fails -#[allow(clippy::unwrap_used)] -#[allow(clippy::expect_used)] -#[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/nym-api-example.sqlite", out_dir); +const SQLITE_DB_FILENAME: &str = "nym-api-example.sqlite"; - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; + let database_path = format!("{out_dir}/{SQLITE_DB_FILENAME}"); + + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + + #[cfg(target_family = "unix")] + write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME) .await - .expect("Failed to create SQLx database connection"); + .ok(); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) + .await + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[derive(FromRow)] struct Exists { @@ -26,8 +37,7 @@ async fn main() { // check if it was already run let res: Exists = sqlx::query_as("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'") .fetch_one(&mut conn) - .await - .unwrap(); + .await?; let already_run = res.exists; @@ -51,7 +61,7 @@ async fn main() { ) .execute(&mut conn) .await - .expect("failed to update post v3 migration tables"); + .context("failed to update post v3 migration tables")?; } #[cfg(target_family = "unix")] @@ -61,4 +71,34 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) +} + +/// use `./enter_db.sh` to inspect DB +#[cfg(target_family = "unix")] +async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { + use std::os::unix::fs::PermissionsExt; + use tokio::{fs::File, io::AsyncWriteExt}; + + if env::var("CI").is_ok() { + return Ok(()); + } + let mut file = File::create("settings.sql").await?; + let settings = ".mode columns +.headers on"; + file.write_all(settings.as_bytes()).await?; + + let mut file = File::create("enter_db.sh").await?; + let contents = format!( + "#!/bin/sh\n\ + sqlite3 -init settings.sql {out_dir}/{db_filename}", + ); + file.write_all(contents.as_bytes()).await?; + + file.set_permissions(std::fs::Permissions::from_mode(0o755)) + .await + .map_err(anyhow::Error::from)?; + + Ok(()) } diff --git a/nym-api/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql b/nym-api/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql new file mode 100644 index 0000000000..67cc50a957 --- /dev/null +++ b/nym-api/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql @@ -0,0 +1,51 @@ +/* + * Copyright 2025 - Nym Technologies SA <contact@nymtech.net> + * SPDX-License-Identifier: GPL-3.0-only + */ + +-- Change performed in this migration: +-- remove PK on expiration_date and instead use composite (epoch_id, expiration_date) PK + + +CREATE TABLE global_expiration_date_signatures_new +( + expiration_date DATE NOT NULL, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +); + +CREATE TABLE partial_expiration_date_signatures_new +( + expiration_date DATE NOT NULL, + + epoch_id INTEGER NOT NULL, + + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +); + +-- global +INSERT INTO global_expiration_date_signatures_new +SELECT * +FROM global_expiration_date_signatures; + +DROP TABLE global_expiration_date_signatures; + +ALTER TABLE global_expiration_date_signatures_new + RENAME TO global_expiration_date_signatures; + +-- partial +INSERT INTO partial_expiration_date_signatures_new +SELECT * +FROM partial_expiration_date_signatures; + +DROP TABLE partial_expiration_date_signatures; + +ALTER TABLE partial_expiration_date_signatures_new + RENAME TO partial_expiration_date_signatures; \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index e75f21b0ba..267cb731f7 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -10,7 +10,6 @@ license.workspace = true bs58 = { workspace = true } cosmrs = { workspace = true } cosmwasm-std = { workspace = true } -getset = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } humantime-serde = { workspace = true } @@ -23,6 +22,7 @@ thiserror.workspace = true time = { workspace = true, features = ["serde", "parsing", "formatting"] } ts-rs = { workspace = true, optional = true } utoipa.workspace = true +tracing = { workspace = true } # for serde on secp256k1 signatures ecdsa = { workspace = true, features = ["serde"] } @@ -36,9 +36,13 @@ nym-ecash-time = { path = "../../common/ecash-time" } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float"] } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } +nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] } +nym-noise-keys = { path = "../../common/nymnoise/keys" } nym-network-defaults = { path = "../../common/network-defaults" } nym-ticketbooks-merkle = { path = "../../common/ticketbooks-merkle" } +nym-ecash-signer-check-types = { path = "../../common/ecash-signer-check-types" } + [dev-dependencies] rand_chacha = { workspace = true } diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs index 3779d27e17..a69d0c1d33 100644 --- a/nym-api/nym-api-requests/src/ecash/models.rs +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -1,7 +1,9 @@ // Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 +use crate::signable::SignedMessage; use cosmrs::AccountId; +use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; use nym_compact_ecash::utils::try_deserialize_g1_projective; @@ -12,14 +14,13 @@ use nym_credentials_interface::{ VerificationKeyAuth, WithdrawalRequest, }; use nym_crypto::asymmetric::ed25519; -use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_signature; use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof}; use serde::{Deserialize, Serialize}; use sha2::Digest; use std::collections::BTreeMap; use std::ops::Deref; use thiserror::Error; -use time::Date; +use time::{Date, OffsetDateTime}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, ToSchema)] @@ -541,74 +542,6 @@ pub struct CommitedDeposit { pub merkle_index: usize, } -// -// - -// make sure only our types can implement this trait (to ensure infallible serialisation) -mod private { - use crate::ecash::models::{ - IssuedTicketbooksChallengeCommitmentRequestBody, - IssuedTicketbooksChallengeCommitmentResponseBody, IssuedTicketbooksDataRequestBody, - IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponseBody, - }; - - pub trait Sealed {} - - // requests - impl Sealed for IssuedTicketbooksChallengeCommitmentRequestBody {} - impl Sealed for IssuedTicketbooksDataRequestBody {} - - // responses - impl Sealed for IssuedTicketbooksChallengeCommitmentResponseBody {} - impl Sealed for IssuedTicketbooksForResponseBody {} - impl Sealed for IssuedTicketbooksDataResponseBody {} -} - -// the trait is not public as it's only defined on types that are guaranteed to not panic when serialised -pub trait SignableMessageBody: Serialize + private::Sealed { - fn sign(self, key: &ed25519::PrivateKey) -> SignedMessage<Self> - where - Self: Sized, - { - let signature = key.sign(self.plaintext()); - SignedMessage { - body: self, - signature, - } - } - - fn plaintext(&self) -> Vec<u8> { - #[allow(clippy::unwrap_used)] - // SAFETY: all types that implement this trait have valid serialisations - serde_json::to_vec(&self).unwrap() - } -} - -impl<T> SignableMessageBody for T where T: Serialize + private::Sealed {} - -#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct SignedMessage<T> { - pub body: T, - #[schema(value_type = String)] - #[serde(with = "bs58_ed25519_signature")] - pub signature: ed25519::Signature, -} - -impl<T> SignedMessage<T> { - pub fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool - where - T: SignableMessageBody, - { - let plaintext = self.body.plaintext(); - if plaintext.is_empty() { - return false; - } - - pub_key.verify(&plaintext, &self.signature).is_ok() - } -} - pub type IssuedTicketbooksDataRequest = SignedMessage<IssuedTicketbooksDataRequestBody>; pub type IssuedTicketbooksChallengeCommitmentRequest = SignedMessage<IssuedTicketbooksChallengeCommitmentRequestBody>; @@ -826,6 +759,31 @@ pub struct IssuedTicketbooksForCount { pub count: u32, } +pub type EcashSignerStatusResponse = SignedMessage<EcashSignerStatusResponseBody>; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[serde(rename_all = "camelCase")] +// includes all pre-requisites for successful (assuming valid request) `/blind-sign` +pub struct EcashSignerStatusResponseBody { + #[serde(with = "time::serde::rfc3339")] + #[schema(value_type = String)] + pub current_time: OffsetDateTime, + + /// Current, perceived, dkg epoch id + pub dkg_ecash_epoch_id: EpochId, + + /// Flag indicating whether the operator has explicitly disabled signer functionalities in the api + pub signer_disabled: bool, + + /// Flag indicating whether this api thinks it's a valid ecash signer for the current epoch + pub is_ecash_signer: bool, + + /// Flag indicating whether this api thinks it has valid signing keys. + /// It might be a valid signer that's not disabled, but the keys might have accidentally been + /// removed due to invalid data migration. + pub has_signing_keys: bool, +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 681f1c6b0f..371ab9ba56 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -11,6 +11,7 @@ pub mod legacy; pub mod models; pub mod nym_nodes; pub mod pagination; +pub mod signable; // The response type we fetch from the network details endpoint. #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs deleted file mode 100644 index a39b6f25c7..0000000000 --- a/nym-api/nym-api-requests/src/models.rs +++ /dev/null @@ -1,1868 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: Apache-2.0 - -#![allow(deprecated)] - -use crate::helpers::unix_epoch; -use crate::helpers::PlaceholderJsonSchemaImpl; -use crate::legacy::{ - LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, -}; -use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; -use crate::pagination::PaginatedResponse; -use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; -use nym_contracts_common::NaiveFloat; -use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; -use nym_crypto::asymmetric::x25519::{ - self, - serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, -}; -use nym_mixnet_contract_common::nym_node::Role; -use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; -use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{ - EpochId, GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent, -}; -use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; -use nym_node_requests::api::v1::authenticator::models::Authenticator; -use nym_node_requests::api::v1::gateway::models::Wireguard; -use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; -use schemars::gen::SchemaGenerator; -use schemars::schema::{InstanceType, Schema, SchemaObject}; -use schemars::JsonSchema; -use serde::{Deserialize, Deserializer, Serialize}; -use std::collections::BTreeMap; -use std::fmt::{Debug, Display, Formatter}; -use std::net::IpAddr; -use std::ops::{Deref, DerefMut}; -use std::{fmt, time::Duration}; -use thiserror::Error; -use time::{Date, OffsetDateTime}; -use utoipa::{IntoParams, ToResponse, ToSchema}; - -pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct RequestError { - message: String, -} - -impl RequestError { - pub fn new<S: Into<String>>(msg: S) -> Self { - RequestError { - message: msg.into(), - } - } - - pub fn message(&self) -> &str { - &self.message - } - - pub fn empty() -> Self { - Self { - message: String::new(), - } - } -} - -impl Display for RequestError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(&self.message, f) - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts" - ) -)] -#[serde(rename_all = "snake_case")] -pub enum MixnodeStatus { - Active, // in both the active set and the rewarded set - Standby, // only in the rewarded set - Inactive, // in neither the rewarded set nor the active set, but is bonded - NotFound, // doesn't even exist in the bonded set -} -impl MixnodeStatus { - pub fn is_active(&self) -> bool { - *self == MixnodeStatus::Active - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts" - ) -)] -pub struct MixnodeCoreStatusResponse { - pub mix_id: NodeId, - pub count: i32, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts" - ) -)] -pub struct GatewayCoreStatusResponse { - pub identity: String, - pub count: i32, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts" - ) -)] -pub struct MixnodeStatusResponse { - pub status: MixnodeStatus, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct NodePerformance { - #[schema(value_type = String)] - pub most_recent: Performance, - #[schema(value_type = String)] - pub last_hour: Performance, - #[schema(value_type = String)] - pub last_24h: Performance, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema, ToSchema)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts") -)] -pub enum DisplayRole { - EntryGateway, - Layer1, - Layer2, - Layer3, - ExitGateway, - Standby, -} - -impl From<Role> for DisplayRole { - fn from(role: Role) -> Self { - match role { - Role::EntryGateway => DisplayRole::EntryGateway, - Role::Layer1 => DisplayRole::Layer1, - Role::Layer2 => DisplayRole::Layer2, - Role::Layer3 => DisplayRole::Layer3, - Role::ExitGateway => DisplayRole::ExitGateway, - Role::Standby => DisplayRole::Standby, - } - } -} - -impl From<DisplayRole> for Role { - fn from(role: DisplayRole) -> Self { - match role { - DisplayRole::EntryGateway => Role::EntryGateway, - DisplayRole::Layer1 => Role::Layer1, - DisplayRole::Layer2 => Role::Layer2, - DisplayRole::Layer3 => Role::Layer3, - DisplayRole::ExitGateway => Role::ExitGateway, - DisplayRole::Standby => Role::Standby, - } - } -} - -// imo for now there's no point in exposing more than that, -// nym-api shouldn't be calculating apy or stake saturation for you. -// it should just return its own metrics (performance) and then you can do with it as you wish -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts" - ) -)] -pub struct NodeAnnotation { - #[cfg_attr(feature = "generate-ts", ts(type = "string"))] - // legacy - #[schema(value_type = String)] - pub last_24h_performance: Performance, - pub current_role: Option<DisplayRole>, - - pub detailed_performance: DetailedNodePerformance, -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/DetailedNodePerformance.ts" - ) -)] -#[non_exhaustive] -pub struct DetailedNodePerformance { - /// routing_score * config_score - pub performance_score: f64, - - pub routing_score: RoutingScore, - pub config_score: ConfigScore, -} - -impl DetailedNodePerformance { - pub fn new( - performance_score: f64, - routing_score: RoutingScore, - config_score: ConfigScore, - ) -> DetailedNodePerformance { - Self { - performance_score, - routing_score, - config_score, - } - } - - pub fn to_rewarding_performance(&self) -> Performance { - Performance::naive_try_from_f64(self.performance_score).unwrap_or_default() - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export, export_to = "ts-packages/types/src/types/rust/RoutingScore.ts") -)] -#[non_exhaustive] -pub struct RoutingScore { - /// Total score after taking all the criteria into consideration - pub score: f64, -} - -impl RoutingScore { - pub fn new(score: f64) -> RoutingScore { - Self { score } - } - - pub fn legacy_performance(&self) -> Performance { - Performance::naive_try_from_f64(self.score).unwrap_or_default() - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export, export_to = "ts-packages/types/src/types/rust/ConfigScore.ts") -)] -#[non_exhaustive] -pub struct ConfigScore { - /// Total score after taking all the criteria into consideration - pub score: f64, - - pub versions_behind: Option<u32>, - pub self_described_api_available: bool, - pub accepted_terms_and_conditions: bool, - pub runs_nym_node_binary: bool, -} - -impl ConfigScore { - pub fn new( - score: f64, - versions_behind: u32, - accepted_terms_and_conditions: bool, - runs_nym_node_binary: bool, - ) -> ConfigScore { - Self { - score, - versions_behind: Some(versions_behind), - self_described_api_available: true, - accepted_terms_and_conditions, - runs_nym_node_binary, - } - } - - pub fn bad_semver() -> ConfigScore { - ConfigScore { - score: 0.0, - versions_behind: None, - self_described_api_available: true, - accepted_terms_and_conditions: false, - runs_nym_node_binary: false, - } - } - - pub fn unavailable() -> ConfigScore { - ConfigScore { - score: 0.0, - versions_behind: None, - self_described_api_available: false, - accepted_terms_and_conditions: false, - runs_nym_node_binary: false, - } - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts" - ) -)] -pub struct AnnotationResponse { - #[schema(value_type = u32)] - pub node_id: NodeId, - pub annotation: Option<NodeAnnotation>, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts" - ) -)] -pub struct NodePerformanceResponse { - #[schema(value_type = u32)] - pub node_id: NodeId, - pub performance: Option<f64>, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts" - ) -)] -pub struct NodeDatePerformanceResponse { - #[schema(value_type = u32)] - pub node_id: NodeId, - #[schema(value_type = String, example = "1970-01-01")] - #[schemars(with = "String")] - #[cfg_attr(feature = "generate-ts", ts(type = "string"))] - pub date: Date, - pub performance: Option<f64>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[schema(title = "LegacyMixNodeDetailsWithLayer")] -pub struct LegacyMixNodeDetailsWithLayerSchema { - /// Basic bond information of this mixnode, such as owner address, original pledge, etc. - #[schema(example = "unimplemented schema")] - pub bond_information: String, - - /// Details used for computation of rewarding related data. - #[schema(example = "unimplemented schema")] - pub rewarding_details: String, - - /// Adjustments to the mixnode that are ought to happen during future epoch transitions. - #[schema(example = "unimplemented schema")] - pub pending_changes: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct MixNodeBondAnnotated { - #[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)] - pub mixnode_details: LegacyMixNodeDetailsWithLayer, - #[schema(value_type = String)] - pub stake_saturation: StakeSaturation, - #[schema(value_type = String)] - pub uncapped_stake_saturation: StakeSaturation, - // NOTE: the performance field is deprecated in favour of node_performance - #[schema(value_type = String)] - pub performance: Performance, - pub node_performance: NodePerformance, - #[schema(value_type = String)] - pub estimated_operator_apy: Decimal, - #[schema(value_type = String)] - pub estimated_delegators_apy: Decimal, - pub blacklisted: bool, - - // a rather temporary thing until we query self-described endpoints of mixnodes - #[serde(default)] - #[schema(value_type = Vec<String>)] - pub ip_addresses: Vec<IpAddr>, -} - -#[derive(Debug, Error)] -pub enum MalformedNodeBond { - #[error("the associated ed25519 identity key is malformed")] - InvalidEd25519Key, - - #[error("the associated x25519 sphinx key is malformed")] - InvalidX25519Key, -} - -impl MixNodeBondAnnotated { - pub fn mix_node(&self) -> &MixNode { - &self.mixnode_details.bond_information.mix_node - } - - pub fn mix_id(&self) -> NodeId { - self.mixnode_details.mix_id() - } - - pub fn identity_key(&self) -> &str { - self.mixnode_details.bond_information.identity() - } - - pub fn owner(&self) -> &Addr { - self.mixnode_details.bond_information.owner() - } - - pub fn version(&self) -> &str { - &self.mixnode_details.bond_information.mix_node.version - } - - pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> { - Ok(SkimmedNode { - node_id: self.mix_id(), - ed25519_identity_pubkey: self - .identity_key() - .parse() - .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, - ip_addresses: self.ip_addresses.clone(), - mix_port: self.mix_node().mix_port, - x25519_sphinx_pubkey: self - .mix_node() - .sphinx_key - .parse() - .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, - role, - supported_roles: DeclaredRoles { - mixnode: true, - entry: false, - exit_nr: false, - exit_ipr: false, - }, - entry: None, - performance: self.node_performance.last_24h, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct GatewayBondAnnotated { - pub gateway_bond: LegacyGatewayBondWithId, - - #[serde(default)] - pub self_described: Option<GatewayDescription>, - - // NOTE: the performance field is deprecated in favour of node_performance - #[schema(value_type = String)] - pub performance: Performance, - pub node_performance: NodePerformance, - pub blacklisted: bool, - - #[serde(default)] - #[schema(value_type = Vec<String>)] - pub ip_addresses: Vec<IpAddr>, -} - -impl GatewayBondAnnotated { - pub fn version(&self) -> &str { - &self.gateway_bond.gateway.version - } - - pub fn identity(&self) -> &String { - self.gateway_bond.bond.identity() - } - - pub fn owner(&self) -> &Addr { - self.gateway_bond.bond.owner() - } - - pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> { - Ok(SkimmedNode { - node_id: self.gateway_bond.node_id, - ip_addresses: self.ip_addresses.clone(), - ed25519_identity_pubkey: self - .gateway_bond - .gateway - .identity_key - .parse() - .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, - mix_port: self.gateway_bond.bond.gateway.mix_port, - x25519_sphinx_pubkey: self - .gateway_bond - .gateway - .sphinx_key - .parse() - .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, - role, - supported_roles: DeclaredRoles { - mixnode: false, - entry: true, - exit_nr: false, - exit_ipr: false, - }, - entry: Some(BasicEntryInformation { - hostname: None, - ws_port: self.gateway_bond.bond.gateway.clients_port, - wss_port: None, - }), - performance: self.node_performance.last_24h, - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct GatewayDescription { - // for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly) -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)] -pub struct ComputeRewardEstParam { - #[schema(value_type = Option<String>)] - #[param(value_type = Option<String>)] - pub performance: Option<Performance>, - pub active_in_rewarded_set: Option<bool>, - pub pledge_amount: Option<u64>, - pub total_delegation: Option<u64>, - #[schema(value_type = Option<CoinSchema>)] - #[param(value_type = Option<CoinSchema>)] - pub interval_operating_cost: Option<Coin>, - #[schema(value_type = Option<String>)] - #[param(value_type = Option<String>)] - pub profit_margin_percent: Option<Percent>, -} - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts" - ) -)] -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct RewardEstimationResponse { - pub estimation: RewardEstimate, - pub reward_params: RewardingParams, - pub epoch: Interval, - #[cfg_attr(feature = "generate-ts", ts(type = "number"))] - pub as_at: i64, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct UptimeResponse { - #[schema(value_type = u32)] - pub mix_id: NodeId, - // The same as node_performance.last_24h. Legacy - pub avg_uptime: u8, - pub node_performance: NodePerformance, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct GatewayUptimeResponse { - pub identity: String, - // The same as node_performance.last_24h. Legacy - pub avg_uptime: u8, - pub node_performance: NodePerformance, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts" - ) -)] -pub struct StakeSaturationResponse { - #[cfg_attr(feature = "generate-ts", ts(type = "string"))] - #[schema(value_type = String)] - pub saturation: StakeSaturation, - - #[cfg_attr(feature = "generate-ts", ts(type = "string"))] - #[schema(value_type = String)] - pub uncapped_saturation: StakeSaturation, - pub as_at: i64, -} - -pub type StakeSaturation = Decimal; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/SelectionChance.ts" - ) -)] -#[deprecated] -pub enum SelectionChance { - High, - Good, - Low, -} - -impl From<f64> for SelectionChance { - fn from(p: f64) -> SelectionChance { - match p { - p if p >= 0.7 => SelectionChance::High, - p if p >= 0.3 => SelectionChance::Good, - _ => SelectionChance::Low, - } - } -} - -impl From<Decimal> for SelectionChance { - fn from(p: Decimal) -> Self { - match p { - p if p >= Decimal::from_ratio(70u32, 100u32) => SelectionChance::High, - p if p >= Decimal::from_ratio(30u32, 100u32) => SelectionChance::Good, - _ => SelectionChance::Low, - } - } -} - -impl fmt::Display for SelectionChance { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SelectionChance::High => write!(f, "High"), - SelectionChance::Good => write!(f, "Good"), - SelectionChance::Low => write!(f, "Low"), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts" - ) -)] -#[deprecated] -pub struct InclusionProbabilityResponse { - pub in_active: SelectionChance, - pub in_reserve: SelectionChance, -} - -impl fmt::Display for InclusionProbabilityResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "in_active: {}, in_reserve: {}", - self.in_active, self.in_reserve - ) - } -} - -#[deprecated] -#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] -pub struct AllInclusionProbabilitiesResponse { - pub inclusion_probabilities: Vec<InclusionProbability>, - pub samples: u64, - pub elapsed: Duration, - pub delta_max: f64, - pub delta_l2: f64, - pub as_at: i64, -} - -#[deprecated] -#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] -pub struct InclusionProbability { - #[schema(value_type = u32)] - pub mix_id: NodeId, - pub in_active: f64, - pub in_reserve: f64, -} - -type Uptime = u8; - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct MixnodeStatusReportResponse { - pub mix_id: NodeId, - pub identity: IdentityKey, - pub owner: String, - #[schema(value_type = u8)] - pub most_recent: Uptime, - #[schema(value_type = u8)] - pub last_hour: Uptime, - #[schema(value_type = u8)] - pub last_day: Uptime, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct GatewayStatusReportResponse { - pub identity: String, - pub owner: String, - #[schema(value_type = u8)] - pub most_recent: Uptime, - #[schema(value_type = u8)] - pub last_hour: Uptime, - #[schema(value_type = u8)] - pub last_day: Uptime, -} - -#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts" - ) -)] -pub struct PerformanceHistoryResponse { - #[schema(value_type = u32)] - pub node_id: NodeId, - pub history: PaginatedResponse<HistoricalPerformanceResponse>, -} - -#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts" - ) -)] -pub struct UptimeHistoryResponse { - #[schema(value_type = u32)] - pub node_id: NodeId, - pub history: PaginatedResponse<HistoricalUptimeResponse>, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts" - ) -)] -pub struct HistoricalUptimeResponse { - #[schema(value_type = String, example = "1970-01-01")] - #[schemars(with = "String")] - #[cfg_attr(feature = "generate-ts", ts(type = "string"))] - pub date: Date, - - pub uptime: Uptime, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts" - ) -)] -pub struct HistoricalPerformanceResponse { - #[schema(value_type = String, example = "1970-01-01")] - #[schemars(with = "String")] - #[cfg_attr(feature = "generate-ts", ts(type = "string"))] - pub date: Date, - - pub performance: f64, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct OldHistoricalUptimeResponse { - pub date: String, - #[schema(value_type = u8)] - pub uptime: Uptime, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct MixnodeUptimeHistoryResponse { - pub mix_id: NodeId, - pub identity: String, - pub owner: String, - pub history: Vec<OldHistoricalUptimeResponse>, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct GatewayUptimeHistoryResponse { - pub identity: String, - pub owner: String, - pub history: Vec<OldHistoricalUptimeResponse>, -} - -#[derive(ToSchema)] -#[schema(title = "Coin")] -pub struct CoinSchema { - pub denom: String, - #[schema(value_type = String)] - pub amount: Uint128, -} - -#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema, ToResponse)] -pub struct CirculatingSupplyResponse { - #[schema(value_type = CoinSchema)] - pub total_supply: Coin, - #[schema(value_type = CoinSchema)] - pub mixmining_reserve: Coin, - #[schema(value_type = CoinSchema)] - pub vesting_tokens: Coin, - #[schema(value_type = CoinSchema)] - pub circulating_supply: Coin, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct HostInformation { - #[schema(value_type = Vec<String>)] - pub ip_address: Vec<IpAddr>, - pub hostname: Option<String>, - pub keys: HostKeys, -} - -impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation { - fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self { - HostInformation { - ip_address: value.ip_address, - hostname: value.hostname, - keys: value.keys.into(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct HostKeys { - #[serde(with = "bs58_ed25519_pubkey")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub ed25519: ed25519::PublicKey, - - #[serde(with = "bs58_x25519_pubkey")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub x25519: x25519::PublicKey, - - #[serde(default)] - #[serde(with = "option_bs58_x25519_pubkey")] - #[schemars(with = "Option<String>")] - #[schema(value_type = String)] - pub x25519_noise: Option<x25519::PublicKey>, -} - -impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys { - fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self { - HostKeys { - ed25519: value.ed25519_identity, - x25519: value.x25519_sphinx, - x25519_noise: value.x25519_noise, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct WebSockets { - pub ws_port: u16, - - pub wss_port: Option<u16>, -} - -impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets { - fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self { - WebSockets { - ws_port: value.ws_port, - wss_port: value.wss_port, - } - } -} - -pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error> -where - D: Deserializer<'de>, -{ - Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch())) -} - -// for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)] -pub struct OffsetDateTimeJsonSchemaWrapper( - #[serde( - default = "unix_epoch", - with = "crate::helpers::overengineered_offset_date_time_serde" - )] - #[schema(inline)] - pub OffsetDateTime, -); - -impl Default for OffsetDateTimeJsonSchemaWrapper { - fn default() -> Self { - OffsetDateTimeJsonSchemaWrapper(unix_epoch()) - } -} - -impl Display for OffsetDateTimeJsonSchemaWrapper { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(&self.0, f) - } -} - -impl From<OffsetDateTimeJsonSchemaWrapper> for OffsetDateTime { - fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self { - value.0 - } -} - -impl From<OffsetDateTime> for OffsetDateTimeJsonSchemaWrapper { - fn from(value: OffsetDateTime) -> Self { - OffsetDateTimeJsonSchemaWrapper(value) - } -} - -impl Deref for OffsetDateTimeJsonSchemaWrapper { - type Target = OffsetDateTime; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for OffsetDateTimeJsonSchemaWrapper { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -// implementation taken from: https://github.com/GREsau/schemars/pull/207 -impl JsonSchema for OffsetDateTimeJsonSchemaWrapper { - fn is_referenceable() -> bool { - false - } - - fn schema_name() -> String { - "DateTime".into() - } - - fn json_schema(_: &mut SchemaGenerator) -> Schema { - SchemaObject { - instance_type: Some(InstanceType::String.into()), - format: Some("date-time".into()), - ..Default::default() - } - .into() - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct NymNodeDescription { - #[schema(value_type = u32)] - pub node_id: NodeId, - pub contract_node_type: DescribedNodeType, - pub description: NymNodeData, -} - -impl NymNodeDescription { - pub fn version(&self) -> &str { - &self.description.build_information.build_version - } - - pub fn entry_information(&self) -> BasicEntryInformation { - BasicEntryInformation { - hostname: self.description.host_information.hostname.clone(), - ws_port: self.description.mixnet_websockets.ws_port, - wss_port: self.description.mixnet_websockets.wss_port, - } - } - - pub fn ed25519_identity_key(&self) -> ed25519::PublicKey { - self.description.host_information.keys.ed25519 - } - - pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { - let keys = &self.description.host_information.keys; - let entry = if self.description.declared_role.entry { - Some(self.entry_information()) - } else { - None - }; - - SkimmedNode { - node_id: self.node_id, - ed25519_identity_pubkey: keys.ed25519, - ip_addresses: self.description.host_information.ip_address.clone(), - mix_port: self.description.mix_port(), - x25519_sphinx_pubkey: keys.x25519, - // we can't use the declared roles, we have to take whatever was provided in the contract. - // why? say this node COULD operate as an exit, but it might be the case the contract decided - // to assign it an ENTRY role only. we have to use that one instead. - role, - supported_roles: self.description.declared_role, - entry, - performance, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[serde(rename_all = "snake_case")] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts" - ) -)] -pub enum DescribedNodeType { - LegacyMixnode, - LegacyGateway, - NymNode, -} - -impl DescribedNodeType { - pub fn is_nym_node(&self) -> bool { - matches!(self, DescribedNodeType::NymNode) - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts" - ) -)] -pub struct DeclaredRoles { - pub mixnode: bool, - pub entry: bool, - pub exit_nr: bool, - pub exit_ipr: bool, -} - -impl DeclaredRoles { - pub fn can_operate_exit_gateway(&self) -> bool { - self.exit_ipr && self.exit_nr - } -} - -impl From<NodeRoles> for DeclaredRoles { - fn from(value: NodeRoles) -> Self { - DeclaredRoles { - mixnode: value.mixnode_enabled, - entry: value.gateway_enabled, - exit_nr: value.gateway_enabled && value.network_requester_enabled, - exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled, - } - } -} - -// this struct is getting quite bloated... -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct NymNodeData { - #[serde(default)] - pub last_polled: OffsetDateTimeJsonSchemaWrapper, - - pub host_information: HostInformation, - - #[serde(default)] - pub declared_role: DeclaredRoles, - - #[serde(default)] - pub auxiliary_details: AuxiliaryDetails, - - // TODO: do we really care about ALL build info or just the version? - pub build_information: BinaryBuildInformationOwned, - - #[serde(default)] - pub network_requester: Option<NetworkRequesterDetails>, - - #[serde(default)] - pub ip_packet_router: Option<IpPacketRouterDetails>, - - #[serde(default)] - pub authenticator: Option<AuthenticatorDetails>, - - #[serde(default)] - pub wireguard: Option<WireguardDetails>, - - // for now we only care about their ws/wss situation, nothing more - pub mixnet_websockets: WebSockets, -} - -impl NymNodeData { - pub fn mix_port(&self) -> u16 { - self.auxiliary_details - .announce_ports - .mix_port - .unwrap_or(DEFAULT_MIX_LISTENING_PORT) - } - - pub fn verloc_port(&self) -> u16 { - self.auxiliary_details - .announce_ports - .verloc_port - .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct LegacyDescribedGateway { - pub bond: GatewayBond, - pub self_described: Option<NymNodeData>, -} - -impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway { - fn from(bond: LegacyGatewayBondWithId) -> Self { - LegacyDescribedGateway { - bond: bond.bond, - self_described: None, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct LegacyDescribedMixNode { - pub bond: LegacyMixNodeBondWithLayer, - pub self_described: Option<NymNodeData>, -} - -impl From<LegacyMixNodeBondWithLayer> for LegacyDescribedMixNode { - fn from(bond: LegacyMixNodeBondWithLayer) -> Self { - LegacyDescribedMixNode { - bond, - self_described: None, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct NetworkRequesterDetails { - /// address of the embedded network requester - pub address: String, - - /// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list - pub uses_exit_policy: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct IpPacketRouterDetails { - /// address of the embedded ip packet router - pub address: String, -} - -// works for current simple case. -impl From<IpPacketRouter> for IpPacketRouterDetails { - fn from(value: IpPacketRouter) -> Self { - IpPacketRouterDetails { - address: value.address, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct AuthenticatorDetails { - /// address of the embedded authenticator - pub address: String, -} - -// works for current simple case. -impl From<Authenticator> for AuthenticatorDetails { - fn from(value: Authenticator) -> Self { - AuthenticatorDetails { - address: value.address, - } - } -} -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct WireguardDetails { - pub port: u16, - pub public_key: String, -} - -// works for current simple case. -impl From<Wireguard> for WireguardDetails { - fn from(value: Wireguard) -> Self { - WireguardDetails { - port: value.port, - public_key: value.public_key, - } - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct ApiHealthResponse { - pub status: ApiStatus, - #[serde(default)] - pub chain_status: ChainStatus, - pub uptime: u64, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum ApiStatus { - Up, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, schemars::JsonSchema, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum ChainStatus { - Synced, - #[default] - Unknown, - Stalled { - #[serde( - serialize_with = "humantime_serde::serialize", - deserialize_with = "humantime_serde::deserialize" - )] - approximate_amount: Duration, - }, -} - -impl ApiHealthResponse { - pub fn new_healthy(uptime: Duration) -> Self { - ApiHealthResponse { - status: ApiStatus::Up, - chain_status: ChainStatus::Synced, - uptime: uptime.as_secs(), - } - } -} - -impl ApiStatus { - pub fn is_up(&self) -> bool { - matches!(self, ApiStatus::Up) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct SignerInformationResponse { - pub cosmos_address: String, - - pub identity: String, - - pub announce_address: String, - - pub verification_key: Option<String>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, Default, ToSchema)] -pub struct TestNode { - pub node_id: Option<u32>, - pub identity_key: Option<String>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct TestRoute { - pub gateway: TestNode, - pub layer1: TestNode, - pub layer2: TestNode, - pub layer3: TestNode, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct PartialTestResult { - pub monitor_run_id: i64, - pub timestamp: i64, - pub overall_reliability_for_all_routes_in_monitor_run: Option<u8>, - pub test_routes: TestRoute, -} - -pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>; -pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>; - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct NetworkMonitorRunDetailsResponse { - pub monitor_run_id: i64, - pub network_reliability: f64, - pub total_sent: usize, - pub total_received: usize, - - // integer score to number of nodes with that score - pub mixnode_results: BTreeMap<u8, usize>, - pub gateway_results: BTreeMap<u8, usize>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct NoiseDetails { - #[schemars(with = "String")] - #[serde(with = "bs58_x25519_pubkey")] - #[schema(value_type = String)] - pub x25119_pubkey: x25519::PublicKey, - - pub mixnet_port: u16, - - #[schema(value_type = Vec<String>)] - pub ip_addresses: Vec<IpAddr>, -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct NodeRefreshBody { - #[serde(with = "bs58_ed25519_pubkey")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub node_identity: ed25519::PublicKey, - - // a poor man's nonce - pub request_timestamp: i64, - - #[schemars(with = "PlaceholderJsonSchemaImpl")] - #[schema(value_type = String)] - pub signature: ed25519::Signature, -} - -impl NodeRefreshBody { - pub fn plaintext(node_identity: ed25519::PublicKey, request_timestamp: i64) -> Vec<u8> { - node_identity - .to_bytes() - .into_iter() - .chain(request_timestamp.to_be_bytes()) - .chain(b"describe-cache-refresh-request".iter().copied()) - .collect() - } - - pub fn new(private_key: &ed25519::PrivateKey) -> Self { - let node_identity = private_key.public_key(); - let request_timestamp = OffsetDateTime::now_utc().unix_timestamp(); - let signature = private_key.sign(Self::plaintext(node_identity, request_timestamp)); - NodeRefreshBody { - node_identity, - request_timestamp, - signature, - } - } - - pub fn verify_signature(&self) -> bool { - self.node_identity - .verify( - Self::plaintext(self.node_identity, self.request_timestamp), - &self.signature, - ) - .is_ok() - } - - pub fn is_stale(&self) -> bool { - let Ok(encoded) = OffsetDateTime::from_unix_timestamp(self.request_timestamp) else { - return true; - }; - let now = OffsetDateTime::now_utc(); - - if encoded > now { - return true; - } - - if (encoded + Duration::from_secs(30)) < now { - return true; - } - - false - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct RewardedSetResponse { - #[serde(default)] - #[schema(value_type = u32)] - pub epoch_id: EpochId, - - pub entry_gateways: Vec<NodeId>, - - pub exit_gateways: Vec<NodeId>, - - pub layer1: Vec<NodeId>, - - pub layer2: Vec<NodeId>, - - pub layer3: Vec<NodeId>, - - pub standby: Vec<NodeId>, -} - -impl From<RewardedSetResponse> for nym_mixnet_contract_common::EpochRewardedSet { - fn from(res: RewardedSetResponse) -> Self { - nym_mixnet_contract_common::EpochRewardedSet { - epoch_id: res.epoch_id, - assignment: nym_mixnet_contract_common::RewardedSet { - entry_gateways: res.entry_gateways, - exit_gateways: res.exit_gateways, - layer1: res.layer1, - layer2: res.layer2, - layer3: res.layer3, - standby: res.standby, - }, - } - } -} - -impl From<nym_mixnet_contract_common::EpochRewardedSet> for RewardedSetResponse { - fn from(r: nym_mixnet_contract_common::EpochRewardedSet) -> Self { - RewardedSetResponse { - epoch_id: r.epoch_id, - entry_gateways: r.assignment.entry_gateways, - exit_gateways: r.assignment.exit_gateways, - layer1: r.assignment.layer1, - layer2: r.assignment.layer2, - layer3: r.assignment.layer3, - standby: r.assignment.standby, - } - } -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct ChainStatusResponse { - pub connected_nyxd: String, - pub status: DetailedChainStatus, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct DetailedChainStatus { - pub abci: crate::models::tendermint_types::AbciInfo, - pub latest_block: BlockInfo, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct BlockInfo { - pub block_id: BlockId, - pub block: FullBlockInfo, - // if necessary we might put block data here later too -} - -impl From<tendermint_rpc::endpoint::block::Response> for BlockInfo { - fn from(value: tendermint_rpc::endpoint::block::Response) -> Self { - BlockInfo { - block_id: value.block_id.into(), - block: FullBlockInfo { - header: value.block.header.into(), - }, - } - } -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct FullBlockInfo { - pub header: BlockHeader, -} - -// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields -pub mod tendermint_types { - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - use tendermint::abci::response::Info; - use tendermint::block::header::Version; - use tendermint::{block, Hash}; - use utoipa::ToSchema; - - #[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] - pub struct AbciInfo { - /// Some arbitrary information. - pub data: String, - - /// The application software semantic version. - pub version: String, - - /// The application protocol version. - pub app_version: u64, - - /// The latest block for which the app has called [`Commit`]. - pub last_block_height: u64, - - /// The latest result of [`Commit`]. - pub last_block_app_hash: String, - } - - impl From<Info> for AbciInfo { - fn from(value: Info) -> Self { - AbciInfo { - data: value.data, - version: value.version, - app_version: value.app_version, - last_block_height: value.last_block_height.value(), - last_block_app_hash: value.last_block_app_hash.to_string(), - } - } - } - - /// `Version` contains the protocol version for the blockchain and the - /// application. - /// - /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version> - #[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, - )] - pub struct HeaderVersion { - /// Block version - pub block: u64, - - /// App version - pub app: u64, - } - - impl From<tendermint::block::header::Version> for HeaderVersion { - fn from(value: Version) -> Self { - HeaderVersion { - block: value.block, - app: value.app, - } - } - } - - /// Block identifiers which contain two distinct Merkle roots of the block, - /// as well as the number of parts in the block. - /// - /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid> - /// - /// Default implementation is an empty Id as defined by the Go implementation in - /// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>. - /// - /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). - /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. - /// See: <https://github.com/informalsystems/tendermint-rs/issues/663> - #[derive( - Serialize, - Deserialize, - Copy, - Clone, - Debug, - Default, - Hash, - Eq, - PartialEq, - PartialOrd, - Ord, - JsonSchema, - ToSchema, - )] - pub struct BlockId { - /// The block's main hash is the Merkle root of all the fields in the - /// block header. - #[schemars(with = "String")] - #[schema(value_type = String)] - pub hash: Hash, - - /// Parts header (if available) is used for secure gossipping of the block - /// during consensus. It is the Merkle root of the complete serialized block - /// cut into parts. - /// - /// PartSet is used to split a byteslice of data into parts (pieces) for - /// transmission. By splitting data into smaller parts and computing a - /// Merkle root hash on the list, you can verify that a part is - /// legitimately part of the complete data, and the part can be forwarded - /// to other peers before all the parts are known. In short, it's a fast - /// way to propagate a large file over a gossip network. - /// - /// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset> - /// - /// PartSetHeader in protobuf is defined as never nil using the gogoproto - /// annotations. This does not translate to Rust, but we can indicate this - /// in the domain type. - pub part_set_header: PartSetHeader, - } - - impl From<block::Id> for BlockId { - fn from(value: block::Id) -> Self { - BlockId { - hash: value.hash, - part_set_header: value.part_set_header.into(), - } - } - } - - /// Block parts header - #[derive( - Clone, - Copy, - Debug, - Default, - Hash, - Eq, - PartialEq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, - ToSchema, - )] - #[non_exhaustive] - pub struct PartSetHeader { - /// Number of parts in this block - pub total: u32, - - /// Hash of the parts set header, - #[schemars(with = "String")] - #[schema(value_type = String)] - pub hash: Hash, - } - - impl From<tendermint::block::parts::Header> for PartSetHeader { - fn from(value: block::parts::Header) -> Self { - PartSetHeader { - total: value.total, - hash: value.hash, - } - } - } - - /// Block `Header` values contain metadata about the block and about the - /// consensus, as well as commitments to the data in the current block, the - /// previous block, and the results returned by the application. - /// - /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header> - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] - pub struct BlockHeader { - /// Header version - pub version: HeaderVersion, - - /// Chain ID - pub chain_id: String, - - /// Current block height - pub height: u64, - - /// Current timestamp - #[schemars(with = "String")] - #[schema(value_type = String)] - pub time: tendermint::Time, - - /// Previous block info - pub last_block_id: Option<BlockId>, - - /// Commit from validators from the last block - #[schemars(with = "Option<String>")] - #[schema(value_type = Option<String>)] - pub last_commit_hash: Option<Hash>, - - /// Merkle root of transaction hashes - #[schemars(with = "Option<String>")] - #[schema(value_type = Option<String>)] - pub data_hash: Option<Hash>, - - /// Validators for the current block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub validators_hash: Hash, - - /// Validators for the next block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub next_validators_hash: Hash, - - /// Consensus params for the current block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub consensus_hash: Hash, - - /// State after txs from the previous block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub app_hash: Hash, - - /// Root hash of all results from the txs from the previous block - #[schemars(with = "Option<String>")] - #[schema(value_type = Option<String>)] - pub last_results_hash: Option<Hash>, - - /// Hash of evidence included in the block - #[schemars(with = "Option<String>")] - #[schema(value_type = Option<String>)] - pub evidence_hash: Option<Hash>, - - /// Original proposer of the block - #[serde(with = "nym_serde_helpers::hex")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub proposer_address: Vec<u8>, - } - - impl From<block::Header> for BlockHeader { - fn from(value: block::Header) -> Self { - BlockHeader { - version: value.version.into(), - chain_id: value.chain_id.to_string(), - height: value.height.value(), - time: value.time, - last_block_id: value.last_block_id.map(Into::into), - last_commit_hash: value.last_commit_hash, - data_hash: value.data_hash, - validators_hash: value.validators_hash, - next_validators_hash: value.next_validators_hash, - consensus_hash: value.consensus_hash, - app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), - last_results_hash: value.last_results_hash, - evidence_hash: value.evidence_hash, - proposer_address: value.proposer_address.as_bytes().to_vec(), - } - } - } -} - -use crate::models::tendermint_types::{BlockHeader, BlockId}; -pub use config_score::*; - -pub mod config_score { - use nym_contracts_common::NaiveFloat; - use serde::{Deserialize, Serialize}; - use std::cmp::Ordering; - use utoipa::ToSchema; - - #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] - pub struct ConfigScoreDataResponse { - pub parameters: ConfigScoreParams, - pub version_history: Vec<HistoricalNymNodeVersionEntry>, - } - - #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] - pub struct HistoricalNymNodeVersionEntry { - /// The unique, ordered, id of this particular entry - pub id: u32, - - /// Data associated with this particular version - pub version_information: HistoricalNymNodeVersion, - } - - impl PartialOrd for HistoricalNymNodeVersionEntry { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - // we only care about id for the purposes of ordering as they should have unique data - self.id.partial_cmp(&other.id) - } - } - - impl From<nym_mixnet_contract_common::HistoricalNymNodeVersionEntry> - for HistoricalNymNodeVersionEntry - { - fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersionEntry) -> Self { - HistoricalNymNodeVersionEntry { - id: value.id, - version_information: value.version_information.into(), - } - } - } - - #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] - pub struct HistoricalNymNodeVersion { - /// Version of the nym node that is going to be used for determining the version score of a node. - /// note: value stored here is pre-validated `semver::Version` - pub semver: String, - - /// Block height of when this version has been added to the contract - pub introduced_at_height: u64, - // for now ignore that field. it will give nothing useful to the users - // pub difference_since_genesis: TotalVersionDifference, - } - - impl From<nym_mixnet_contract_common::HistoricalNymNodeVersion> for HistoricalNymNodeVersion { - fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersion) -> Self { - HistoricalNymNodeVersion { - semver: value.semver, - introduced_at_height: value.introduced_at_height, - } - } - } - - #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] - pub struct ConfigScoreParams { - /// Defines weights for calculating numbers of versions behind the current release. - pub version_weights: OutdatedVersionWeights, - - /// Defines the parameters of the formula for calculating the version score - pub version_score_formula_params: VersionScoreFormulaParams, - } - - /// Defines weights for calculating numbers of versions behind the current release. - #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] - pub struct OutdatedVersionWeights { - pub major: u32, - pub minor: u32, - pub patch: u32, - pub prerelease: u32, - } - - /// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) - /// define the relevant parameters - #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] - pub struct VersionScoreFormulaParams { - pub penalty: f64, - pub penalty_scaling: f64, - } - - impl From<nym_mixnet_contract_common::ConfigScoreParams> for ConfigScoreParams { - fn from(value: nym_mixnet_contract_common::ConfigScoreParams) -> Self { - ConfigScoreParams { - version_weights: value.version_weights.into(), - version_score_formula_params: value.version_score_formula_params.into(), - } - } - } - - impl From<nym_mixnet_contract_common::OutdatedVersionWeights> for OutdatedVersionWeights { - fn from(value: nym_mixnet_contract_common::OutdatedVersionWeights) -> Self { - OutdatedVersionWeights { - major: value.major, - minor: value.minor, - patch: value.patch, - prerelease: value.prerelease, - } - } - } - - impl From<nym_mixnet_contract_common::VersionScoreFormulaParams> for VersionScoreFormulaParams { - fn from(value: nym_mixnet_contract_common::VersionScoreFormulaParams) -> Self { - VersionScoreFormulaParams { - penalty: value.penalty.naive_to_f64(), - penalty_scaling: value.penalty_scaling.naive_to_f64(), - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn offset_date_time_json_schema_wrapper_serde_backwards_compat() { - let mut dummy = OffsetDateTimeJsonSchemaWrapper::default(); - dummy.0 += Duration::from_millis(1); - let ser = serde_json::to_string(&dummy).unwrap(); - - assert_eq!("\"1970-01-01 00:00:00.001 +00:00:00\"", ser); - - let human_readable = "\"2024-05-23 07:41:02.756283766 +00:00:00\""; - let rfc3339 = "\"2002-10-02T15:00:00Z\""; - let rfc3339_offset = "\"2002-10-02T10:00:00-05:00\""; - - let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(human_readable).unwrap(); - assert_eq!(de.0.unix_timestamp(), 1716450062); - - let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339).unwrap(); - assert_eq!(de.0.unix_timestamp(), 1033570800); - - let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339_offset).unwrap(); - assert_eq!(de.0.unix_timestamp(), 1033570800); - - let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>("\"nonsense\"").unwrap(); - assert_eq!(de.0.unix_timestamp(), 0); - } -} diff --git a/nym-api/nym-api-requests/src/models/api_status.rs b/nym-api/nym-api-requests/src/models/api_status.rs new file mode 100644 index 0000000000..c10b9e3bae --- /dev/null +++ b/nym-api/nym-api-requests/src/models/api_status.rs @@ -0,0 +1,68 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use utoipa::ToSchema; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct ApiHealthResponse { + pub status: ApiStatus, + #[serde(default)] + pub chain_status: ChainStatus, + pub uptime: u64, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum ApiStatus { + Up, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ChainStatus { + Synced, + #[default] + Unknown, + Stalled { + #[serde( + serialize_with = "humantime_serde::serialize", + deserialize_with = "humantime_serde::deserialize" + )] + approximate_amount: Duration, + }, +} + +impl ChainStatus { + pub fn is_synced(&self) -> bool { + matches!(self, ChainStatus::Synced) + } +} + +impl ApiHealthResponse { + pub fn new_healthy(uptime: Duration) -> Self { + ApiHealthResponse { + status: ApiStatus::Up, + chain_status: ChainStatus::Synced, + uptime: uptime.as_secs(), + } + } +} + +impl ApiStatus { + pub fn is_up(&self) -> bool { + matches!(self, ApiStatus::Up) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct SignerInformationResponse { + pub cosmos_address: String, + + pub identity: String, + + pub announce_address: String, + + pub verification_key: Option<String>, +} diff --git a/nym-api/nym-api-requests/src/models/circulating_supply.rs b/nym-api/nym-api-requests/src/models/circulating_supply.rs new file mode 100644 index 0000000000..f191f0ccd2 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/circulating_supply.rs @@ -0,0 +1,19 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use super::CoinSchema; +use cosmwasm_std::Coin; +use serde::{Deserialize, Serialize}; +use utoipa::{ToResponse, ToSchema}; + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema, ToResponse)] +pub struct CirculatingSupplyResponse { + #[schema(value_type = CoinSchema)] + pub total_supply: Coin, + #[schema(value_type = CoinSchema)] + pub mixmining_reserve: Coin, + #[schema(value_type = CoinSchema)] + pub vesting_tokens: Coin, + #[schema(value_type = CoinSchema)] + pub circulating_supply: Coin, +} diff --git a/nym-api/nym-api-requests/src/models/described.rs b/nym-api/nym-api-requests/src/models/described.rs new file mode 100644 index 0000000000..33e82e1642 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/described.rs @@ -0,0 +1,391 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper}; +use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode}; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::NodeId; +use nym_network_defaults::{ + DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_PORT, +}; +use nym_node_requests::api::v1::authenticator::models::Authenticator; +use nym_node_requests::api::v1::gateway::models::Wireguard; +use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; +use nym_noise_keys::VersionedNoiseKey; +use serde::{Deserialize, Serialize}; +use std::net::IpAddr; +use tracing::warn; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct HostInformation { + #[schema(value_type = Vec<String>)] + pub ip_address: Vec<IpAddr>, + pub hostname: Option<String>, + pub keys: HostKeys, +} + +impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation { + fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self { + HostInformation { + ip_address: value.ip_address, + hostname: value.hostname, + keys: value.keys.into(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct HostKeys { + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub ed25519: ed25519::PublicKey, + + #[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")] + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub x25519: x25519::PublicKey, + + pub current_x25519_sphinx_key: SphinxKey, + + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option<SphinxKey>, + + #[serde(default)] + pub x25519_versioned_noise: Option<VersionedNoiseKey>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct SphinxKey { + pub rotation_id: u32, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub public_key: x25519::PublicKey, +} + +impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey { + fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self { + SphinxKey { + rotation_id: value.rotation_id, + public_key: value.public_key, + } + } +} + +impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys { + fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self { + HostKeys { + ed25519: value.ed25519_identity, + x25519: value.x25519_sphinx, + current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(), + pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into), + x25519_versioned_noise: value.x25519_versioned_noise, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct WebSockets { + pub ws_port: u16, + + pub wss_port: Option<u16>, +} + +impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets { + fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self { + WebSockets { + ws_port: value.ws_port, + wss_port: value.wss_port, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NoiseDetails { + pub key: VersionedNoiseKey, + + pub mixnet_port: u16, + + #[schema(value_type = Vec<String>)] + pub ip_addresses: Vec<IpAddr>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NymNodeDescription { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub contract_node_type: DescribedNodeType, + pub description: NymNodeData, +} + +impl NymNodeDescription { + pub fn version(&self) -> &str { + &self.description.build_information.build_version + } + + pub fn entry_information(&self) -> BasicEntryInformation { + BasicEntryInformation { + hostname: self.description.host_information.hostname.clone(), + ws_port: self.description.mixnet_websockets.ws_port, + wss_port: self.description.mixnet_websockets.wss_port, + } + } + + pub fn ed25519_identity_key(&self) -> ed25519::PublicKey { + self.description.host_information.keys.ed25519 + } + + pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey { + let keys = &self.description.host_information.keys; + + if keys.current_x25519_sphinx_key.rotation_id == u32::MAX { + // legacy case (i.e. node doesn't support rotation) + return keys.current_x25519_sphinx_key.public_key; + } + + if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id { + // it's the 'current' key + return keys.current_x25519_sphinx_key.public_key; + } + + if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key { + if pre_announced.rotation_id == current_rotation_id { + return pre_announced.public_key; + } + } + + warn!( + "unexpected key rotation {current_rotation_id} for node {}", + self.node_id + ); + // this should never be reached, but just in case, return the fallback option + keys.current_x25519_sphinx_key.public_key + } + + pub fn to_skimmed_node( + &self, + current_rotation_id: u32, + role: NodeRole, + performance: Performance, + ) -> SkimmedNode { + let keys = &self.description.host_information.keys; + let entry = if self.description.declared_role.entry { + Some(self.entry_information()) + } else { + None + }; + + SkimmedNode { + node_id: self.node_id, + ed25519_identity_pubkey: keys.ed25519, + ip_addresses: self.description.host_information.ip_address.clone(), + mix_port: self.description.mix_port(), + x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id), + // we can't use the declared roles, we have to take whatever was provided in the contract. + // why? say this node COULD operate as an exit, but it might be the case the contract decided + // to assign it an ENTRY role only. we have to use that one instead. + role, + supported_roles: self.description.declared_role, + entry, + performance, + } + } + + pub fn to_semi_skimmed_node( + &self, + current_rotation_id: u32, + role: NodeRole, + performance: Performance, + ) -> SemiSkimmedNode { + let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance); + + SemiSkimmedNode { + basic: skimmed_node, + x25519_noise_versioned_key: self + .description + .host_information + .keys + .x25519_versioned_noise, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts" + ) +)] +pub enum DescribedNodeType { + LegacyMixnode, + LegacyGateway, + NymNode, +} + +impl DescribedNodeType { + pub fn is_nym_node(&self) -> bool { + matches!(self, DescribedNodeType::NymNode) + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts" + ) +)] +pub struct DeclaredRoles { + pub mixnode: bool, + pub entry: bool, + pub exit_nr: bool, + pub exit_ipr: bool, +} + +impl DeclaredRoles { + pub fn can_operate_exit_gateway(&self) -> bool { + self.exit_ipr && self.exit_nr + } +} + +impl From<NodeRoles> for DeclaredRoles { + fn from(value: NodeRoles) -> Self { + DeclaredRoles { + mixnode: value.mixnode_enabled, + entry: value.gateway_enabled, + exit_nr: value.gateway_enabled && value.network_requester_enabled, + exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NetworkRequesterDetails { + /// address of the embedded network requester + pub address: String, + + /// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list + pub uses_exit_policy: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct IpPacketRouterDetails { + /// address of the embedded ip packet router + pub address: String, +} + +// works for current simple case. +impl From<IpPacketRouter> for IpPacketRouterDetails { + fn from(value: IpPacketRouter) -> Self { + IpPacketRouterDetails { + address: value.address, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct AuthenticatorDetails { + /// address of the embedded authenticator + pub address: String, +} + +// works for current simple case. +impl From<Authenticator> for AuthenticatorDetails { + fn from(value: Authenticator) -> Self { + AuthenticatorDetails { + address: value.address, + } + } +} +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct WireguardDetails { + // NOTE: the port field is deprecated in favour of tunnel_port + pub port: u16, + #[serde(default = "default_tunnel_port")] + pub tunnel_port: u16, + #[serde(default = "default_metadata_port")] + pub metadata_port: u16, + pub public_key: String, +} + +fn default_tunnel_port() -> u16 { + WG_TUNNEL_PORT +} +fn default_metadata_port() -> u16 { + WG_METADATA_PORT +} + +// works for current simple case. +impl From<Wireguard> for WireguardDetails { + fn from(value: Wireguard) -> Self { + WireguardDetails { + port: value.port, + tunnel_port: value.tunnel_port, + metadata_port: value.metadata_port, + public_key: value.public_key, + } + } +} + +// this struct is getting quite bloated... +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NymNodeData { + #[serde(default)] + pub last_polled: OffsetDateTimeJsonSchemaWrapper, + + pub host_information: HostInformation, + + #[serde(default)] + pub declared_role: DeclaredRoles, + + #[serde(default)] + pub auxiliary_details: AuxiliaryDetails, + + // TODO: do we really care about ALL build info or just the version? + pub build_information: BinaryBuildInformationOwned, + + #[serde(default)] + pub network_requester: Option<NetworkRequesterDetails>, + + #[serde(default)] + pub ip_packet_router: Option<IpPacketRouterDetails>, + + #[serde(default)] + pub authenticator: Option<AuthenticatorDetails>, + + #[serde(default)] + pub wireguard: Option<WireguardDetails>, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSockets, +} + +impl NymNodeData { + pub fn mix_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .mix_port + .unwrap_or(DEFAULT_MIX_LISTENING_PORT) + } + + pub fn verloc_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .verloc_port + .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT) + } +} diff --git a/nym-api/nym-api-requests/src/models/legacy.rs b/nym-api/nym-api-requests/src/models/legacy.rs new file mode 100644 index 0000000000..c6cb6b2a78 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/legacy.rs @@ -0,0 +1,364 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use super::{CoinSchema, DeclaredRoles}; +use crate::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use crate::models::{NodePerformance, NymNodeData, StakeSaturation}; +use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode}; +use cosmwasm_std::{Addr, Coin, Decimal}; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::rewarding::RewardEstimate; +use nym_mixnet_contract_common::{GatewayBond, Interval, MixNode, NodeId, RewardingParams}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::net::IpAddr; +use std::time::Duration; +use thiserror::Error; +use utoipa::{IntoParams, ToSchema}; + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct LegacyDescribedGateway { + pub bond: GatewayBond, + pub self_described: Option<NymNodeData>, +} + +impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway { + fn from(bond: LegacyGatewayBondWithId) -> Self { + LegacyDescribedGateway { + bond: bond.bond, + self_described: None, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct LegacyDescribedMixNode { + pub bond: LegacyMixNodeBondWithLayer, + pub self_described: Option<NymNodeData>, +} + +impl From<LegacyMixNodeBondWithLayer> for LegacyDescribedMixNode { + fn from(bond: LegacyMixNodeBondWithLayer) -> Self { + LegacyDescribedMixNode { + bond, + self_described: None, + } + } +} + +#[deprecated] +#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] +pub struct InclusionProbability { + #[schema(value_type = u32)] + pub mix_id: NodeId, + pub in_active: f64, + pub in_reserve: f64, +} + +#[deprecated] +#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] +pub struct AllInclusionProbabilitiesResponse { + pub inclusion_probabilities: Vec<InclusionProbability>, + pub samples: u64, + pub elapsed: Duration, + pub delta_max: f64, + pub delta_l2: f64, + pub as_at: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/SelectionChance.ts" + ) +)] +#[deprecated] +pub enum SelectionChance { + High, + Good, + Low, +} + +impl From<f64> for SelectionChance { + fn from(p: f64) -> SelectionChance { + match p { + p if p >= 0.7 => SelectionChance::High, + p if p >= 0.3 => SelectionChance::Good, + _ => SelectionChance::Low, + } + } +} + +impl From<Decimal> for SelectionChance { + fn from(p: Decimal) -> Self { + match p { + p if p >= Decimal::from_ratio(70u32, 100u32) => SelectionChance::High, + p if p >= Decimal::from_ratio(30u32, 100u32) => SelectionChance::Good, + _ => SelectionChance::Low, + } + } +} + +impl fmt::Display for SelectionChance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SelectionChance::High => write!(f, "High"), + SelectionChance::Good => write!(f, "Good"), + SelectionChance::Low => write!(f, "Low"), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts" + ) +)] +#[deprecated] +pub struct InclusionProbabilityResponse { + pub in_active: SelectionChance, + pub in_reserve: SelectionChance, +} + +impl fmt::Display for InclusionProbabilityResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "in_active: {}, in_reserve: {}", + self.in_active, self.in_reserve + ) + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)] +pub struct ComputeRewardEstParam { + #[schema(value_type = Option<String>)] + #[param(value_type = Option<String>)] + pub performance: Option<Performance>, + pub active_in_rewarded_set: Option<bool>, + pub pledge_amount: Option<u64>, + pub total_delegation: Option<u64>, + #[schema(value_type = Option<CoinSchema>)] + #[param(value_type = Option<CoinSchema>)] + pub interval_operating_cost: Option<Coin>, + #[schema(value_type = Option<String>)] + #[param(value_type = Option<String>)] + pub profit_margin_percent: Option<Percent>, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts" + ) +)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct RewardEstimationResponse { + pub estimation: RewardEstimate, + pub reward_params: RewardingParams, + pub epoch: Interval, + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] + pub as_at: i64, +} + +impl MixNodeBondAnnotated { + pub fn mix_node(&self) -> &MixNode { + &self.mixnode_details.bond_information.mix_node + } + + pub fn mix_id(&self) -> NodeId { + self.mixnode_details.mix_id() + } + + pub fn identity_key(&self) -> &str { + self.mixnode_details.bond_information.identity() + } + + pub fn owner(&self) -> &Addr { + self.mixnode_details.bond_information.owner() + } + + pub fn version(&self) -> &str { + &self.mixnode_details.bond_information.mix_node.version + } + + pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> { + Ok(SkimmedNode { + node_id: self.mix_id(), + ed25519_identity_pubkey: self + .identity_key() + .parse() + .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, + ip_addresses: self.ip_addresses.clone(), + mix_port: self.mix_node().mix_port, + x25519_sphinx_pubkey: self + .mix_node() + .sphinx_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, + role, + supported_roles: DeclaredRoles { + mixnode: true, + entry: false, + exit_nr: false, + exit_ipr: false, + }, + entry: None, + performance: self.node_performance.last_24h, + }) + } + + pub fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result<SemiSkimmedNode, MalformedNodeBond> { + let skimmed_node = self.try_to_skimmed_node(role)?; + Ok(SemiSkimmedNode { + basic: skimmed_node, + x25519_noise_versioned_key: None, // legacy node won't ever support Noise + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct GatewayBondAnnotated { + pub gateway_bond: LegacyGatewayBondWithId, + + #[serde(default)] + pub self_described: Option<GatewayDescription>, + + // NOTE: the performance field is deprecated in favour of node_performance + #[schema(value_type = String)] + pub performance: Performance, + pub node_performance: NodePerformance, + pub blacklisted: bool, + + #[serde(default)] + #[schema(value_type = Vec<String>)] + pub ip_addresses: Vec<IpAddr>, +} + +impl GatewayBondAnnotated { + pub fn version(&self) -> &str { + &self.gateway_bond.gateway.version + } + + pub fn identity(&self) -> &String { + self.gateway_bond.bond.identity() + } + + pub fn owner(&self) -> &Addr { + self.gateway_bond.bond.owner() + } + + pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> { + Ok(SkimmedNode { + node_id: self.gateway_bond.node_id, + ip_addresses: self.ip_addresses.clone(), + ed25519_identity_pubkey: self + .gateway_bond + .gateway + .identity_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, + mix_port: self.gateway_bond.bond.gateway.mix_port, + x25519_sphinx_pubkey: self + .gateway_bond + .gateway + .sphinx_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, + role, + supported_roles: DeclaredRoles { + mixnode: false, + entry: true, + exit_nr: false, + exit_ipr: false, + }, + entry: Some(BasicEntryInformation { + hostname: None, + ws_port: self.gateway_bond.bond.gateway.clients_port, + wss_port: None, + }), + performance: self.node_performance.last_24h, + }) + } + + pub fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result<SemiSkimmedNode, MalformedNodeBond> { + let skimmed_node = self.try_to_skimmed_node(role)?; + Ok(SemiSkimmedNode { + basic: skimmed_node, + x25519_noise_versioned_key: None, // legacy node won't ever support Noise + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct GatewayDescription { + // for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly) +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[schema(title = "LegacyMixNodeDetailsWithLayer")] +pub struct LegacyMixNodeDetailsWithLayerSchema { + /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + #[schema(example = "unimplemented schema")] + pub bond_information: String, + + /// Details used for computation of rewarding related data. + #[schema(example = "unimplemented schema")] + pub rewarding_details: String, + + /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[schema(example = "unimplemented schema")] + pub pending_changes: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct MixNodeBondAnnotated { + #[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)] + pub mixnode_details: LegacyMixNodeDetailsWithLayer, + #[schema(value_type = String)] + pub stake_saturation: StakeSaturation, + #[schema(value_type = String)] + pub uncapped_stake_saturation: StakeSaturation, + // NOTE: the performance field is deprecated in favour of node_performance + #[schema(value_type = String)] + pub performance: Performance, + pub node_performance: NodePerformance, + #[schema(value_type = String)] + pub estimated_operator_apy: Decimal, + #[schema(value_type = String)] + pub estimated_delegators_apy: Decimal, + pub blacklisted: bool, + + // a rather temporary thing until we query self-described endpoints of mixnodes + #[serde(default)] + #[schema(value_type = Vec<String>)] + pub ip_addresses: Vec<IpAddr>, +} + +#[derive(Debug, Error)] +pub enum MalformedNodeBond { + #[error("the associated ed25519 identity key is malformed")] + InvalidEd25519Key, + + #[error("the associated x25519 sphinx key is malformed")] + InvalidX25519Key, +} diff --git a/nym-api/nym-api-requests/src/models/mixnet.rs b/nym-api/nym-api-requests/src/models/mixnet.rs new file mode 100644 index 0000000000..42f8bf1a72 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/mixnet.rs @@ -0,0 +1,242 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState, NodeId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::warn; +use utoipa::ToSchema; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct KeyRotationInfoResponse { + #[serde(flatten)] + pub details: KeyRotationDetails, + + // helper field that holds calculated data based on the `details` field + // this is to expose the information in a format more easily accessible by humans + // without having to do any calculations + pub progress: KeyRotationProgressInfo, +} + +impl From<KeyRotationDetails> for KeyRotationInfoResponse { + fn from(details: KeyRotationDetails) -> Self { + KeyRotationInfoResponse { + details, + progress: KeyRotationProgressInfo { + current_key_rotation_id: details.current_key_rotation_id(), + current_rotation_starting_epoch: details.current_rotation_starting_epoch_id(), + current_rotation_ending_epoch: details.current_rotation_starting_epoch_id() + + details.key_rotation_state.validity_epochs + - 1, + }, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct KeyRotationProgressInfo { + pub current_key_rotation_id: u32, + + pub current_rotation_starting_epoch: u32, + + pub current_rotation_ending_epoch: u32, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct KeyRotationDetails { + pub key_rotation_state: KeyRotationState, + + #[schema(value_type = u32)] + pub current_absolute_epoch_id: EpochId, + + #[serde(with = "time::serde::rfc3339")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub current_epoch_start: OffsetDateTime, + + pub epoch_duration: Duration, +} + +impl KeyRotationDetails { + pub fn current_key_rotation_id(&self) -> u32 { + self.key_rotation_state + .key_rotation_id(self.current_absolute_epoch_id) + } + + pub fn next_rotation_starting_epoch_id(&self) -> EpochId { + self.key_rotation_state + .next_rotation_starting_epoch_id(self.current_absolute_epoch_id) + } + + pub fn current_rotation_starting_epoch_id(&self) -> EpochId { + self.key_rotation_state + .current_rotation_starting_epoch_id(self.current_absolute_epoch_id) + } + + fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 { + let elapsed = (now - self.current_epoch_start).as_seconds_f32(); + elapsed / self.epoch_duration.as_secs_f32() + } + + pub fn is_epoch_stuck(&self) -> bool { + let now = OffsetDateTime::now_utc(); + let progress = self.current_epoch_progress(now); + if progress > 1. { + let into_next = 1. - progress; + // if epoch hasn't progressed for more than 20% of its duration, mark is as stuck + if into_next > 0.2 { + let diff_time = + Duration::from_secs_f32(into_next * self.epoch_duration.as_secs_f32()); + let expected_epoch_end = self.current_epoch_start + self.epoch_duration; + warn!("the current epoch is expected to have been over by {expected_epoch_end}. it's already {} overdue!", humantime_serde::re::humantime::format_duration(diff_time)); + return true; + } + } + + false + } + + // based on the current **TIME**, determine what's the expected current rotation id + pub fn expected_current_rotation_id(&self) -> KeyRotationId { + let now = OffsetDateTime::now_utc(); + let current_end = now + self.epoch_duration; + if now < current_end { + return self + .key_rotation_state + .key_rotation_id(self.current_absolute_epoch_id); + } + + let diff = now - current_end; + let passed_epochs = diff / self.epoch_duration; + let expected_current_epoch = self.current_absolute_epoch_id + passed_epochs.floor() as u32; + + self.key_rotation_state + .key_rotation_id(expected_current_epoch) + } + + pub fn until_next_rotation(&self) -> Option<Duration> { + let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc()); + if current_epoch_progress > 1. { + return None; + } + + let next_rotation_epoch = self.next_rotation_starting_epoch_id(); + let full_remaining = + (next_rotation_epoch - self.current_absolute_epoch_id).checked_add(1)?; + + let epochs_until_next_rotation = (1. - current_epoch_progress) + full_remaining as f32; + + Some(Duration::from_secs_f32( + epochs_until_next_rotation * self.epoch_duration.as_secs_f32(), + )) + } + + pub fn epoch_start_time(&self, absolute_epoch_id: EpochId) -> OffsetDateTime { + match absolute_epoch_id.cmp(&self.current_absolute_epoch_id) { + Ordering::Less => { + let diff = self.current_absolute_epoch_id - absolute_epoch_id; + self.current_epoch_start - diff * self.epoch_duration + } + Ordering::Equal => self.current_epoch_start, + Ordering::Greater => { + let diff = absolute_epoch_id - self.current_absolute_epoch_id; + self.current_epoch_start + diff * self.epoch_duration + } + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct RewardedSetResponse { + #[serde(default)] + #[schema(value_type = u32)] + pub epoch_id: EpochId, + + pub entry_gateways: Vec<NodeId>, + + pub exit_gateways: Vec<NodeId>, + + pub layer1: Vec<NodeId>, + + pub layer2: Vec<NodeId>, + + pub layer3: Vec<NodeId>, + + pub standby: Vec<NodeId>, +} + +impl From<RewardedSetResponse> for nym_mixnet_contract_common::EpochRewardedSet { + fn from(res: RewardedSetResponse) -> Self { + nym_mixnet_contract_common::EpochRewardedSet { + epoch_id: res.epoch_id, + assignment: nym_mixnet_contract_common::RewardedSet { + entry_gateways: res.entry_gateways, + exit_gateways: res.exit_gateways, + layer1: res.layer1, + layer2: res.layer2, + layer3: res.layer3, + standby: res.standby, + }, + } + } +} + +impl From<nym_mixnet_contract_common::EpochRewardedSet> for RewardedSetResponse { + fn from(r: nym_mixnet_contract_common::EpochRewardedSet) -> Self { + RewardedSetResponse { + epoch_id: r.epoch_id, + entry_gateways: r.assignment.entry_gateways, + exit_gateways: r.assignment.exit_gateways, + layer1: r.assignment.layer1, + layer2: r.assignment.layer2, + layer3: r.assignment.layer3, + standby: r.assignment.standby, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema, ToSchema)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts") +)] +pub enum DisplayRole { + EntryGateway, + Layer1, + Layer2, + Layer3, + ExitGateway, + Standby, +} + +impl From<Role> for DisplayRole { + fn from(role: Role) -> Self { + match role { + Role::EntryGateway => DisplayRole::EntryGateway, + Role::Layer1 => DisplayRole::Layer1, + Role::Layer2 => DisplayRole::Layer2, + Role::Layer3 => DisplayRole::Layer3, + Role::ExitGateway => DisplayRole::ExitGateway, + Role::Standby => DisplayRole::Standby, + } + } +} + +impl From<DisplayRole> for Role { + fn from(role: DisplayRole) -> Self { + match role { + DisplayRole::EntryGateway => Role::EntryGateway, + DisplayRole::Layer1 => Role::Layer1, + DisplayRole::Layer2 => Role::Layer2, + DisplayRole::Layer3 => Role::Layer3, + DisplayRole::ExitGateway => Role::ExitGateway, + DisplayRole::Standby => Role::Standby, + } + } +} diff --git a/nym-api/nym-api-requests/src/models/mod.rs b/nym-api/nym-api-requests/src/models/mod.rs new file mode 100644 index 0000000000..6d7ab23f96 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/mod.rs @@ -0,0 +1,63 @@ +// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +#![allow(deprecated)] + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::{Display, Formatter}; + +pub mod api_status; +pub mod circulating_supply; +pub mod described; +pub mod legacy; +pub mod mixnet; +pub mod network; +pub mod network_monitor; +pub mod node_status; +pub mod schema_helpers; + +// don't break existing imports +pub use api_status::*; +pub use circulating_supply::*; +pub use described::*; +pub use legacy::*; +pub use mixnet::*; +pub use network::*; +pub use network_monitor::*; +pub use node_status::*; +pub use schema_helpers::*; + +pub use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState}; +pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; +pub use nym_noise_keys::VersionedNoiseKey; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct RequestError { + message: String, +} + +impl RequestError { + pub fn new<S: Into<String>>(msg: S) -> Self { + RequestError { + message: msg.into(), + } + } + + pub fn message(&self) -> &str { + &self.message + } + + pub fn empty() -> Self { + Self { + message: String::new(), + } + } +} + +impl Display for RequestError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.message, f) + } +} diff --git a/nym-api/nym-api-requests/src/models/network.rs b/nym-api/nym-api-requests/src/models/network.rs new file mode 100644 index 0000000000..e998656c67 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/network.rs @@ -0,0 +1,562 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::ecash::models::EcashSignerStatusResponse; +use crate::models::tendermint_types::{BlockHeader, BlockId}; +use crate::models::{ChainStatus, SignerInformationResponse}; +use crate::signable::SignedMessage; +use nym_coconut_dkg_common::types::EpochId; +use nym_crypto::asymmetric::ed25519::PublicKey; +use nym_ecash_signer_check_types::helper_traits::{ + ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse, TimestampedResponse, + Verifiable, +}; +use nym_ecash_signer_check_types::status::SignerResult; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::OffsetDateTime; +use utoipa::ToSchema; + +pub type ChainBlocksStatusResponse = SignedMessage<ChainBlocksStatusResponseBody>; +pub type SignersStatusResponse = SignedMessage<SignersStatusResponseBody>; +pub type DetailedSignersStatusResponse = SignedMessage<DetailedSignersStatusResponseBody>; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SignersStatusResponseBody { + #[serde(with = "time::serde::rfc3339")] + #[schema(value_type = String)] + pub as_at: OffsetDateTime, + + pub overview: SignersStatusOverview, + + pub results: Vec<MinimalSignerResult>, +} + +pub type TypedSignerResult = SignerResult< + SignerInformationResponse, + EcashSignerStatusResponse, + ChainStatusResponse, + ChainBlocksStatusResponse, +>; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MinimalSignerResult { + pub announce_address: String, + pub owner_address: String, + pub node_index: u64, + pub public_key: String, + + pub local_chain_working: bool, + pub credential_issuance_available: bool, +} + +impl From<&TypedSignerResult> for MinimalSignerResult { + fn from(result: &TypedSignerResult) -> MinimalSignerResult { + MinimalSignerResult { + announce_address: result.information.announce_address.clone(), + owner_address: result.information.owner_address.clone(), + node_index: result.information.node_index, + public_key: result.information.public_key.clone(), + local_chain_working: result.chain_available(), + credential_issuance_available: result.signing_available(), + } + } +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DetailedSignersStatusResponseBody { + #[serde(with = "time::serde::rfc3339")] + #[schema(value_type = String)] + pub as_at: OffsetDateTime, + + pub overview: SignersStatusOverview, + + pub details: Vec<TypedSignerResult>, +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SignersStatusOverview { + #[schema(value_type = Option<u64>)] + pub epoch_id: Option<EpochId>, + + pub signing_threshold: Option<u64>, + pub threshold_available: Option<bool>, + + pub total_signers: usize, + pub unreachable_signers: usize, + pub malformed_signers: usize, + + // unreachable or outdated + pub unknown_local_chain_status: usize, + pub working_local_chain: usize, + + // i.e. provided signature + pub provably_stalled_local_chain: usize, + pub unprovably_stalled_local_chain: usize, + + // unreachable or outdated + pub unknown_credential_issuance_status: usize, + pub working_credential_issuance: usize, + + // i.e. provided signature + pub provably_unavailable_credential_issuance: usize, + pub unprovably_unavailable_credential_issuance: usize, +} + +impl SignersStatusOverview { + pub fn new(results: &[TypedSignerResult], signing_threshold: Option<u64>) -> Self { + let epoch_id = results.first().map(|r| r.dkg_epoch_id); + + let mut unreachable_signers = 0; + let mut malformed_signers = 0; + let mut unknown_local_chain_status = 0; + let mut working_local_chain = 0; + let mut provably_stalled_local_chain = 0; + let mut unprovably_stalled_local_chain = 0; + let mut unknown_credential_issuance_status = 0; + let mut working_credential_issuance = 0; + let mut provably_unavailable_credential_issuance = 0; + let mut unprovably_unavailable_credential_issuance = 0; + + for result in results { + if result.signer_unreachable() { + unreachable_signers += 1; + } + if result.malformed_details() { + malformed_signers += 1; + } + + if result.unknown_chain_status() { + unknown_local_chain_status += 1; + } + if result.chain_available() { + working_local_chain += 1; + } + if result.chain_provably_stalled() { + provably_stalled_local_chain += 1; + } + if result.chain_unprovably_stalled() { + unprovably_stalled_local_chain += 1; + } + + if result.unknown_signing_status() { + unknown_credential_issuance_status += 1; + } + if result.signing_available() { + working_credential_issuance += 1; + } + if result.signing_provably_unavailable() { + provably_unavailable_credential_issuance += 1; + } + if result.signing_unprovably_unavailable() { + unprovably_unavailable_credential_issuance += 1; + } + } + + SignersStatusOverview { + epoch_id, + signing_threshold, + threshold_available: signing_threshold.map(|threshold| { + (working_local_chain as u64) >= threshold + && (working_credential_issuance as u64) >= threshold + }), + total_signers: results.len(), + unreachable_signers, + malformed_signers, + unknown_local_chain_status, + working_local_chain, + provably_stalled_local_chain, + unprovably_stalled_local_chain, + unknown_credential_issuance_status, + working_credential_issuance, + provably_unavailable_credential_issuance, + unprovably_unavailable_credential_issuance, + } + } +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ChainBlocksStatusResponseBody { + #[serde(with = "time::serde::rfc3339")] + #[schema(value_type = String)] + pub current_time: OffsetDateTime, + + pub latest_cached_block: Option<DetailedChainStatus>, + + // explicit indication of THIS signer whether it thinks the chain is stalled + pub chain_status: ChainStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct ChainStatusResponse { + pub connected_nyxd: String, + pub status: DetailedChainStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct DetailedChainStatus { + pub abci: crate::models::tendermint_types::AbciInfo, + pub latest_block: BlockInfo, +} + +impl DetailedChainStatus { + pub fn stall_status(&self, now: OffsetDateTime, threshold: Duration) -> ChainStatus { + let block_time: OffsetDateTime = self.latest_block.block.header.time.into(); + let diff = now - block_time; + if diff > threshold { + ChainStatus::Stalled { + approximate_amount: diff.unsigned_abs(), + } + } else { + ChainStatus::Synced + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct BlockInfo { + pub block_id: BlockId, + pub block: FullBlockInfo, + // if necessary we might put block data here later too +} + +impl From<tendermint_rpc::endpoint::block::Response> for BlockInfo { + fn from(value: tendermint_rpc::endpoint::block::Response) -> Self { + BlockInfo { + block_id: value.block_id.into(), + block: FullBlockInfo { + header: value.block.header.into(), + }, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct FullBlockInfo { + pub header: BlockHeader, +} + +// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields +pub mod tendermint_types { + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use tendermint::abci::response::Info; + use tendermint::block::header::Version; + use tendermint::{block, Hash}; + use utoipa::ToSchema; + + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct AbciInfo { + /// Some arbitrary information. + pub data: String, + + /// The application software semantic version. + pub version: String, + + /// The application protocol version. + pub app_version: u64, + + /// The latest block for which the app has called [`Commit`]. + pub last_block_height: u64, + + /// The latest result of [`Commit`]. + pub last_block_app_hash: String, + } + + impl From<Info> for AbciInfo { + fn from(value: Info) -> Self { + AbciInfo { + data: value.data, + version: value.version, + app_version: value.app_version, + last_block_height: value.last_block_height.value(), + last_block_app_hash: value.last_block_app_hash.to_string(), + } + } + } + + /// `Version` contains the protocol version for the blockchain and the + /// application. + /// + /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version> + #[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, + )] + pub struct HeaderVersion { + /// Block version + pub block: u64, + + /// App version + pub app: u64, + } + + impl From<tendermint::block::header::Version> for HeaderVersion { + fn from(value: Version) -> Self { + HeaderVersion { + block: value.block, + app: value.app, + } + } + } + + /// Block identifiers which contain two distinct Merkle roots of the block, + /// as well as the number of parts in the block. + /// + /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid> + /// + /// Default implementation is an empty Id as defined by the Go implementation in + /// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>. + /// + /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). + /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. + /// See: <https://github.com/informalsystems/tendermint-rs/issues/663> + #[derive( + Serialize, + Deserialize, + Copy, + Clone, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + JsonSchema, + ToSchema, + )] + pub struct BlockId { + /// The block's main hash is the Merkle root of all the fields in the + /// block header. + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + + /// Parts header (if available) is used for secure gossipping of the block + /// during consensus. It is the Merkle root of the complete serialized block + /// cut into parts. + /// + /// PartSet is used to split a byteslice of data into parts (pieces) for + /// transmission. By splitting data into smaller parts and computing a + /// Merkle root hash on the list, you can verify that a part is + /// legitimately part of the complete data, and the part can be forwarded + /// to other peers before all the parts are known. In short, it's a fast + /// way to propagate a large file over a gossip network. + /// + /// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset> + /// + /// PartSetHeader in protobuf is defined as never nil using the gogoproto + /// annotations. This does not translate to Rust, but we can indicate this + /// in the domain type. + pub part_set_header: PartSetHeader, + } + + impl From<block::Id> for BlockId { + fn from(value: block::Id) -> Self { + BlockId { + hash: value.hash, + part_set_header: value.part_set_header.into(), + } + } + } + + /// Block parts header + #[derive( + Clone, + Copy, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, + ToSchema, + )] + #[non_exhaustive] + pub struct PartSetHeader { + /// Number of parts in this block + pub total: u32, + + /// Hash of the parts set header, + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + } + + impl From<tendermint::block::parts::Header> for PartSetHeader { + fn from(value: block::parts::Header) -> Self { + PartSetHeader { + total: value.total, + hash: value.hash, + } + } + } + + /// Block `Header` values contain metadata about the block and about the + /// consensus, as well as commitments to the data in the current block, the + /// previous block, and the results returned by the application. + /// + /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header> + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct BlockHeader { + /// Header version + pub version: HeaderVersion, + + /// Chain ID + pub chain_id: String, + + /// Current block height + pub height: u64, + + /// Current timestamp + #[schemars(with = "String")] + #[schema(value_type = String)] + pub time: tendermint::Time, + + /// Previous block info + pub last_block_id: Option<BlockId>, + + /// Commit from validators from the last block + #[schemars(with = "Option<String>")] + #[schema(value_type = Option<String>)] + pub last_commit_hash: Option<Hash>, + + /// Merkle root of transaction hashes + #[schemars(with = "Option<String>")] + #[schema(value_type = Option<String>)] + pub data_hash: Option<Hash>, + + /// Validators for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub validators_hash: Hash, + + /// Validators for the next block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub next_validators_hash: Hash, + + /// Consensus params for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub consensus_hash: Hash, + + /// State after txs from the previous block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub app_hash: Hash, + + /// Root hash of all results from the txs from the previous block + #[schemars(with = "Option<String>")] + #[schema(value_type = Option<String>)] + pub last_results_hash: Option<Hash>, + + /// Hash of evidence included in the block + #[schemars(with = "Option<String>")] + #[schema(value_type = Option<String>)] + pub evidence_hash: Option<Hash>, + + /// Original proposer of the block + #[serde(with = "nym_serde_helpers::hex")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub proposer_address: Vec<u8>, + } + + impl From<block::Header> for BlockHeader { + fn from(value: block::Header) -> Self { + BlockHeader { + version: value.version.into(), + chain_id: value.chain_id.to_string(), + height: value.height.value(), + time: value.time, + last_block_id: value.last_block_id.map(Into::into), + last_commit_hash: value.last_commit_hash, + data_hash: value.data_hash, + validators_hash: value.validators_hash, + next_validators_hash: value.next_validators_hash, + consensus_hash: value.consensus_hash, + app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), + last_results_hash: value.last_results_hash, + evidence_hash: value.evidence_hash, + proposer_address: value.proposer_address.as_bytes().to_vec(), + } + } + } +} + +// implement required traits for the signer responses + +impl LegacyChainResponse for ChainStatusResponse { + fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool { + self.status.stall_status(now, stall_threshold).is_synced() + } +} + +impl Verifiable for ChainBlocksStatusResponse { + fn verify_signature(&self, pub_key: &PublicKey) -> bool { + self.verify_signature(pub_key) + } +} + +impl TimestampedResponse for ChainBlocksStatusResponse { + fn timestamp(&self) -> OffsetDateTime { + self.body.current_time + } +} + +impl ChainResponse for ChainBlocksStatusResponse { + fn chain_synced(&self) -> bool { + self.body.chain_status.is_synced() + } +} + +impl LegacySignerResponse for SignerInformationResponse { + fn signer_identity(&self) -> &str { + &self.identity + } + + fn signer_verification_key(&self) -> &Option<String> { + &self.verification_key + } +} + +impl Verifiable for EcashSignerStatusResponse { + fn verify_signature(&self, pub_key: &PublicKey) -> bool { + self.verify_signature(pub_key) + } +} + +impl TimestampedResponse for EcashSignerStatusResponse { + fn timestamp(&self) -> OffsetDateTime { + self.body.current_time + } +} + +impl SignerResponse for EcashSignerStatusResponse { + fn has_signing_keys(&self) -> bool { + self.body.has_signing_keys + } + + fn signer_disabled(&self) -> bool { + self.body.signer_disabled + } + + fn is_ecash_signer(&self) -> bool { + self.body.is_ecash_signer + } + + fn dkg_ecash_epoch_id(&self) -> EpochId { + self.body.dkg_ecash_epoch_id + } +} diff --git a/nym-api/nym-api-requests/src/models/network_monitor.rs b/nym-api/nym-api-requests/src/models/network_monitor.rs new file mode 100644 index 0000000000..c253bd28ca --- /dev/null +++ b/nym-api/nym-api-requests/src/models/network_monitor.rs @@ -0,0 +1,74 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::pagination::PaginatedResponse; +use nym_mixnet_contract_common::NodeId; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, Default, ToSchema)] +pub struct TestNode { + pub node_id: Option<u32>, + pub identity_key: Option<String>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct TestRoute { + pub gateway: TestNode, + pub layer1: TestNode, + pub layer2: TestNode, + pub layer3: TestNode, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct PartialTestResult { + pub monitor_run_id: i64, + pub timestamp: i64, + pub overall_reliability_for_all_routes_in_monitor_run: Option<u8>, + pub test_routes: TestRoute, +} + +pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>; +pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>; + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NetworkMonitorRunDetailsResponse { + pub monitor_run_id: i64, + pub network_reliability: f64, + pub total_sent: usize, + pub total_received: usize, + + // integer score to number of nodes with that score + pub mixnode_results: BTreeMap<u8, usize>, + pub gateway_results: BTreeMap<u8, usize>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts" + ) +)] +pub struct MixnodeCoreStatusResponse { + pub mix_id: NodeId, + pub count: i64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts" + ) +)] +pub struct GatewayCoreStatusResponse { + pub identity: String, + pub count: i64, +} diff --git a/nym-api/nym-api-requests/src/models/node_status.rs b/nym-api/nym-api-requests/src/models/node_status.rs new file mode 100644 index 0000000000..03bd0ce2d8 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/node_status.rs @@ -0,0 +1,587 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::PlaceholderJsonSchemaImpl; +use crate::pagination::PaginatedResponse; +use cosmwasm_std::Decimal; +use nym_contracts_common::{IdentityKey, NaiveFloat}; +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::NodeId; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::{Date, OffsetDateTime}; +use utoipa::ToSchema; + +use crate::models::DisplayRole; +pub use config_score::*; + +pub type StakeSaturation = Decimal; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts" + ) +)] +pub struct StakeSaturationResponse { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + #[schema(value_type = String)] + pub saturation: StakeSaturation, + + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + #[schema(value_type = String)] + pub uncapped_saturation: StakeSaturation, + pub as_at: i64, +} + +pub mod config_score { + use nym_contracts_common::NaiveFloat; + use serde::{Deserialize, Serialize}; + use std::cmp::Ordering; + use utoipa::ToSchema; + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct ConfigScoreDataResponse { + pub parameters: ConfigScoreParams, + pub version_history: Vec<HistoricalNymNodeVersionEntry>, + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] + pub struct HistoricalNymNodeVersionEntry { + /// The unique, ordered, id of this particular entry + pub id: u32, + + /// Data associated with this particular version + pub version_information: HistoricalNymNodeVersion, + } + + impl PartialOrd for HistoricalNymNodeVersionEntry { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + // we only care about id for the purposes of ordering as they should have unique data + self.id.partial_cmp(&other.id) + } + } + + impl From<nym_mixnet_contract_common::HistoricalNymNodeVersionEntry> + for HistoricalNymNodeVersionEntry + { + fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersionEntry) -> Self { + HistoricalNymNodeVersionEntry { + id: value.id, + version_information: value.version_information.into(), + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] + pub struct HistoricalNymNodeVersion { + /// Version of the nym node that is going to be used for determining the version score of a node. + /// note: value stored here is pre-validated `semver::Version` + pub semver: String, + + /// Block height of when this version has been added to the contract + pub introduced_at_height: u64, + // for now ignore that field. it will give nothing useful to the users + // pub difference_since_genesis: TotalVersionDifference, + } + + impl From<nym_mixnet_contract_common::HistoricalNymNodeVersion> for HistoricalNymNodeVersion { + fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersion) -> Self { + HistoricalNymNodeVersion { + semver: value.semver, + introduced_at_height: value.introduced_at_height, + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct ConfigScoreParams { + /// Defines weights for calculating numbers of versions behind the current release. + pub version_weights: OutdatedVersionWeights, + + /// Defines the parameters of the formula for calculating the version score + pub version_score_formula_params: VersionScoreFormulaParams, + } + + /// Defines weights for calculating numbers of versions behind the current release. + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct OutdatedVersionWeights { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, + } + + /// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) + /// define the relevant parameters + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct VersionScoreFormulaParams { + pub penalty: f64, + pub penalty_scaling: f64, + } + + impl From<nym_mixnet_contract_common::ConfigScoreParams> for ConfigScoreParams { + fn from(value: nym_mixnet_contract_common::ConfigScoreParams) -> Self { + ConfigScoreParams { + version_weights: value.version_weights.into(), + version_score_formula_params: value.version_score_formula_params.into(), + } + } + } + + impl From<nym_mixnet_contract_common::OutdatedVersionWeights> for OutdatedVersionWeights { + fn from(value: nym_mixnet_contract_common::OutdatedVersionWeights) -> Self { + OutdatedVersionWeights { + major: value.major, + minor: value.minor, + patch: value.patch, + prerelease: value.prerelease, + } + } + } + + impl From<nym_mixnet_contract_common::VersionScoreFormulaParams> for VersionScoreFormulaParams { + fn from(value: nym_mixnet_contract_common::VersionScoreFormulaParams) -> Self { + VersionScoreFormulaParams { + penalty: value.penalty.naive_to_f64(), + penalty_scaling: value.penalty_scaling.naive_to_f64(), + } + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NodeRefreshBody { + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub node_identity: ed25519::PublicKey, + + // a poor man's nonce + pub request_timestamp: i64, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + #[schema(value_type = String)] + pub signature: ed25519::Signature, +} + +impl NodeRefreshBody { + pub fn plaintext(node_identity: ed25519::PublicKey, request_timestamp: i64) -> Vec<u8> { + node_identity + .to_bytes() + .into_iter() + .chain(request_timestamp.to_be_bytes()) + .chain(b"describe-cache-refresh-request".iter().copied()) + .collect() + } + + pub fn new(private_key: &ed25519::PrivateKey) -> Self { + let node_identity = private_key.public_key(); + let request_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + let signature = private_key.sign(Self::plaintext(node_identity, request_timestamp)); + NodeRefreshBody { + node_identity, + request_timestamp, + signature, + } + } + + pub fn verify_signature(&self) -> bool { + self.node_identity + .verify( + Self::plaintext(self.node_identity, self.request_timestamp), + &self.signature, + ) + .is_ok() + } + + pub fn is_stale(&self) -> bool { + let Ok(encoded) = OffsetDateTime::from_unix_timestamp(self.request_timestamp) else { + return true; + }; + let now = OffsetDateTime::now_utc(); + + if encoded > now { + return true; + } + + if (encoded + Duration::from_secs(30)) < now { + return true; + } + + false + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct UptimeResponse { + #[schema(value_type = u32)] + pub mix_id: NodeId, + // The same as node_performance.last_24h. Legacy + pub avg_uptime: u8, + pub node_performance: NodePerformance, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct GatewayUptimeResponse { + pub identity: String, + // The same as node_performance.last_24h. Legacy + pub avg_uptime: u8, + pub node_performance: NodePerformance, +} + +type Uptime = u8; + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct MixnodeStatusReportResponse { + pub mix_id: NodeId, + pub identity: IdentityKey, + pub owner: String, + #[schema(value_type = u8)] + pub most_recent: Uptime, + #[schema(value_type = u8)] + pub last_hour: Uptime, + #[schema(value_type = u8)] + pub last_day: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct GatewayStatusReportResponse { + pub identity: String, + pub owner: String, + #[schema(value_type = u8)] + pub most_recent: Uptime, + #[schema(value_type = u8)] + pub last_hour: Uptime, + #[schema(value_type = u8)] + pub last_day: Uptime, +} + +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts" + ) +)] +pub struct PerformanceHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse<HistoricalPerformanceResponse>, +} + +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts" + ) +)] +pub struct UptimeHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse<HistoricalUptimeResponse>, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts" + ) +)] +pub struct HistoricalUptimeResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub date: Date, + + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts" + ) +)] +pub struct HistoricalPerformanceResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub date: Date, + + pub performance: f64, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct OldHistoricalUptimeResponse { + pub date: String, + #[schema(value_type = u8)] + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct MixnodeUptimeHistoryResponse { + pub mix_id: NodeId, + pub identity: String, + pub owner: String, + pub history: Vec<OldHistoricalUptimeResponse>, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct GatewayUptimeHistoryResponse { + pub identity: String, + pub owner: String, + pub history: Vec<OldHistoricalUptimeResponse>, +} + +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema, Default, +)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts" + ) +)] +#[serde(rename_all = "snake_case")] +pub enum MixnodeStatus { + Active, // in both the active set and the rewarded set + Standby, // only in the rewarded set + Inactive, // in neither the rewarded set nor the active set, but is bonded + #[default] + NotFound, // doesn't even exist in the bonded set +} +impl MixnodeStatus { + pub fn is_active(&self) -> bool { + *self == MixnodeStatus::Active + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts" + ) +)] +pub struct MixnodeStatusResponse { + pub status: MixnodeStatus, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct NodePerformance { + #[schema(value_type = String)] + pub most_recent: Performance, + #[schema(value_type = String)] + pub last_hour: Performance, + #[schema(value_type = String)] + pub last_24h: Performance, +} + +// imo for now there's no point in exposing more than that, +// nym-api shouldn't be calculating apy or stake saturation for you. +// it should just return its own metrics (performance) and then you can do with it as you wish +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts" + ) +)] +pub struct NodeAnnotation { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + // legacy + #[schema(value_type = String)] + pub last_24h_performance: Performance, + pub current_role: Option<DisplayRole>, + + pub detailed_performance: DetailedNodePerformance, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/DetailedNodePerformance.ts" + ) +)] +#[non_exhaustive] +pub struct DetailedNodePerformance { + /// routing_score * config_score + pub performance_score: f64, + + pub routing_score: RoutingScore, + pub config_score: ConfigScore, +} + +impl DetailedNodePerformance { + pub fn new( + performance_score: f64, + routing_score: RoutingScore, + config_score: ConfigScore, + ) -> DetailedNodePerformance { + Self { + performance_score, + routing_score, + config_score, + } + } + + pub fn to_rewarding_performance(&self) -> Performance { + Performance::naive_try_from_f64(self.performance_score).unwrap_or_default() + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/RoutingScore.ts") +)] +#[non_exhaustive] +pub struct RoutingScore { + /// Total score after taking all the criteria into consideration + pub score: f64, +} + +impl RoutingScore { + pub fn new(score: f64) -> RoutingScore { + Self { score } + } + + pub const fn zero() -> RoutingScore { + RoutingScore { score: 0.0 } + } + + pub fn legacy_performance(&self) -> Performance { + Performance::naive_try_from_f64(self.score).unwrap_or_default() + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/ConfigScore.ts") +)] +#[non_exhaustive] +pub struct ConfigScore { + /// Total score after taking all the criteria into consideration + pub score: f64, + + pub versions_behind: Option<u32>, + pub self_described_api_available: bool, + pub accepted_terms_and_conditions: bool, + pub runs_nym_node_binary: bool, +} + +impl ConfigScore { + pub fn new( + score: f64, + versions_behind: u32, + accepted_terms_and_conditions: bool, + runs_nym_node_binary: bool, + ) -> ConfigScore { + Self { + score, + versions_behind: Some(versions_behind), + self_described_api_available: true, + accepted_terms_and_conditions, + runs_nym_node_binary, + } + } + + pub fn bad_semver() -> ConfigScore { + ConfigScore { + score: 0.0, + versions_behind: None, + self_described_api_available: true, + accepted_terms_and_conditions: false, + runs_nym_node_binary: false, + } + } + + pub fn unavailable() -> ConfigScore { + ConfigScore { + score: 0.0, + versions_behind: None, + self_described_api_available: false, + accepted_terms_and_conditions: false, + runs_nym_node_binary: false, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts" + ) +)] +pub struct AnnotationResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub annotation: Option<NodeAnnotation>, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts" + ) +)] +pub struct NodePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub performance: Option<f64>, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts" + ) +)] +pub struct NodeDatePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub date: Date, + pub performance: Option<f64>, +} diff --git a/nym-api/nym-api-requests/src/models/schema_helpers.rs b/nym-api/nym-api-requests/src/models/schema_helpers.rs new file mode 100644 index 0000000000..d42c18fc44 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/schema_helpers.rs @@ -0,0 +1,128 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::unix_epoch; +use cosmwasm_std::Uint128; +use schemars::schema::{InstanceType, Schema, SchemaObject}; +use schemars::{JsonSchema, SchemaGenerator}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::ops::{Deref, DerefMut}; +use time::OffsetDateTime; +use utoipa::ToSchema; + +#[derive(ToSchema)] +#[schema(title = "Coin")] +pub struct CoinSchema { + pub denom: String, + #[schema(value_type = String)] + pub amount: Uint128, +} + +pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error> +where + D: Deserializer<'de>, +{ + Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch())) +} + +// for all intents and purposes it's just OffsetDateTime, but we need JsonSchema... +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)] +pub struct OffsetDateTimeJsonSchemaWrapper( + #[serde( + default = "unix_epoch", + with = "crate::helpers::overengineered_offset_date_time_serde" + )] + #[schema(inline)] + pub OffsetDateTime, +); + +impl Default for OffsetDateTimeJsonSchemaWrapper { + fn default() -> Self { + OffsetDateTimeJsonSchemaWrapper(unix_epoch()) + } +} + +impl Display for OffsetDateTimeJsonSchemaWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl From<OffsetDateTimeJsonSchemaWrapper> for OffsetDateTime { + fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self { + value.0 + } +} + +impl From<OffsetDateTime> for OffsetDateTimeJsonSchemaWrapper { + fn from(value: OffsetDateTime) -> Self { + OffsetDateTimeJsonSchemaWrapper(value) + } +} + +impl Deref for OffsetDateTimeJsonSchemaWrapper { + type Target = OffsetDateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OffsetDateTimeJsonSchemaWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// implementation taken from: https://github.com/GREsau/schemars/pull/207 +impl JsonSchema for OffsetDateTimeJsonSchemaWrapper { + fn is_referenceable() -> bool { + false + } + + fn schema_name() -> String { + "DateTime".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + SchemaObject { + instance_type: Some(InstanceType::String.into()), + format: Some("date-time".into()), + ..Default::default() + } + .into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn offset_date_time_json_schema_wrapper_serde_backwards_compat() { + let mut dummy = OffsetDateTimeJsonSchemaWrapper::default(); + dummy.0 += Duration::from_millis(1); + let ser = serde_json::to_string(&dummy).unwrap(); + + assert_eq!("\"1970-01-01 00:00:00.001 +00:00:00\"", ser); + + let human_readable = "\"2024-05-23 07:41:02.756283766 +00:00:00\""; + let rfc3339 = "\"2002-10-02T15:00:00Z\""; + let rfc3339_offset = "\"2002-10-02T10:00:00-05:00\""; + + let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(human_readable).unwrap(); + assert_eq!(de.0.unix_timestamp(), 1716450062); + + let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339).unwrap(); + assert_eq!(de.0.unix_timestamp(), 1033570800); + + let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339_offset).unwrap(); + assert_eq!(de.0.unix_timestamp(), 1033570800); + + let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>("\"nonsense\"").unwrap(); + assert_eq!(de.0.unix_timestamp(), 0); + } +} diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 544e07a861..ca7ffab7e8 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -8,14 +8,41 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{Interval, NodeId}; +use nym_mixnet_contract_common::{EpochId, Interval, NodeId}; +use nym_noise_keys::VersionedNoiseKey; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::IpAddr; use time::OffsetDateTime; use utoipa::ToSchema; -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +pub struct SkimmedNodesWithMetadata { + pub nodes: Vec<SkimmedNode>, + pub metadata: NodesResponseMetadata, +} + +impl SkimmedNodesWithMetadata { + pub fn new(nodes: Vec<SkimmedNode>, metadata: NodesResponseMetadata) -> Self { + SkimmedNodesWithMetadata { nodes, metadata } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +pub struct SemiSkimmedNodesWithMetadata { + pub nodes: Vec<SemiSkimmedNode>, + pub metadata: NodesResponseMetadata, +} + +impl SemiSkimmedNodesWithMetadata { + pub fn new(nodes: Vec<SemiSkimmedNode>, metadata: NodesResponseMetadata) -> Self { + SemiSkimmedNodesWithMetadata { nodes, metadata } + } +} + +#[derive( + Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq, +)] #[serde(rename_all = "kebab-case")] pub enum TopologyRequestStatus { NoUpdates, @@ -43,20 +70,60 @@ impl<T: ToSchema> CachedNodesResponse<T> { } } +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +pub struct NodesResponseMetadata { + pub status: Option<TopologyRequestStatus>, + #[schema(value_type = u32)] + pub absolute_epoch_id: EpochId, + pub rotation_id: u32, + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, +} + +impl NodesResponseMetadata { + pub fn consistency_check(&self, other: &NodesResponseMetadata) -> bool { + self.status == other.status + && self.absolute_epoch_id == other.absolute_epoch_id + && self.rotation_id == other.rotation_id + } + + pub fn refreshed_at(&self) -> OffsetDateTime { + self.refreshed_at.into() + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] -pub struct PaginatedCachedNodesResponse<T> { +// can't add any new fields here, even with #[serde(default)] and whatnot, +// because it will break all clients using bincode : ( +pub struct PaginatedCachedNodesResponseV1<T> { pub status: Option<TopologyRequestStatus>, pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, pub nodes: PaginatedResponse<T>, } -impl<T> PaginatedCachedNodesResponse<T> { +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaginatedCachedNodesResponseV2<T> { + pub metadata: NodesResponseMetadata, + pub nodes: PaginatedResponse<T>, +} + +impl<T> From<PaginatedCachedNodesResponseV2<T>> for PaginatedCachedNodesResponseV1<T> { + fn from(res: PaginatedCachedNodesResponseV2<T>) -> Self { + PaginatedCachedNodesResponseV1 { + status: res.metadata.status, + refreshed_at: res.metadata.refreshed_at, + nodes: res.nodes, + } + } +} + +impl<T> PaginatedCachedNodesResponseV2<T> { pub fn new_full( + absolute_epoch_id: EpochId, + rotation_id: u32, refreshed_at: impl Into<OffsetDateTimeJsonSchemaWrapper>, nodes: Vec<T>, ) -> Self { - PaginatedCachedNodesResponse { - refreshed_at: refreshed_at.into(), + PaginatedCachedNodesResponseV2 { nodes: PaginatedResponse { pagination: Pagination { total: nodes.len(), @@ -65,19 +132,22 @@ impl<T> PaginatedCachedNodesResponse<T> { }, data: nodes, }, - status: None, + metadata: NodesResponseMetadata { + refreshed_at: refreshed_at.into(), + status: None, + absolute_epoch_id, + rotation_id, + }, } } - pub fn fresh(mut self, interval: Option<Interval>) -> Self { - let iv = interval.map(TopologyRequestStatus::Fresh); - self.status = iv; + pub fn fresh(mut self, interval: Interval) -> Self { + self.metadata.status = Some(TopologyRequestStatus::Fresh(interval)); self } - pub fn no_updates() -> Self { - PaginatedCachedNodesResponse { - refreshed_at: OffsetDateTime::now_utc().into(), + pub fn no_updates(absolute_epoch_id: EpochId, rotation_id: u32) -> Self { + PaginatedCachedNodesResponseV2 { nodes: PaginatedResponse { pagination: Pagination { total: 0, @@ -86,7 +156,12 @@ impl<T> PaginatedCachedNodesResponse<T> { }, data: Vec::new(), }, - status: Some(TopologyRequestStatus::NoUpdates), + metadata: NodesResponseMetadata { + refreshed_at: OffsetDateTime::now_utc().into(), + status: Some(TopologyRequestStatus::NoUpdates), + absolute_epoch_id, + rotation_id, + }, } } } @@ -202,7 +277,8 @@ impl SkimmedNode { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SemiSkimmedNode { pub basic: SkimmedNode, - pub x25519_noise_pubkey: String, + + pub x25519_noise_versioned_key: Option<VersionedNoiseKey>, // pub location: } diff --git a/nym-api/nym-api-requests/src/signable.rs b/nym-api/nym-api-requests/src/signable.rs new file mode 100644 index 0000000000..5b1bb90584 --- /dev/null +++ b/nym-api/nym-api-requests/src/signable.rs @@ -0,0 +1,75 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_signature; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +// the trait is not public as it's only defined on types that are guaranteed to not panic when serialised +pub trait SignableMessageBody: Serialize + sealed::Sealed { + fn sign(self, key: &ed25519::PrivateKey) -> SignedMessage<Self> + where + Self: Sized, + { + let signature = key.sign(self.plaintext()); + SignedMessage { + body: self, + signature, + } + } + + fn plaintext(&self) -> Vec<u8> { + #[allow(clippy::unwrap_used)] + // SAFETY: all types that implement this trait have valid serialisations + serde_json::to_vec(&self).unwrap() + } +} + +impl<T> SignableMessageBody for T where T: Serialize + sealed::Sealed {} + +#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SignedMessage<T> { + pub body: T, + #[schema(value_type = String)] + #[serde(with = "bs58_ed25519_signature")] + pub signature: ed25519::Signature, +} + +impl<T> SignedMessage<T> { + pub fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool + where + T: SignableMessageBody, + { + let plaintext = self.body.plaintext(); + if plaintext.is_empty() { + return false; + } + + pub_key.verify(&plaintext, &self.signature).is_ok() + } +} + +// make sure only our types can implement this trait (to ensure infallible serialisation) +pub(crate) mod sealed { + use crate::ecash::models::*; + use crate::models::{ + ChainBlocksStatusResponseBody, DetailedSignersStatusResponseBody, SignersStatusResponseBody, + }; + + pub trait Sealed {} + + // requests + impl Sealed for IssuedTicketbooksChallengeCommitmentRequestBody {} + impl Sealed for IssuedTicketbooksDataRequestBody {} + + // responses + impl Sealed for IssuedTicketbooksChallengeCommitmentResponseBody {} + impl Sealed for IssuedTicketbooksForResponseBody {} + impl Sealed for IssuedTicketbooksDataResponseBody {} + impl Sealed for EcashSignerStatusResponseBody {} + impl Sealed for ChainBlocksStatusResponseBody {} + impl Sealed for SignersStatusResponseBody {} + impl Sealed for DetailedSignersStatusResponseBody {} +} diff --git a/nym-api/settings.sql b/nym-api/settings.sql new file mode 100644 index 0000000000..321fef528a --- /dev/null +++ b/nym-api/settings.sql @@ -0,0 +1,2 @@ +.mode columns +.headers on \ No newline at end of file diff --git a/nym-api/src/circulating_supply_api/cache/data.rs b/nym-api/src/circulating_supply_api/cache/data.rs deleted file mode 100644 index 77ddbaaee2..0000000000 --- a/nym-api/src/circulating_supply_api/cache/data.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::caching::Cache; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::Coin; - -pub(crate) struct CirculatingSupplyCacheData { - // no need to cache that one as it's constant, but let's put it here for consistency sake - pub(crate) total_supply: Coin, - pub(crate) mixmining_reserve: Cache<Coin>, - pub(crate) vesting_tokens: Cache<Coin>, - pub(crate) circulating_supply: Cache<Coin>, -} - -impl CirculatingSupplyCacheData { - pub fn new(mix_denom: String) -> CirculatingSupplyCacheData { - let zero_coin = Coin::new(0, &mix_denom); - - CirculatingSupplyCacheData { - total_supply: Coin::new(1_000_000_000_000_000, mix_denom), - mixmining_reserve: Cache::new(zero_coin.clone()), - vesting_tokens: Cache::new(zero_coin.clone()), - circulating_supply: Cache::new(zero_coin), - } - } -} - -impl<'a> From<&'a CirculatingSupplyCacheData> for CirculatingSupplyResponse { - fn from(value: &'a CirculatingSupplyCacheData) -> Self { - CirculatingSupplyResponse { - total_supply: value.total_supply.clone().into(), - mixmining_reserve: value.mixmining_reserve.clone().into(), - vesting_tokens: value.vesting_tokens.clone().into(), - circulating_supply: value.circulating_supply.clone().into(), - } - } -} diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs deleted file mode 100644 index 87e401da9d..0000000000 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use self::data::CirculatingSupplyCacheData; -use cosmwasm_std::Addr; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::nyxd::Coin; -use std::ops::Deref; -use std::{ - sync::{atomic::AtomicBool, Arc}, - time::Duration, -}; -use thiserror::Error; -use tokio::sync::RwLock; -use tokio::time; -use tracing::{error, info}; - -mod data; -pub(crate) mod refresher; - -#[derive(Debug, Error)] -enum CirculatingSupplyCacheError { - // this can only happen if somebody decides to set their staking address - // before https://github.com/nymtech/nym/pull/2796 is deployed - #[error("vesting account owned by {owner} with id {account_id} appeared more than once in the query response")] - DuplicateVestingAccountEntry { owner: Addr, account_id: u32 }, - - // this can happen if somehow the query was incomplete, like some paged sub-query didn't return full result - // or there's a bug with paging. or if, somehow, a vesting account got removed from the contract - #[error("got an inconsistent number of vesting account. received data on {got}, but expected {expected}")] - InconsistentNumberOfVestingAccounts { expected: usize, got: usize }, - - #[error(transparent)] - ClientError { - #[from] - source: NyxdError, - }, -} - -/// A cache for the circulating supply of the network. Circulating supply is calculated by -/// taking the initial supply of 1bn coins, and subtracting the amount of coins that are -/// in the mixmining pool and tied up in vesting. -/// -/// The cache is quite simple and does not include an update listener that the other caches have. -#[derive(Clone)] -pub(crate) struct CirculatingSupplyCache { - initialised: Arc<AtomicBool>, - data: Arc<RwLock<CirculatingSupplyCacheData>>, -} - -impl CirculatingSupplyCache { - pub(crate) fn new(mix_denom: String) -> CirculatingSupplyCache { - CirculatingSupplyCache { - initialised: Arc::new(AtomicBool::new(false)), - data: Arc::new(RwLock::new(CirculatingSupplyCacheData::new(mix_denom))), - } - } - - pub(crate) async fn get_circulating_supply(&self) -> Option<CirculatingSupplyResponse> { - match time::timeout(Duration::from_millis(100), self.data.read()).await { - Ok(cache) => Some(cache.deref().into()), - Err(err) => { - error!("Failed to get circulating supply: {err}"); - None - } - } - } - - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { - let mut cache = self.data.write().await; - - let mut circulating_supply = cache.total_supply.clone(); - circulating_supply.amount -= mixmining_reserve.amount; - circulating_supply.amount -= vesting_tokens.amount; - - info!("Updating circulating supply cache"); - info!("the mixmining reserve is now {mixmining_reserve}"); - info!("the number of tokens still vesting is now {vesting_tokens}"); - info!("the circulating supply is now {circulating_supply}"); - - cache.mixmining_reserve.unchecked_update(mixmining_reserve); - cache.vesting_tokens.unchecked_update(vesting_tokens); - cache - .circulating_supply - .unchecked_update(circulating_supply); - } -} diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs deleted file mode 100644 index 60d7c2bafc..0000000000 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use super::CirculatingSupplyCache; -use crate::circulating_supply_api::cache::CirculatingSupplyCacheError; -use crate::support::nyxd::Client; -use nym_contracts_common::truncate_decimal; -use nym_task::TaskClient; -use nym_validator_client::nyxd::Coin; -use std::collections::HashSet; -use std::sync::atomic::Ordering; -use std::time::Duration; -use tokio::time; -use tracing::{error, trace}; - -pub(crate) struct CirculatingSupplyCacheRefresher { - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, -} - -impl CirculatingSupplyCacheRefresher { - pub(crate) fn new( - nyxd_client: Client, - cache: CirculatingSupplyCache, - caching_interval: Duration, - ) -> Self { - CirculatingSupplyCacheRefresher { - nyxd_client, - cache, - caching_interval, - } - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - let mut interval = time::interval(self.caching_interval); - while !shutdown.is_shutdown() { - tokio::select! { - _ = interval.tick() => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - ret = self.refresh() => { - if let Err(err) = ret { - error!("Failed to refresh circulating supply cache - {err}"); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.initialised.store(true, Ordering::Relaxed) - } - } - } - } - _ = shutdown.recv() => { - trace!("CirculatingSupplyCacheRefresher: Received shutdown"); - } - } - } - } - - async fn get_mixmining_reserve( - &self, - mix_denom: &str, - ) -> Result<Coin, CirculatingSupplyCacheError> { - let reward_pool = self - .nyxd_client - .get_current_rewarding_parameters() - .await? - .interval - .reward_pool; - - Ok(Coin::new(truncate_decimal(reward_pool).u128(), mix_denom)) - } - - async fn get_total_vesting_tokens( - &self, - mix_denom: &str, - ) -> Result<Coin, CirculatingSupplyCacheError> { - let all_vesting = self.nyxd_client.get_all_vesting_coins().await?; - - // sanity check invariants to make sure all accounts got considered and we got no duplicates - // the cache refreshes so infrequently that the performance penalty is negligible - let mut owners = HashSet::new(); - let mut ids = HashSet::new(); - for acc in &all_vesting { - if !owners.insert(acc.owner.clone()) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - - if !ids.insert(acc.account_id) { - return Err(CirculatingSupplyCacheError::DuplicateVestingAccountEntry { - owner: acc.owner.clone(), - account_id: acc.account_id, - }); - } - } - - let current_storage_key = self - .nyxd_client - .get_current_vesting_account_storage_key() - .await?; - if all_vesting.len() != current_storage_key as usize { - return Err( - CirculatingSupplyCacheError::InconsistentNumberOfVestingAccounts { - expected: current_storage_key as usize, - got: all_vesting.len(), - }, - ); - } - - let mut total = Coin::new(0, mix_denom); - for account in all_vesting { - total.amount += account.still_vesting.amount.u128(); - } - - Ok(total) - } - - async fn refresh(&self) -> Result<(), CirculatingSupplyCacheError> { - let chain_details = self.nyxd_client.chain_details().await; - let mix_denom = &chain_details.mix_denom.base; - - let mixmining_reserve = self.get_mixmining_reserve(mix_denom).await?; - let vesting_tokens = self.get_total_vesting_tokens(mix_denom).await?; - - self.cache.update(mixmining_reserve, vesting_tokens).await; - Ok(()) - } -} diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index 3e49659b55..3299afa7f1 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,23 +1,23 @@ // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::{extract, Router}; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::models::CirculatingSupplyResponse; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_validator_client::nyxd::Coin; pub(crate) fn circulating_supply_routes() -> Router<AppState> { Router::new() .route("/", axum::routing::get(get_full_circulating_supply)) .route( - "/total-supply-value", + "/circulating-supply-value", axum::routing::get(get_circulating_supply), ) - .route( - "/circulating-supply-value", - axum::routing::get(get_total_supply), - ) + .route("/total-supply-value", axum::routing::get(get_total_supply)) } #[utoipa::path( @@ -25,18 +25,22 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> { get, path = "/v1/circulating-supply", responses( - (status = 200, body = CirculatingSupplyResponse) - ) + (status = 200, content( + (CirculatingSupplyResponse = "application/json"), + (CirculatingSupplyResponse = "application/yaml"), + (CirculatingSupplyResponse = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_full_circulating_supply( - extract::State(state): extract::State<AppState>, -) -> AxumResult<axum::Json<CirculatingSupplyResponse>> { - match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { - Some(value) => Ok(value.into()), + Query(output): Query<OutputParams>, + State(contract_cache): State<MixnetContractCache>, +) -> AxumResult<FormattedResponse<CirculatingSupplyResponse>> { + let output = output.output.unwrap_or_default(); + + match contract_cache.get_circulating_supply().await { + Some(value) => Ok(output.to_response(value)), None => Err(AxumErrorResponse::internal_msg("unavailable")), } } @@ -46,22 +50,27 @@ async fn get_full_circulating_supply( get, path = "/v1/circulating-supply/total-supply-value", responses( - (status = 200, body = [f64]) - ) + (status = 200, content( + (f64 = "application/json"), + (f64 = "application/yaml"), + (f64 = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_total_supply( - extract::State(state): extract::State<AppState>, -) -> AxumResult<axum::Json<f64>> { - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + Query(output): Query<OutputParams>, + State(contract_cache): State<MixnetContractCache>, +) -> AxumResult<FormattedResponse<f64>> { + let output = output.output.unwrap_or_default(); + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; - Ok(unym_coin_to_float_unym(full_circulating_supply.total_supply.into()).into()) + let total_supply = unym_coin_to_float_unym(full_circulating_supply.total_supply.into()); + + Ok(output.to_response(total_supply)) } #[utoipa::path( @@ -69,22 +78,29 @@ async fn get_total_supply( get, path = "/v1/circulating-supply/circulating-supply-value", responses( - (status = 200, body = [f64]) - ) + (status = 200, content( + (f64 = "application/json"), + (f64 = "application/yaml"), + (f64 = "application/bincode") + )) + ), + params(OutputParams) )] async fn get_circulating_supply( - extract::State(state): extract::State<AppState>, -) -> AxumResult<axum::Json<f64>> { - let full_circulating_supply = match state - .circulating_supply_cache() - .get_circulating_supply() - .await - { + Query(output): Query<OutputParams>, + State(contract_cache): State<MixnetContractCache>, +) -> AxumResult<FormattedResponse<f64>> { + let output = output.output.unwrap_or_default(); + + let full_circulating_supply = match contract_cache.get_circulating_supply().await { Some(res) => res, None => return Err(AxumErrorResponse::internal_msg("unavailable")), }; - Ok(unym_coin_to_float_unym(full_circulating_supply.circulating_supply.into()).into()) + let circulating_supply = + unym_coin_to_float_unym(full_circulating_supply.circulating_supply.into()); + + Ok(output.to_response(circulating_supply)) } // TODO: this is not the best place to put it, it should be more centralised, diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 3253ccaaab..d1a19b2932 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,27 +1,4 @@ // Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use self::cache::refresher::CirculatingSupplyCacheRefresher; -use crate::support::{config, nyxd}; -use nym_task::TaskManager; - -pub(crate) mod cache; pub(crate) mod handlers; - -/// Spawn the circulating supply cache refresher. -pub(crate) fn start_cache_refresh( - config: &config::CirculatingSupplyCacher, - nyxd_client: nyxd::Client, - circulating_supply_cache: &cache::CirculatingSupplyCache, - shutdown: &TaskManager, -) { - if config.enabled { - let refresher = CirculatingSupplyCacheRefresher::new( - nyxd_client, - circulating_supply_cache.to_owned(), - config.debug.caching_interval, - ); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { refresher.run(shutdown_listener).await }); - } -} diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index 8b7baef6e6..7d61643488 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -7,12 +7,14 @@ use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::{Json, Router}; +use axum::Router; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; use nym_api_requests::ecash::VerificationKeyResponse; +use nym_coconut_dkg_common::types::EpochId; use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_http_api_common::{FormattedResponse, Output}; use nym_validator_client::nym_api::rfc_3339_date; use serde::Deserialize; use std::sync::Arc; @@ -45,26 +47,33 @@ pub(crate) fn aggregation_routes() -> Router<AppState> { ), path = "/v1/ecash/master-verification-key", responses( - (status = 200, body = VerificationKeyResponse) - ) + (status = 200, content( + (VerificationKeyResponse = "application/json"), + (VerificationKeyResponse = "application/yaml"), + (VerificationKeyResponse = "application/bincode") + )) + ), )] async fn master_verification_key( State(state): State<Arc<EcashState>>, - Query(EpochIdParam { epoch_id }): Query<EpochIdParam>, -) -> AxumResult<Json<VerificationKeyResponse>> { + Query(EpochIdParam { epoch_id, output }): Query<EpochIdParam>, +) -> AxumResult<FormattedResponse<VerificationKeyResponse>> { trace!("aggregated_verification_key request"); + let output = output.unwrap_or_default(); // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; let key = state.master_verification_key(epoch_id).await?; - Ok(Json(VerificationKeyResponse::new(key.clone()))) + Ok(output.to_response(VerificationKeyResponse::new(key.clone()))) } #[derive(Deserialize, IntoParams)] struct ExpirationDateParam { expiration_date: Option<String>, + epoch_id: Option<EpochId>, + output: Option<Output>, } #[utoipa::path( @@ -75,14 +84,23 @@ struct ExpirationDateParam { ), path = "/v1/ecash/aggregated-expiration-date-signatures", responses( - (status = 200, body = AggregatedExpirationDateSignatureResponse) - ) + (status = 200, content( + (AggregatedExpirationDateSignatureResponse = "application/json"), + (AggregatedExpirationDateSignatureResponse = "application/yaml"), + (AggregatedExpirationDateSignatureResponse = "application/bincode") + )) + ), )] async fn expiration_date_signatures( State(state): State<Arc<EcashState>>, - Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>, -) -> AxumResult<Json<AggregatedExpirationDateSignatureResponse>> { + Query(ExpirationDateParam { + expiration_date, + epoch_id, + output, + }): Query<ExpirationDateParam>, +) -> AxumResult<FormattedResponse<AggregatedExpirationDateSignatureResponse>> { trace!("aggregated_expiration_date_signatures request"); + let output = output.unwrap_or_default(); let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), @@ -93,15 +111,22 @@ async fn expiration_date_signatures( // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; + let epoch_id = match epoch_id { + Some(epoch_id) => epoch_id, + None => state.current_dkg_epoch().await?, + }; + let expiration_date_signatures = state - .master_expiration_date_signatures(expiration_date) + .master_expiration_date_signatures(expiration_date, epoch_id) .await?; - Ok(Json(AggregatedExpirationDateSignatureResponse { - epoch_id: expiration_date_signatures.epoch_id, - expiration_date, - signatures: expiration_date_signatures.signatures.clone(), - })) + Ok( + output.to_response(AggregatedExpirationDateSignatureResponse { + epoch_id: expiration_date_signatures.epoch_id, + expiration_date, + signatures: expiration_date_signatures.signatures.clone(), + }), + ) } #[utoipa::path( @@ -112,21 +137,26 @@ async fn expiration_date_signatures( ), path = "/v1/ecash/aggregated-coin-indices-signatures", responses( - (status = 200, body = AggregatedCoinIndicesSignatureResponse) - ) + (status = 200, content( + (AggregatedCoinIndicesSignatureResponse = "application/json"), + (AggregatedCoinIndicesSignatureResponse = "application/yaml"), + (AggregatedCoinIndicesSignatureResponse = "application/bincode") + )) + ), )] async fn coin_indices_signatures( - Query(EpochIdParam { epoch_id }): Query<EpochIdParam>, + Query(EpochIdParam { epoch_id, output }): Query<EpochIdParam>, State(state): State<Arc<EcashState>>, -) -> AxumResult<Json<AggregatedCoinIndicesSignatureResponse>> { +) -> AxumResult<FormattedResponse<AggregatedCoinIndicesSignatureResponse>> { trace!("aggregated_coin_indices_signatures request"); + let output = output.unwrap_or_default(); // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; - Ok(Json(AggregatedCoinIndicesSignatureResponse { + Ok(output.to_response(AggregatedCoinIndicesSignatureResponse { epoch_id: coin_indices_signatures.epoch_id, signatures: coin_indices_signatures.signatures.clone(), })) diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs index 9114ebba9f..5b7edfefbe 100644 --- a/nym-api/src/ecash/api_routes/handlers.rs +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -4,8 +4,10 @@ use crate::ecash::api_routes::aggregation::aggregation_routes; use crate::ecash::api_routes::issued::issued_routes; use crate::ecash::api_routes::partial_signing::partial_signing_routes; +use crate::ecash::api_routes::signer_status::signer_status; use crate::ecash::api_routes::spending::spending_routes; use crate::support::http::state::AppState; +use axum::routing::get; use axum::Router; pub(crate) fn ecash_routes() -> Router<AppState> { @@ -14,4 +16,5 @@ pub(crate) fn ecash_routes() -> Router<AppState> { .merge(issued_routes()) .merge(partial_signing_routes()) .merge(spending_routes()) + .route("/signer-status", get(signer_status)) } diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 9d0270acc7..b625ba29ba 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -1,7 +1,10 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use nym_http_api_common::Output; + #[derive(serde::Deserialize, utoipa::IntoParams)] pub(super) struct EpochIdParam { pub(super) epoch_id: Option<u64>, + pub(super) output: Option<Output>, } diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs index 717073e262..914b087b97 100644 --- a/nym-api/src/ecash/api_routes/issued.rs +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -11,8 +11,10 @@ use nym_api_requests::ecash::models::{ IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksCountResponse, IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse, - IssuedTicketbooksOnCountResponse, SignableMessageBody, + IssuedTicketbooksOnCountResponse, }; +use nym_api_requests::signable::SignableMessageBody; +use nym_http_api_common::{FormattedResponse, OutputParams}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp::min; @@ -80,15 +82,20 @@ pub(crate) struct IssuanceDatePathParam { path = "/issued-ticketbooks-count", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksCountResponse), + (status = 200, content( + (IssuedTicketbooksCountResponse = "application/json"), + (IssuedTicketbooksCountResponse = "application/yaml"), + (IssuedTicketbooksCountResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_count( Query(pagination): Query<PaginationRequest>, State(state): State<Arc<EcashState>>, -) -> AxumResult<Json<IssuedTicketbooksCountResponse>> { +) -> AxumResult<FormattedResponse<IssuedTicketbooksCountResponse>> { state.ensure_signer().await?; + let output = pagination.output.unwrap_or_default(); let page = pagination.page.unwrap_or_default(); let per_page = min( @@ -98,9 +105,7 @@ async fn issued_ticketbooks_count( MAX_ISSUANCE_COUNT_PAGE_SIZE, ); - Ok(Json( - state.get_issued_ticketbooks_count(page, per_page).await?, - )) + Ok(output.to_response(state.get_issued_ticketbooks_count(page, per_page).await?)) } /// Returns number of issued ticketbooks for particular expiration date. @@ -109,22 +114,28 @@ async fn issued_ticketbooks_count( tag = "Ecash", get, params( - ExpirationDatePathParam + ExpirationDatePathParam, OutputParams ), path = "/issued-ticketbooks-for-count/{expiration_date}", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksForCountResponse), + (status = 200, content( + (IssuedTicketbooksForCountResponse = "application/json"), + (IssuedTicketbooksForCountResponse = "application/yaml"), + (IssuedTicketbooksForCountResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_for_count( + Query(output): Query<OutputParams>, Path(ExpirationDatePathParam { expiration_date }): Path<ExpirationDatePathParam>, State(state): State<Arc<EcashState>>, -) -> AxumResult<Json<IssuedTicketbooksForCountResponse>> { +) -> AxumResult<FormattedResponse<IssuedTicketbooksForCountResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_for_count(expiration_date) .await?, @@ -137,46 +148,56 @@ async fn issued_ticketbooks_for_count( tag = "Ecash", get, params( - IssuanceDatePathParam + IssuanceDatePathParam, OutputParams ), path = "/issued-ticketbooks-on-count/{issuance_date}", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksOnCountResponse), + (status = 200, content( + (IssuedTicketbooksOnCountResponse = "application/json"), + (IssuedTicketbooksOnCountResponse = "application/yaml"), + (IssuedTicketbooksOnCountResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_on_count( + Query(output): Query<OutputParams>, Path(IssuanceDatePathParam { issuance_date }): Path<IssuanceDatePathParam>, State(state): State<Arc<EcashState>>, -) -> AxumResult<Json<IssuedTicketbooksOnCountResponse>> { +) -> AxumResult<FormattedResponse<IssuedTicketbooksOnCountResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( - state.get_issued_ticketbooks_on_count(issuance_date).await?, - )) + Ok(output.to_response(state.get_issued_ticketbooks_on_count(issuance_date).await?)) } #[utoipa::path( tag = "Ecash", get, params( - ExpirationDatePathParam + ExpirationDatePathParam, OutputParams ), path = "/issued-ticketbooks-for/{expiration_date}", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksForResponse), + (status = 200, content( + (IssuedTicketbooksForResponse = "application/json"), + (IssuedTicketbooksForResponse = "application/yaml"), + (IssuedTicketbooksForResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn issued_ticketbooks_for( + Query(output): Query<OutputParams>, State(state): State<Arc<EcashState>>, Path(ExpirationDatePathParam { expiration_date }): Path<ExpirationDatePathParam>, -) -> AxumResult<Json<IssuedTicketbooksForResponse>> { +) -> AxumResult<FormattedResponse<IssuedTicketbooksForResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_deposits_on(expiration_date) .await? @@ -191,17 +212,24 @@ async fn issued_ticketbooks_for( path = "/issued-ticketbooks-challenge-commitment", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksChallengeCommitmentResponse), + (status = 200, content( + (IssuedTicketbooksChallengeCommitmentResponse = "application/json"), + (IssuedTicketbooksChallengeCommitmentResponse = "application/yaml"), + (IssuedTicketbooksChallengeCommitmentResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - ) + ), + params(OutputParams) )] async fn issued_ticketbooks_challenge_commitment( + Query(output): Query<OutputParams>, State(state): State<Arc<EcashState>>, Json(request): Json<IssuedTicketbooksChallengeCommitmentRequest>, -) -> AxumResult<Json<IssuedTicketbooksChallengeCommitmentResponse>> { +) -> AxumResult<FormattedResponse<IssuedTicketbooksChallengeCommitmentResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_challenge_commitment(request) .await? @@ -216,17 +244,24 @@ async fn issued_ticketbooks_challenge_commitment( path = "/issued-ticketbooks-data", context_path = "/v1/ecash", responses( - (status = 200, body = IssuedTicketbooksDataResponse), + (status = 200, content( + (IssuedTicketbooksDataResponse = "application/json"), + (IssuedTicketbooksDataResponse = "application/yaml"), + (IssuedTicketbooksDataResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - ) + ), + params(OutputParams) )] async fn issued_ticketbooks_data( + Query(output): Query<OutputParams>, State(state): State<Arc<EcashState>>, Json(request): Json<IssuedTicketbooksDataRequest>, -) -> AxumResult<Json<IssuedTicketbooksDataResponse>> { +) -> AxumResult<FormattedResponse<IssuedTicketbooksDataResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); - Ok(Json( + Ok(output.to_response( state .get_issued_ticketbooks_data(request) .await? diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs index 18bdf01d4e..2588905654 100644 --- a/nym-api/src/ecash/api_routes/mod.rs +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -6,4 +6,5 @@ pub(crate) mod handlers; mod helpers; pub(crate) mod issued; pub(crate) mod partial_signing; +pub(crate) mod signer_status; pub(crate) mod spending; diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index b5d53a5ed0..36b71535c8 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -13,7 +13,9 @@ use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, }; +use nym_coconut_dkg_common::types::EpochId; use nym_ecash_time::{cred_exp_date, EcashTime}; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use nym_validator_client::nym_api::rfc_3339_date; use serde::Deserialize; use std::ops::Deref; @@ -41,16 +43,22 @@ pub(crate) fn partial_signing_routes() -> Router<AppState> { request_body = BlindSignRequestBody, path = "/v1/ecash/blind-sign", responses( - (status = 200, body = BlindedSignatureResponse), + (status = 200, content( + (BlindedSignatureResponse = "application/json"), + (BlindedSignatureResponse = "application/yaml"), + (BlindedSignatureResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - - ) + ), + params(OutputParams) )] async fn post_blind_sign( + Query(output): Query<OutputParams>, State(state): State<Arc<EcashState>>, Json(blind_sign_request_body): Json<BlindSignRequestBody>, -) -> AxumResult<Json<BlindedSignatureResponse>> { +) -> AxumResult<FormattedResponse<BlindedSignatureResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); debug!("Received blind sign request"); trace!("body: {:?}", blind_sign_request_body); @@ -73,7 +81,7 @@ async fn post_blind_sign( "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", ); if let Some(blinded_signature) = state.already_issued(deposit_id).await? { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); + return Ok(output.to_response(BlindedSignatureResponse { blinded_signature })); } //check if account was blacklisted @@ -101,12 +109,14 @@ async fn post_blind_sign( .await?; // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) + Ok(output.to_response(BlindedSignatureResponse { blinded_signature })) } #[derive(Deserialize, IntoParams)] struct ExpirationDateParam { expiration_date: Option<String>, + epoch_id: Option<EpochId>, + output: Option<Output>, } #[utoipa::path( @@ -117,15 +127,24 @@ struct ExpirationDateParam { ), path = "/v1/ecash/partial-expiration-date-signatures", responses( - (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 200, content( + (PartialExpirationDateSignatureResponse = "application/json"), + (PartialExpirationDateSignatureResponse = "application/yaml"), + (PartialExpirationDateSignatureResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn partial_expiration_date_signatures( State(state): State<Arc<EcashState>>, - Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>, -) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> { + Query(ExpirationDateParam { + expiration_date, + epoch_id, + output, + }): Query<ExpirationDateParam>, +) -> AxumResult<FormattedResponse<PartialExpirationDateSignatureResponse>> { state.ensure_signer().await?; + let output = output.unwrap_or_default(); let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), @@ -136,11 +155,16 @@ async fn partial_expiration_date_signatures( // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; + let epoch_id = match epoch_id { + Some(epoch_id) => epoch_id, + None => state.current_dkg_epoch().await?, + }; + let expiration_date_signatures = state - .partial_expiration_date_signatures(expiration_date) + .partial_expiration_date_signatures(expiration_date, epoch_id) .await?; - Ok(Json(PartialExpirationDateSignatureResponse { + Ok(output.to_response(PartialExpirationDateSignatureResponse { epoch_id: expiration_date_signatures.epoch_id, expiration_date, signatures: expiration_date_signatures.signatures.clone(), @@ -155,14 +179,18 @@ async fn partial_expiration_date_signatures( ), path = "/v1/ecash/partial-coin-indices-signatures", responses( - (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 200, content( + (PartialCoinIndicesSignatureResponse = "application/json"), + (PartialCoinIndicesSignatureResponse = "application/yaml"), + (PartialCoinIndicesSignatureResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn partial_coin_indices_signatures( State(state): State<Arc<EcashState>>, - Query(EpochIdParam { epoch_id }): Query<EpochIdParam>, -) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> { + Query(EpochIdParam { epoch_id, output }): Query<EpochIdParam>, +) -> AxumResult<FormattedResponse<PartialCoinIndicesSignatureResponse>> { state.ensure_signer().await?; // see if we're not in the middle of new dkg @@ -170,8 +198,10 @@ async fn partial_coin_indices_signatures( let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; - Ok(Json(PartialCoinIndicesSignatureResponse { - epoch_id: coin_indices_signatures.epoch_id, - signatures: coin_indices_signatures.signatures.clone(), - })) + Ok(output + .unwrap_or_default() + .to_response(PartialCoinIndicesSignatureResponse { + epoch_id: coin_indices_signatures.epoch_id, + signatures: coin_indices_signatures.signatures.clone(), + })) } diff --git a/nym-api/src/ecash/api_routes/signer_status.rs b/nym-api/src/ecash/api_routes/signer_status.rs new file mode 100644 index 0000000000..739482cd35 --- /dev/null +++ b/nym-api/src/ecash/api_routes/signer_status.rs @@ -0,0 +1,45 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::state::EcashState; +use crate::node_status_api::models::ApiResult; +use axum::extract::{Query, State}; +use nym_api_requests::ecash::models::{EcashSignerStatusResponse, EcashSignerStatusResponseBody}; +use nym_api_requests::signable::SignableMessageBody; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use std::sync::Arc; +use time::OffsetDateTime; + +#[utoipa::path( + tag = "Ecash", + get, + path = "/signer-status", + context_path = "/v1/ecash", + responses( + (status = 200, content( + (EcashSignerStatusResponse = "application/json"), + (EcashSignerStatusResponse = "application/yaml"), + (EcashSignerStatusResponse = "application/bincode") + )), + ), + params(OutputParams) +)] +pub(crate) async fn signer_status( + Query(params): Query<OutputParams>, + State(state): State<Arc<EcashState>>, +) -> ApiResult<FormattedResponse<EcashSignerStatusResponse>> { + let output = params.get_output(); + + let dkg_ecash_epoch_id = state.current_dkg_epoch().await?; + + Ok(output.to_response( + EcashSignerStatusResponseBody { + current_time: OffsetDateTime::now_utc(), + dkg_ecash_epoch_id, + signer_disabled: state.local.explicitly_disabled, + is_ecash_signer: state.is_dkg_signer(dkg_ecash_epoch_id).await?, + has_signing_keys: state.ecash_signing_key().await.is_ok(), + } + .sign(state.local.identity_keypair.private_key()), + )) +} diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index c0fd546ef6..bae3342245 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -5,7 +5,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::http::StatusCode; use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; @@ -15,6 +15,7 @@ use nym_api_requests::ecash::models::{ }; use nym_compact_ecash::identify::IdentifyResult; use nym_ecash_time::EcashTime; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; @@ -39,9 +40,10 @@ pub(crate) fn spending_routes() -> Router<AppState> { const ONE_AM: Time = time!(1:00); fn reject_ticket( + output: Output, reason: EcashTicketVerificationRejection, -) -> AxumResult<Json<EcashTicketVerificationResponse>> { - Ok(Json(EcashTicketVerificationResponse::reject(reason))) +) -> AxumResult<FormattedResponse<EcashTicketVerificationResponse>> { + Ok(output.to_response(EcashTicketVerificationResponse::reject(reason))) } // TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` @@ -52,23 +54,29 @@ fn reject_ticket( request_body = VerifyEcashTicketBody, path = "/v1/ecash/verify-ecash-ticket", responses( - (status = 200, body = EcashTicketVerificationResponse), + (status = 200, content( + (EcashTicketVerificationResponse = "application/json"), + (EcashTicketVerificationResponse = "application/yaml"), + (EcashTicketVerificationResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), ) )] async fn verify_ticket( + Query(output): Query<OutputParams>, State(state): State<Arc<EcashState>>, // TODO in the future: make it send binary data rather than json Json(verify_ticket_body): Json<VerifyEcashTicketBody>, -) -> AxumResult<Json<EcashTicketVerificationResponse>> { +) -> AxumResult<FormattedResponse<EcashTicketVerificationResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); let credential_data = &verify_ticket_body.credential; let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; // easy check: is there only a single payment attached? if credential_data.payment.spend_value != 1 { - return reject_ticket(EcashTicketVerificationRejection::MultipleTickets); + return reject_ticket(output, EcashTicketVerificationRejection::MultipleTickets); } let sn = &credential_data.encoded_serial_number(); @@ -83,11 +91,14 @@ async fn verify_ticket( // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { - return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { - today: today_ecash, - yesterday: yesterday_ecash, - received: spend_date, - }); + return reject_ticket( + output, + EcashTicketVerificationRejection::InvalidSpentDate { + today: today_ecash, + yesterday: yesterday_ecash, + received: spend_date, + }, + ); } // actual double spend detection with storage @@ -101,7 +112,7 @@ async fn verify_ticket( IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { warn!("Identical payInfo"); - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + return reject_ticket(output, EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending @@ -110,7 +121,7 @@ async fn verify_ticket( pub_key.to_base58_string() ); error!("UNIMPLEMENTED: blacklisting the double spend key"); - return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); + return reject_ticket(output, EcashTicketVerificationRejection::DoubleSpend); } } } @@ -119,7 +130,7 @@ async fn verify_ticket( // perform actual crypto verification if credential_data.verify(&verification_key).is_err() { - return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); + return reject_ticket(output, EcashTicketVerificationRejection::InvalidTicket); } // store credential and check whether it wasn't already there (due to a parallel request) @@ -127,10 +138,10 @@ async fn verify_ticket( .store_verified_ticket(credential_data, gateway_cosmos_addr) .await?; if !was_inserted { - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); + return reject_ticket(output, EcashTicketVerificationRejection::ReplayedTicket); } - Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) + Ok(output.to_response(EcashTicketVerificationResponse { verified: Ok(()) })) } #[utoipa::path( @@ -139,16 +150,23 @@ async fn verify_ticket( request_body = BatchRedeemTicketsBody, path = "/v1/ecash/batch-redeem-ecash-tickets", responses( - (status = 200, body = EcashBatchTicketRedemptionResponse), + (status = 200, content( + (EcashBatchTicketRedemptionResponse = "application/json"), + (EcashBatchTicketRedemptionResponse = "application/yaml"), + (EcashBatchTicketRedemptionResponse = "application/bincode") + )), (status = 400, body = String, description = "this nym-api is not an ecash signer in the current epoch"), - ) + ), + params(OutputParams) )] async fn batch_redeem_tickets( + Query(output): Query<OutputParams>, State(state): State<Arc<EcashState>>, // TODO in the future: make it send binary data rather than json Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>, -) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> { +) -> AxumResult<FormattedResponse<EcashBatchTicketRedemptionResponse>> { state.ensure_signer().await?; + let output = output.output.unwrap_or_default(); // 1. see if that gateway has even submitted any tickets let Some(provider_info) = state @@ -208,7 +226,7 @@ async fn batch_redeem_tickets( // 7. update the time of the last verification for this provider state.update_last_batch_verification(&provider_info).await?; - Ok(Json(EcashBatchTicketRedemptionResponse { + Ok(output.to_response(EcashBatchTicketRedemptionResponse { proposal_accepted: true, })) } @@ -224,7 +242,7 @@ async fn batch_redeem_tickets( ) )] #[deprecated] -async fn double_spending_filter_v1() -> AxumResult<Json<SpentCredentialsResponse>> { +async fn double_spending_filter_v1() -> AxumResult<FormattedResponse<SpentCredentialsResponse>> { AxumResult::Err(AxumErrorResponse::new( "permanently restricted", StatusCode::GONE, diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs index ef5deceb83..4910522a48 100644 --- a/nym-api/src/ecash/comm.rs +++ b/nym-api/src/ecash/comm.rs @@ -83,7 +83,7 @@ impl QueryCommunicationChannel { } } - async fn update_epoch_cache(&self) -> Result<RwLockWriteGuard<CachedEpoch>> { + async fn update_epoch_cache(&self) -> Result<RwLockWriteGuard<'_, CachedEpoch>> { let mut guard = self.cached_epoch.write().await; let epoch = ecash::client::Client::get_current_epoch(&self.nyxd_client).await?; diff --git a/nym-api/src/ecash/dkg/controller/error.rs b/nym-api/src/ecash/dkg/controller/error.rs index 4311ff7db5..34eafaf050 100644 --- a/nym-api/src/ecash/dkg/controller/error.rs +++ b/nym-api/src/ecash/dkg/controller/error.rs @@ -16,7 +16,7 @@ pub enum DkgError { StatePersistenceFailure { path: PathBuf, #[source] - source: EcashError, + source: Box<EcashError>, }, #[error("failed to query for the current DKG epoch state: {source}")] diff --git a/nym-api/src/ecash/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs index 51e0df1586..d5a8f804e3 100644 --- a/nym-api/src/ecash/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -11,7 +11,7 @@ use anyhow::{bail, Result}; use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState}; use nym_crypto::asymmetric::ed25519; use nym_dkg::bte::keys::KeyPair as DkgKeyPair; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{ShutdownManager, ShutdownToken}; use rand::rngs::OsRng; use rand::{CryptoRng, Rng, RngCore}; use std::path::PathBuf; @@ -73,7 +73,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> { persistent_state.save_to_file(save_path).map_err(|source| { DkgError::StatePersistenceFailure { path: save_path.to_path_buf(), - source, + source: Box::new(source), } }) } @@ -273,7 +273,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> { tick_duration < min } - pub(crate) async fn run(mut self, mut shutdown: TaskClient) { + pub(crate) async fn run(mut self, shutdown: ShutdownToken) { let mut interval = interval(self.polling_rate); interval.set_missed_tick_behavior(MissedTickBehavior::Delay); @@ -282,7 +282,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> { let mut last_polled = OffsetDateTime::now_utc(); let mut last_tick_duration = Default::default(); - while !shutdown.is_shutdown() { + while !shutdown.is_cancelled() { tokio::select! { _ = interval.tick() => { let now = OffsetDateTime::now_utc(); @@ -300,7 +300,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> { error!("failed to update the DKG state: {err}") } } - _ = shutdown.recv() => { + _ = shutdown.cancelled() => { trace!("DkgController: Received shutdown"); } } @@ -314,12 +314,12 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> { dkg_bte_keypair: DkgKeyPair, identity_key: ed25519::PublicKey, rng: R, - shutdown: &TaskManager, + shutdown_manager: &ShutdownManager, ) -> Result<()> where R: Sync + Send + 'static, { - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown_manager.clone_token("DKG controller"); let dkg_controller = DkgController::new( config, nyxd_client, diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 41cfd11d36..1dc0b0908e 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -399,7 +399,13 @@ impl<R: RngCore + CryptoRng> DkgController<R> { for (dealer_index, dealing) in dealings.into_iter() { // attempt to decrypt our portion let dk = self.state.dkg_keypair().private_key(); - let share = match decrypt_share(dk, receiver_index, &dealing.ciphertexts, None) { + let share = match decrypt_share( + dkg::params(), + dk, + receiver_index, + &dealing.ciphertexts, + None, + ) { Ok(share) => share, Err(err) => { error!("failed to decrypt share {human_index}/{total} generated from dealer {dealer_index}: {err} - can't generate the full key"); diff --git a/nym-api/src/ecash/dkg/state/serde_helpers.rs b/nym-api/src/ecash/dkg/state/serde_helpers.rs index 8057ef5d20..a19141e443 100644 --- a/nym-api/src/ecash/dkg/state/serde_helpers.rs +++ b/nym-api/src/ecash/dkg/state/serde_helpers.rs @@ -19,7 +19,7 @@ pub(super) mod bte_pk_serde { { let vec: Vec<u8> = Deserialize::deserialize(deserializer)?; PublicKeyWithProof::try_from_bytes(&vec) - .map_err(|err| Error::custom(format_args!("{:?}", err))) + .map_err(|err| Error::custom(format_args!("{err:?}"))) .map(Box::new) } } @@ -55,7 +55,7 @@ pub(super) mod recovered_keys { .into_iter() .map(|(idx, rec)| { RecoveredVerificationKeys::try_from_bytes(&rec) - .map_err(|err| D::Error::custom(format_args!("{:?}", err))) + .map_err(|err| D::Error::custom(format_args!("{err:?}"))) .map(|vk| (idx, vk)) }) .collect() diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs index 62b0955331..7e28e86e96 100644 --- a/nym-api/src/ecash/helpers.rs +++ b/nym-api/src/ecash/helpers.rs @@ -50,7 +50,7 @@ impl CredentialRequest for BlindSignRequestBody { } fn ecash_pubkey(&self) -> PublicKeyUser { - self.ecash_pubkey.clone() + self.ecash_pubkey } } @@ -60,7 +60,7 @@ pub(crate) fn blind_sign<C: CredentialRequest>( ) -> Result<BlindedSignature, EcashError> { Ok(nym_compact_ecash::scheme::withdrawal::issue( signing_key, - request.ecash_pubkey().clone(), + request.ecash_pubkey(), request.withdrawal_request(), request.expiration_date_timestamp(), request.ticketbook_type(), @@ -100,7 +100,7 @@ where &self, key: K, f: F, - ) -> Result<RwLockReadGuard<V>, EcashError> + ) -> Result<RwLockReadGuard<'_, V>, EcashError> where F: FnOnce() -> U, U: Future<Output = Result<V, EcashError>>, diff --git a/nym-api/src/ecash/keys/mod.rs b/nym-api/src/ecash/keys/mod.rs index bf315f8b1a..e7e8bf94c6 100644 --- a/nym-api/src/ecash/keys/mod.rs +++ b/nym-api/src/ecash/keys/mod.rs @@ -65,13 +65,13 @@ impl KeyPair { } } - pub async fn keys(&self) -> Result<RwLockReadGuard<KeyPairWithEpoch>, EcashError> { + pub async fn keys(&self) -> Result<RwLockReadGuard<'_, KeyPairWithEpoch>, EcashError> { let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; RwLockReadGuard::try_map(keypair_guard, |keypair| keypair.as_ref()) .map_err(|_| EcashError::KeyPairNotDerivedYet) } - pub async fn signing_key(&self) -> Result<RwLockReadGuard<SecretKeyAuth>, EcashError> { + pub async fn signing_key(&self) -> Result<RwLockReadGuard<'_, SecretKeyAuth>, EcashError> { let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?; RwLockReadGuard::try_map(keypair_guard, |keypair| { @@ -80,7 +80,7 @@ impl KeyPair { .map_err(|_| EcashError::KeyPairNotDerivedYet) } - pub async fn verification_key(&self) -> Option<RwLockReadGuard<VerificationKeyAuth>> { + pub async fn verification_key(&self) -> Option<RwLockReadGuard<'_, VerificationKeyAuth>> { RwLockReadGuard::try_map(self.get().await?, |maybe_keys| { maybe_keys.as_ref().map(|k| k.keys.verification_key_ref()) }) diff --git a/nym-api/src/ecash/state/cleaner.rs b/nym-api/src/ecash/state/cleaner.rs index 4b4c242e35..478443810b 100644 --- a/nym-api/src/ecash/state/cleaner.rs +++ b/nym-api/src/ecash/state/cleaner.rs @@ -6,7 +6,7 @@ use crate::node_status_api::models::NymApiStorageError; use crate::support::config::Config; use crate::support::storage::NymApiStorage; use nym_ecash_time::ecash_today_date; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::time::Duration; use time::Date; use tokio::task::JoinHandle; @@ -20,11 +20,15 @@ pub struct EcashBackgroundStateCleaner { verified_tickets_retention_period_days: u32, storage: NymApiStorage, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl EcashBackgroundStateCleaner { - pub fn new(global_config: &Config, storage: NymApiStorage, task_client: TaskClient) -> Self { + pub fn new( + global_config: &Config, + storage: NymApiStorage, + shutdown_token: ShutdownToken, + ) -> Self { EcashBackgroundStateCleaner { run_interval: global_config.ecash_signer.debug.stale_data_cleaner_interval, issued_ticketbooks_retention_period_days: global_config @@ -36,7 +40,7 @@ impl EcashBackgroundStateCleaner { .debug .verified_tickets_retention_period_days, storage, - task_client, + shutdown_token, } } @@ -68,7 +72,7 @@ impl EcashBackgroundStateCleaner { let mut ticker = tokio::time::interval(self.run_interval); loop { tokio::select! { - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { trace!("EcashBackgroundStateCleaner: Received shutdown"); break; } diff --git a/nym-api/src/ecash/state/global.rs b/nym-api/src/ecash/state/global.rs index 24d1ff00ff..d1e94d4cae 100644 --- a/nym-api/src/ecash/state/global.rs +++ b/nym-api/src/ecash/state/global.rs @@ -5,6 +5,7 @@ use crate::ecash::helpers::{ CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures, }; +use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::VerificationKeyAuth; use nym_validator_client::nyxd::AccountId; use time::Date; @@ -18,7 +19,7 @@ pub(crate) struct GlobalEcachState { pub(crate) coin_index_signatures: CachedImmutableEpochItem<IssuedCoinIndicesSignatures>, pub(crate) expiration_date_signatures: - CachedImmutableItems<Date, IssuedExpirationDateSignatures>, + CachedImmutableItems<(EpochId, Date), IssuedExpirationDateSignatures>, } impl GlobalEcachState { diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index b9e54ec711..3563825942 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -9,6 +9,7 @@ use crate::ecash::helpers::{ use crate::ecash::keys::KeyPair; use crate::ecash::storage::models::IssuedHash; use nym_api_requests::ecash::models::{CommitedDeposit, DepositId}; +use nym_coconut_dkg_common::types::EpochId; use nym_crypto::asymmetric::ed25519; use nym_ticketbooks_merkle::{ IssuedTicketbook, IssuedTicketbooksFullMerkleProof, IssuedTicketbooksMerkleTree, MerkleLeaf, @@ -143,7 +144,7 @@ pub(crate) struct LocalEcashState { pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem<IssuedCoinIndicesSignatures>, pub(crate) partial_expiration_date_signatures: - CachedImmutableItems<Date, IssuedExpirationDateSignatures>, + CachedImmutableItems<(EpochId, Date), IssuedExpirationDateSignatures>, // merkle trees for ticketbooks issued for particular expiration dates pub(crate) issued_merkle_trees: Arc<RwLock<HashMap<Date, DailyMerkleTree>>>, diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index d9ac0e5dce..78ca592acf 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -44,13 +44,12 @@ use nym_ecash_contract_common::deposit::{Deposit, DepositId}; use nym_ecash_contract_common::msg::ExecuteMsg; use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; use nym_ecash_time::{ecash_default_expiration_date, ecash_today_date}; -use nym_task::TaskClient; +use nym_task::ShutdownManager; use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof, MerkleLeaf}; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; use rand::{thread_rng, RngCore}; use std::collections::HashMap; -use std::ops::Deref; use time::{Date, OffsetDateTime}; use tokio::sync::{RwLockReadGuard, RwLockWriteGuard}; use tokio::task::JoinHandle; @@ -127,7 +126,7 @@ impl EcashState { key_pair: KeyPair, comm_channel: D, storage: NymApiStorage, - task_client: TaskClient, + shutdown_manager: &ShutdownManager, ) -> Self where C: LocalClient + Send + Sync + 'static, @@ -136,7 +135,11 @@ impl EcashState { Self { config: EcashStateConfig::new(global_config), background_cleaner_state: BackgroundCleanerState::WaitingStartup( - EcashBackgroundStateCleaner::new(global_config, storage.clone(), task_client), + EcashBackgroundStateCleaner::new( + global_config, + storage.clone(), + shutdown_manager.clone_token("ecash-state-data-cleaner"), + ), ), global: GlobalEcachState::new(contract_address), local: LocalEcashState::new( @@ -162,14 +165,11 @@ impl EcashState { } } - /// Ensures that this nym-api is one of ecash signers for the current epoch - pub(crate) async fn ensure_signer(&self) -> Result<()> { - if self.local.explicitly_disabled { - return Err(EcashError::NotASigner); - } - - let epoch_id = self.aux.current_epoch().await?; + pub(crate) async fn current_dkg_epoch(&self) -> Result<EpochId> { + self.aux.current_epoch().await + } + pub(crate) async fn is_dkg_signer(&self, epoch_id: EpochId) -> Result<bool> { let is_epoch_signer = self .local .active_signer @@ -183,29 +183,40 @@ impl EcashState { Ok(ecash_signers.iter().any(|c| c.cosmos_address == address)) }) .await?; + Ok(*is_epoch_signer) + } - if !is_epoch_signer.deref() { + /// Ensures that this nym-api is one of ecash signers for the current epoch + pub(crate) async fn ensure_signer(&self) -> Result<()> { + if self.local.explicitly_disabled { + return Err(EcashError::NotASigner); + } + + let epoch_id = self.current_dkg_epoch().await?; + let is_epoch_signer = self.is_dkg_signer(epoch_id).await?; + + if !is_epoch_signer { return Err(EcashError::NotASigner); } Ok(()) } - pub(crate) async fn ecash_signing_key(&self) -> Result<RwLockReadGuard<SecretKeyAuth>> { + pub(crate) async fn ecash_signing_key(&self) -> Result<RwLockReadGuard<'_, SecretKeyAuth>> { self.local.ecash_keypair.signing_key().await } #[allow(dead_code)] pub(crate) async fn current_master_verification_key( &self, - ) -> Result<RwLockReadGuard<VerificationKeyAuth>> { + ) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>> { self.master_verification_key(None).await } pub(crate) async fn master_verification_key( &self, epoch_id: Option<EpochId>, - ) -> Result<RwLockReadGuard<VerificationKeyAuth>> { + ) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>> { let epoch_id = match epoch_id { Some(id) => id, None => self.aux.current_epoch().await?, @@ -251,7 +262,7 @@ impl EcashState { pub(crate) async fn master_coin_index_signatures( &self, epoch_id: Option<EpochId>, - ) -> Result<RwLockReadGuard<IssuedCoinIndicesSignatures>> { + ) -> Result<RwLockReadGuard<'_, IssuedCoinIndicesSignatures>> { let epoch_id = match epoch_id { Some(id) => id, None => self.aux.current_epoch().await?, @@ -337,7 +348,7 @@ impl EcashState { pub(crate) async fn partial_coin_index_signatures( &self, epoch_id: Option<EpochId>, - ) -> Result<RwLockReadGuard<IssuedCoinIndicesSignatures>> { + ) -> Result<RwLockReadGuard<'_, IssuedCoinIndicesSignatures>> { let epoch_id = match epoch_id { Some(id) => id, None => self.aux.current_epoch().await?, @@ -394,10 +405,11 @@ impl EcashState { pub(crate) async fn master_expiration_date_signatures( &self, expiration_date: Date, - ) -> Result<RwLockReadGuard<IssuedExpirationDateSignatures>> { + epoch_id: EpochId, + ) -> Result<RwLockReadGuard<'_, IssuedExpirationDateSignatures>> { self.global .expiration_date_signatures - .get_or_init(expiration_date, || async { + .get_or_init((epoch_id, expiration_date), || async { // 1. sanity check to see if the expiration_date is not nonsense ensure_sane_expiration_date(expiration_date)?; @@ -405,7 +417,7 @@ impl EcashState { if let Some(master_sigs) = self .aux .storage - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id) .await? { return Ok(master_sigs); @@ -428,13 +440,16 @@ impl EcashState { // check if we're attempting to query ourselves, in that case just get local signature // rather than making the http query let partial = if Some(api.cosmos_address) == cosmos_address { - self.partial_expiration_date_signatures(expiration_date) + self.partial_expiration_date_signatures(expiration_date, epoch_id) .await? .signatures .clone() } else { api.api_client - .partial_expiration_date_signatures(Some(expiration_date)) + .partial_expiration_date_signatures( + Some(expiration_date), + Some(epoch_id), + ) .await? .signatures }; @@ -473,10 +488,11 @@ impl EcashState { pub(crate) async fn partial_expiration_date_signatures( &self, expiration_date: Date, - ) -> Result<RwLockReadGuard<IssuedExpirationDateSignatures>> { + epoch_id: EpochId, + ) -> Result<RwLockReadGuard<'_, IssuedExpirationDateSignatures>> { self.local .partial_expiration_date_signatures - .get_or_init(expiration_date, || async { + .get_or_init((epoch_id, expiration_date), || async { // 1. sanity check to see if the expiration_date is not nonsense ensure_sane_expiration_date(expiration_date)?; @@ -484,7 +500,7 @@ impl EcashState { if let Some(partial_sigs) = self .aux .storage - .get_partial_expiration_date_signatures(expiration_date) + .get_partial_expiration_date_signatures(expiration_date, epoch_id) .await? { return Ok(partial_sigs); @@ -714,7 +730,7 @@ impl EcashState { async fn get_updated_merkle_read( &self, expiration_date: Date, - ) -> Result<RwLockReadGuard<DailyMerkleTree>> { + ) -> Result<RwLockReadGuard<'_, DailyMerkleTree>> { let write_guard = self.get_updated_full_write(expiration_date).await?; // SAFETY: the entry was either not empty or we just inserted data in there, whilst never dropping the lock @@ -728,7 +744,7 @@ impl EcashState { async fn get_updated_full_write( &self, expiration_date: Date, - ) -> Result<RwLockWriteGuard<HashMap<Date, DailyMerkleTree>>> { + ) -> Result<RwLockWriteGuard<'_, HashMap<Date, DailyMerkleTree>>> { let mut write_guard = self.local.issued_merkle_trees.write().await; // double check if it's still empty in case another task has already grabbed the write lock and performed the update diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index e714600ef6..52b5940f04 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -128,6 +128,7 @@ pub trait EcashStorageManagerExt { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error>; async fn insert_partial_expiration_date_signatures( &self, @@ -139,6 +140,7 @@ pub trait EcashStorageManagerExt { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error>; async fn insert_master_expiration_date_signatures( &self, @@ -501,15 +503,17 @@ impl EcashStorageManagerExt for StorageManager { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" SELECT epoch_id as "epoch_id: u32", serialised_signatures FROM partial_expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) .fetch_optional(&self.connection_pool) .await @@ -535,15 +539,17 @@ impl EcashStorageManagerExt for StorageManager { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" SELECT epoch_id as "epoch_id: u32", serialised_signatures FROM global_expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) .fetch_optional(&self.connection_pool) .await diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index 197affb6a8..1131b42ccb 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -143,6 +143,7 @@ pub trait EcashStorageExt { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError>; async fn insert_partial_expiration_date_signatures( @@ -154,6 +155,7 @@ pub trait EcashStorageExt { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError>; async fn insert_master_expiration_date_signatures( @@ -456,10 +458,11 @@ impl EcashStorageExt for NymApiStorage { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError> { let Some(raw) = self .manager - .get_partial_expiration_date_signatures(expiration_date) + .get_partial_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); @@ -491,10 +494,11 @@ impl EcashStorageExt for NymApiStorage { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError> { let Some(raw) = self .manager - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 443e68c7e0..db067962b9 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,22 +1,25 @@ // Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; -use crate::node_describe_cache::DescribedNodes; +use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::config; -use crate::support::http::state::{AppState, ChainStatusCache, ForcedRefresh}; +use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; +use crate::support::http::state::force_refresh::ForcedRefresh; +use crate::support::http::state::AppState; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; +use crate::unstable_routes::v1::account::cache::AddressInfoCache; use async_trait::async_trait; use axum::Router; use axum_test::http::StatusCode; @@ -29,9 +32,10 @@ use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes}; use cw4::{Cw4Contract, MemberResponse}; use nym_api_requests::ecash::models::{ IssuedTicketbooksChallengeCommitmentRequestBody, IssuedTicketbooksChallengeCommitmentResponse, - IssuedTicketbooksForResponse, SignableMessageBody, + IssuedTicketbooksForResponse, }; use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse}; +use nym_api_requests::signable::SignableMessageBody; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails, }; @@ -55,10 +59,10 @@ use nym_crypto::asymmetric::ed25519; use nym_dkg::{NodeIndex, Threshold}; use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting}; use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse}; -use nym_task::TaskClient; +use nym_task::ShutdownManager; use nym_validator_client::nym_api::routes::{ - API_VERSION, ECASH_BLIND_SIGN, ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, - ECASH_ISSUED_TICKETBOOKS_FOR, ECASH_ROUTES, + ECASH_BLIND_SIGN, ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, ECASH_ISSUED_TICKETBOOKS_FOR, + ECASH_ROUTES, V1_API_VERSION, }; use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; @@ -1274,10 +1278,11 @@ impl TestFixture { AppState { nyxd_client, chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)), + ecash_signers_cache: Default::default(), + address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000), forced_refresh: ForcedRefresh::new(true), - nym_contract_cache: NymContractCache::new(), + mixnet_contract_cache: MixnetContractCache::new(), node_status_cache: NodeStatusCache::new(), - circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), storage, described_nodes_cache: SharedCache::<DescribedNodes>::new(), network_details: NetworkDetails::new( @@ -1285,6 +1290,7 @@ impl TestFixture { NymNetworkDetails::new_empty(), ), node_info_cache: unstable::NodeInfoCache::default(), + contract_info_cache: ContractDetailsCache::new(Duration::from_secs(42)), api_status: ApiStatusState::new(None), ecash_state: Arc::new(ecash_state), } @@ -1342,7 +1348,7 @@ impl TestFixture { staged_key_pair, comm_channel, storage.clone(), - TaskClient::dummy(), + &ShutdownManager::empty_mock(), ); // ideally this would have been generic, but that's way too much work @@ -1419,7 +1425,9 @@ impl TestFixture { async fn issue_ticketbook(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { let response = self .axum - .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) + .post(&format!( + "/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}" + )) .json(&req) .await; @@ -1434,7 +1442,7 @@ impl TestFixture { let response = self .axum .get(&format!( - "/{API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_FOR}/{expiration_date}" + "/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_FOR}/{expiration_date}" )) .await; @@ -1450,7 +1458,7 @@ impl TestFixture { let dummy_keypair = ed25519::KeyPair::new(&mut OsRng); self.axum .post(&format!( - "/{API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT}" + "/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT}" )) .json( &IssuedTicketbooksChallengeCommitmentRequestBody { @@ -1481,7 +1489,6 @@ mod credential_tests { use super::*; use crate::ecash::storage::EcashStorageExt; use axum::http::StatusCode; - use nym_task::TaskClient; use nym_ticketbooks_merkle::MerkleLeaf; #[tokio::test] @@ -1517,7 +1524,9 @@ mod credential_tests { let response = test_fixture .axum - .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) + .post(&format!( + "/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}" + )) .json(&request_body) .await; @@ -1570,7 +1579,7 @@ mod credential_tests { staged_key_pair, comm_channel, storage.clone(), - TaskClient::dummy(), + &ShutdownManager::empty_mock(), ); let deposit_id = 42; @@ -1701,7 +1710,9 @@ mod credential_tests { let response = test .axum - .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) + .post(&format!( + "/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}" + )) .json(&request_body) .await; diff --git a/nym-api/src/epoch_operations/error.rs b/nym-api/src/epoch_operations/error.rs index a608817425..14e74c3e90 100644 --- a/nym-api/src/epoch_operations/error.rs +++ b/nym-api/src/epoch_operations/error.rs @@ -56,9 +56,6 @@ pub enum RewardingError { source: rand::distributions::WeightedError, }, - #[error("could not obtain the current interval rewarding parameters")] - RewardingParamsRetrievalFailure, - #[error("{0}")] GenericError(#[from] anyhow::Error), } diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index d21fd1aadb..5f1e5a2f6b 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -5,12 +5,16 @@ use crate::epoch_operations::EpochAdvancer; use crate::support::caching::Cache; use cosmwasm_std::{Decimal, Fraction}; use nym_api_requests::models::NodeAnnotation; +use nym_mixnet_contract_common::helpers::IntoBaseDecimal; use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor}; -use nym_mixnet_contract_common::{EpochRewardedSet, ExecuteMsg, NodeId, RewardingParams}; +use nym_mixnet_contract_common::{ + EpochRewardedSet, ExecuteMsg, NodeId, RewardedSet, RewardingParams, +}; use serde::{Deserialize, Serialize}; +use std::cmp::max; use std::collections::HashMap; use tokio::sync::RwLockReadGuard; -use tracing::error; +use tracing::{debug, error}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub(crate) struct NodeWithPerformance { @@ -73,9 +77,140 @@ pub(super) fn stake_to_f64(stake: Decimal) -> f64 { } } +struct PerNodeWork { + active: WorkFactor, + standby: WorkFactor, +} + +struct NodeWorkCalculationComponents { + active_set_size: Decimal, + standby_set_size: Decimal, + per_node_work: PerNodeWork, +} + +impl NodeWorkCalculationComponents { + fn standby_set_work_share(&self) -> Decimal { + self.standby_set_size * self.per_node_work.standby + } + + fn active_set_work_share(&self) -> Decimal { + self.active_set_size * self.per_node_work.active + } +} + +fn default_node_work_calculation( + nodes: &RewardedSet, + global_rewarding_params: RewardingParams, +) -> NodeWorkCalculationComponents { + let per_node_work = PerNodeWork { + active: global_rewarding_params.active_node_work(), + standby: global_rewarding_params.standby_node_work(), + }; + // SANITY CHECK: + // SAFETY: 0 decimal places is within the range of `Decimal` + #[allow(clippy::unwrap_used)] + let standby_set_size = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap(); + #[allow(clippy::unwrap_used)] + let active_set_size = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap(); + + NodeWorkCalculationComponents { + active_set_size, + standby_set_size, + per_node_work, + } +} + +fn manual_node_work_calculation( + nodes: &RewardedSet, + global_rewarding_params: RewardingParams, +) -> NodeWorkCalculationComponents { + // calculate everything manually based on the actual rewarded set on hand + // but always attempt to minimise the node work, so take the maximum values + // of the set sizes between new and old parameters + // (more nodes = smaller per-node work as it has to be spread through more entries) + let rewarded_set_size = max( + global_rewarding_params.rewarded_set.rewarded_set_size(), + nodes.rewarded_set_size() as u32, + ); + let standby_set_size = max( + global_rewarding_params.rewarded_set.standby, + nodes.standby_set_size() as u32, + ); + // the unwraps here are fine as we're guaranteed an `u32` is going to fit in a Decimal with 0 decimal places + #[allow(clippy::unwrap_used)] + let rewarded_set_size_dec = rewarded_set_size.into_base_decimal().unwrap(); + #[allow(clippy::unwrap_used)] + let standby_set_size_dec = standby_set_size.into_base_decimal().unwrap(); + #[allow(clippy::unwrap_used)] + let active_set_size = rewarded_set_size + .saturating_sub(standby_set_size) + .into_base_decimal() + .unwrap(); + + let standby_node_work = global_rewarding_params + .interval + .standby_node_work(rewarded_set_size_dec, standby_set_size_dec); + let active_node_work = global_rewarding_params + .interval + .active_node_work(standby_node_work); + let per_node_work = PerNodeWork { + active: active_node_work, + standby: standby_node_work, + }; + + NodeWorkCalculationComponents { + active_set_size, + standby_set_size: standby_set_size_dec, + per_node_work, + } +} + +fn determine_per_node_work( + nodes: &RewardedSet, + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + global_rewarding_params: RewardingParams, +) -> PerNodeWork { + // currently we are using constant omega for nodes, but that will change with tickets + // or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere + let res = if nodes.matches_parameters(global_rewarding_params.rewarded_set) { + default_node_work_calculation(nodes, global_rewarding_params) + } else { + error!("the current rewarded set does not much current rewarding parameters. this could only be expected if rewarded set distribution has been changed mid-epoch"); + manual_node_work_calculation(nodes, global_rewarding_params) + }; + + let active_node_work_factor = res.per_node_work.active; + let standby_node_work_factor = res.per_node_work.standby; + + debug!("using {active_node_work_factor} as active node work factor and {standby_node_work_factor} as standby node work factor"); + + let standby_share = res.standby_set_work_share(); + let active_share = res.active_set_work_share(); + let total_work = standby_share + active_share; + + // this HAS TO blow up. there's no recovery + #[allow(clippy::panic)] + if total_work > Decimal::one() { + panic!("work calculation logic is flawed! somehow the total work in the system is greater than 1! \ + total work={total_work}, \ + active set share={active_share}, \ + standby share={standby_share}, \ + active node work factor={active_node_work_factor}, \ + standby node work factor={standby_node_work_factor}, \ + active set size={} \ + standby set size={}", res.active_set_size, res.standby_set_size); + } + + PerNodeWork { + active: active_node_work_factor, + standby: standby_node_work_factor, + } +} + impl EpochAdvancer { fn load_performance( - status_cache: &Option<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>>, + status_cache: &Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>>, node_id: NodeId, ) -> NodeWithPerformance { let Some(status_cache) = status_cache.as_ref() else { @@ -99,23 +234,9 @@ impl EpochAdvancer { global_rewarding_params: RewardingParams, ) -> Vec<RewardedNodeWithParams> { let nodes = &nodes.assignment; - // currently we are using constant omega for nodes, but that will change with tickets - // or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere - let active_node_work_factor = global_rewarding_params.active_node_work(); - let standby_node_work_factor = global_rewarding_params.standby_node_work(); - - // SANITY CHECK: - // SAFETY: 0 decimal places is within the range of `Decimal` - #[allow(clippy::unwrap_used)] - let standby_share = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap() - * standby_node_work_factor; - #[allow(clippy::unwrap_used)] - let active_share = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap() - * active_node_work_factor; - let total_work = standby_share + active_share; - - // this HAS TO blow up. there's no recovery - assert!(total_work <= Decimal::one(), "work calculation logic is flawed! somehow the total work in the system is greater than 1!"); + let nodes_work = determine_per_node_work(nodes, global_rewarding_params); + let active_node_work_factor = nodes_work.active; + let standby_node_work_factor = nodes_work.standby; let status_cache = self.status_cache.node_annotations().await; if status_cache.is_none() { @@ -161,6 +282,9 @@ impl EpochAdvancer { #[cfg(test)] mod tests { use super::*; + use nym_contracts_common::Percent; + use nym_mixnet_contract_common::reward_params::RewardedSetParams; + use nym_mixnet_contract_common::IntervalRewardParams; fn compare_large_floats(a: f64, b: f64) { // for very large floats, allow for smaller larger epsilon @@ -171,9 +295,9 @@ mod tests { }; if a > b { - assert!(a - b < epsilon, "{} != {}", a, b) + assert!(a - b < epsilon, "{a} != {b}") } else { - assert!(b - a < epsilon, "{} != {}", a, b) + assert!(b - a < epsilon, "{a} != {b}") } } @@ -206,4 +330,72 @@ mod tests { compare_large_floats(expected_f64, stake_to_f64(decimal)) } } + + fn dummy_rewarding_params() -> RewardingParams { + RewardingParams { + interval: IntervalRewardParams { + reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), + epoch_reward_budget: Decimal::from_ratio(100_000_000_000_000u128, 1234u32) + * Decimal::percent(1), + stake_saturation_point: Decimal::from_ratio(123_456_000_000_000u128, 313u32), + sybil_resistance: Percent::from_percentage_value(23).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(1).unwrap(), + }, + rewarded_set: RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 20, + }, + } + } + + #[test] + fn determining_nodes_work() { + let params = dummy_rewarding_params(); + // matched parameters + let rewarded_set = RewardedSet { + entry_gateways: (1..) + .take(params.rewarded_set.entry_gateways as usize) + .collect(), + exit_gateways: (1000..) + .take(params.rewarded_set.exit_gateways as usize) + .collect(), + layer1: (2000..) + .take(params.rewarded_set.mixnodes as usize / 3) + .collect(), + layer2: (3000..) + .take(params.rewarded_set.mixnodes as usize / 3) + .collect(), + layer3: (4000..) + .take(params.rewarded_set.mixnodes as usize / 3) + .collect(), + standby: (5000..) + .take(params.rewarded_set.standby as usize) + .collect(), + }; + + let work = determine_per_node_work(&rewarded_set, params); + assert_eq!(work.active, params.active_node_work()); + assert_eq!(work.standby, params.standby_node_work()); + + // updated + // here we're interested in the fact that the calculation does not panic, i.e. total work <= 1 + let params = dummy_rewarding_params(); + let rewarded_set = RewardedSet { + entry_gateways: (1..).take(250).collect(), + exit_gateways: (1000..).take(100).collect(), + layer1: (2000..).take(10).collect(), + layer2: (3000..).take(10).collect(), + layer3: (4000..).take(10).collect(), + standby: (5000..).take(5).collect(), + }; + + let work = determine_per_node_work(&rewarded_set, params); + assert_ne!(work.active, params.active_node_work()); + assert_ne!(work.standby, params.standby_node_work()); + } } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index 0a1f8bb566..2322d4be24 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -12,17 +12,17 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. -use crate::node_describe_cache::DescribedNodes; +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::{NodeStatusCache, ONE_DAY}; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::RefreshRequester; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; use error::RewardingError; pub(crate) use helpers::RewardedNodeWithParams; use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval}; -use nym_task::{TaskClient, TaskManager}; -use std::collections::HashSet; +use nym_task::{ShutdownManager, ShutdownToken}; use std::time::Duration; use tokio::time::sleep; use tracing::{error, info, trace, warn}; @@ -38,7 +38,8 @@ mod transition_beginning; // this is struct responsible for advancing an epoch pub struct EpochAdvancer { nyxd_client: Client, - nym_contract_cache: NymContractCache, + mixnet_contract_cache: MixnetContractCache, + mixnet_contract_cache_refresh_requester: RefreshRequester, described_cache: SharedCache<DescribedNodes>, status_cache: NodeStatusCache, storage: NymApiStorage, @@ -53,14 +54,16 @@ impl EpochAdvancer { pub(crate) fn new( nyxd_client: Client, - nym_contract_cache: NymContractCache, + mixnet_contract_cache: MixnetContractCache, + mixnet_contract_cache_refresh_requester: RefreshRequester, status_cache: NodeStatusCache, described_cache: SharedCache<DescribedNodes>, storage: NymApiStorage, ) -> Self { EpochAdvancer { nyxd_client, - nym_contract_cache, + mixnet_contract_cache, + mixnet_contract_cache_refresh_requester, described_cache, status_cache, storage, @@ -120,7 +123,7 @@ impl EpochAdvancer { let epoch_end = interval.current_epoch_end(); - let nym_nodes = self.nym_contract_cache.nym_nodes().await; + let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; if nym_nodes.is_empty() { // that's a bit weird, but ok @@ -162,62 +165,15 @@ impl EpochAdvancer { let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; - Ok(()) - } - - // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. - // instead clients are meant to be filtering out them themselves based on the provided scores. - async fn update_legacy_node_blacklist( - &mut self, - interval: &Interval, - ) -> Result<(), RewardingError> { - info!("Updating blacklists"); - - let mut mix_blacklist_add = HashSet::new(); - let mut mix_blacklist_remove = HashSet::new(); - let mut gate_blacklist_add = HashSet::new(); - let mut gate_blacklist_remove = HashSet::new(); - - let mixnodes = self - .storage - .get_all_avg_mix_reliability_in_last_24hr(interval.current_epoch_end_unix_timestamp()) - .await?; - let gateways = self - .storage - .get_all_avg_gateway_reliability_in_last_24hr( - interval.current_epoch_end_unix_timestamp(), - ) - .await?; - - // TODO: Make thresholds configurable - for mix in mixnodes { - if mix.value() <= 50.0 { - mix_blacklist_add.insert(mix.mix_id()); - } else { - mix_blacklist_remove.insert(mix.mix_id()); - } - } - - self.nym_contract_cache - .update_mixnodes_blacklist(mix_blacklist_add, mix_blacklist_remove) - .await; - - for gateway in gateways { - if gateway.value() <= 50.0 { - gate_blacklist_add.insert(gateway.node_id()); - } else { - gate_blacklist_remove.insert(gateway.node_id()); - } - } - - self.nym_contract_cache - .update_gateways_blacklist(gate_blacklist_add, gate_blacklist_remove) - .await; + // after all epoch progression has finished - force refresh the mixnet contract cache, + // so we'd know about new rewarded set (that's easier than manually overwriting the data) + self.mixnet_contract_cache_refresh_requester + .request_cache_refresh(); Ok(()) } - async fn wait_until_epoch_end(&mut self, shutdown: &mut TaskClient) -> Option<Interval> { + async fn wait_until_epoch_end(&mut self, shutdown_token: &ShutdownToken) -> Option<Interval> { const POLL_INTERVAL: Duration = Duration::from_secs(120); loop { @@ -228,7 +184,7 @@ impl EpochAdvancer { _ = sleep(POLL_INTERVAL) => { continue }, - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("wait_until_epoch_end: Received shutdown"); break None } @@ -256,7 +212,7 @@ impl EpochAdvancer { _ = sleep(wait_time) => { }, - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("wait_until_epoch_end: Received shutdown"); break None } @@ -265,23 +221,25 @@ impl EpochAdvancer { } } - pub(crate) async fn run(&mut self, mut shutdown: TaskClient) -> Result<(), RewardingError> { + pub(crate) async fn run( + &mut self, + shutdown_token: ShutdownToken, + ) -> Result<(), RewardingError> { info!("waiting for initial contract cache values before we can start rewarding"); - self.nym_contract_cache.wait_for_initial_values().await; + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await; info!("waiting for initial self-described cache values before we can start rewarding"); self.described_cache.naive_wait_for_initial_values().await; - while !shutdown.is_shutdown() { - let interval_details = match self.wait_until_epoch_end(&mut shutdown).await { + while !shutdown_token.is_cancelled() { + let interval_details = match self.wait_until_epoch_end(&shutdown_token).await { // received a shutdown None => return Ok(()), Some(interval) => interval, }; - if let Err(err) = self.update_legacy_node_blacklist(&interval_details).await { - error!("failed to update the node blacklist - {err}"); - continue; - } + if let Err(err) = self.perform_epoch_operations(interval_details).await { error!("failed to perform epoch operations - {err}"); sleep(Duration::from_secs(30)).await; @@ -293,21 +251,23 @@ impl EpochAdvancer { pub(crate) fn start( nyxd_client: Client, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, + mixnet_contract_cache_refresh_requester: RefreshRequester, status_cache: &NodeStatusCache, described_cache: SharedCache<DescribedNodes>, storage: &NymApiStorage, - shutdown: &TaskManager, + shutdown_manager: &ShutdownManager, ) { - let mut rewarded_set_updater = EpochAdvancer::new( + let mut epoch_advancer = EpochAdvancer::new( nyxd_client, nym_contract_cache.to_owned(), + mixnet_contract_cache_refresh_requester, status_cache.to_owned(), described_cache, storage.to_owned(), ); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await }); + let shutdown_listener = shutdown_manager.clone_token("epoch-advancer"); + tokio::spawn(async move { epoch_advancer.run(shutdown_listener).await }); } } diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 40c1124b05..d513579ef2 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -6,7 +6,9 @@ use crate::epoch_operations::helpers::stake_to_f64; use crate::EpochAdvancer; use cosmwasm_std::Decimal; use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams}; -use nym_mixnet_contract_common::{EpochState, NodeId, NymNodeDetails, RewardedSet}; +use nym_mixnet_contract_common::{ + EpochState, NodeId, NymNodeDetails, RewardedSet, RewardingParams, +}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; use std::collections::HashSet; @@ -25,14 +27,14 @@ enum AvailableRole { } #[derive(Debug, Clone)] -struct NodeWithStakeAndPerformance { +struct NodeWithSaturationAndPerformance { node_id: NodeId, available_roles: Vec<AvailableRole>, - total_stake: Decimal, + saturation: Decimal, performance: Performance, } -impl NodeWithStakeAndPerformance { +impl NodeWithSaturationAndPerformance { fn to_selection_weight(&self) -> f64 { let scaled_performance = match self.performance.checked_pow(20) { Ok(perf) => perf, @@ -42,7 +44,7 @@ impl NodeWithStakeAndPerformance { } }; - let scaled_stake = self.total_stake * scaled_performance; + let scaled_stake = self.saturation * scaled_performance; stake_to_f64(scaled_stake) } @@ -62,7 +64,7 @@ impl NodeWithStakeAndPerformance { impl EpochAdvancer { fn determine_rewarded_set( &self, - nodes: Vec<NodeWithStakeAndPerformance>, + nodes: Vec<NodeWithSaturationAndPerformance>, spec: RewardedSetParams, ) -> Result<RewardedSet, RewardingError> { if nodes.is_empty() { @@ -204,7 +206,8 @@ impl EpochAdvancer { async fn attach_performance_to_eligible_nodes( &self, nym_nodes: &[NymNodeDetails], - ) -> Vec<NodeWithStakeAndPerformance> { + reward_params: &RewardingParams, + ) -> Vec<NodeWithSaturationAndPerformance> { let mut with_performance = Vec::new(); // SAFETY: the cache MUST HAVE been initialised before now @@ -218,7 +221,7 @@ impl EpochAdvancer { for nym_node in nym_nodes { let node_id = nym_node.node_id(); - let total_stake = nym_node.total_stake(); + let saturation = nym_node.rewarding_details.bond_saturation(reward_params); let Some(self_described) = described_cache.get_description(&node_id) else { continue; @@ -230,7 +233,7 @@ impl EpochAdvancer { }; let performance = annotation.detailed_performance.to_rewarding_performance(); - debug!("nym-node {node_id}: stake: {total_stake}, performance: {performance}"); + debug!("nym-node {node_id}: saturation: {saturation}, performance: {performance}"); let mut available_roles = Vec::new(); if self_described.declared_role.mixnode { @@ -248,10 +251,10 @@ impl EpochAdvancer { continue; } - with_performance.push(NodeWithStakeAndPerformance { + with_performance.push(NodeWithSaturationAndPerformance { node_id: nym_node.node_id(), available_roles, - total_stake, + saturation, performance, }) } @@ -273,13 +276,8 @@ impl EpochAdvancer { } info!("attempting to assign the rewarded set for the upcoming epoch..."); - let nodes_with_performance = - self.attach_performance_to_eligible_nodes(nym_nodes).await; - if let Err(err) = self - ._update_rewarded_set_and_advance_epoch(nodes_with_performance) - .await - { + if let Err(err) = self._update_rewarded_set_and_advance_epoch(nym_nodes).await { error!("FAILED to assign the rewarded set... - {err}"); Err(err) } else { @@ -300,15 +298,19 @@ impl EpochAdvancer { async fn _update_rewarded_set_and_advance_epoch( &self, - all_nodes: Vec<NodeWithStakeAndPerformance>, + nym_nodes: &[NymNodeDetails], ) -> Result<(), RewardingError> { // we grab rewarding parameters here as they might have gotten updated when performing epoch actions let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?; debug!("Rewarding parameters: {rewarding_parameters:?}"); + let nodes_with_performance = self + .attach_performance_to_eligible_nodes(nym_nodes, &rewarding_parameters) + .await; + let new_rewarded_set = - self.determine_rewarded_set(all_nodes, rewarding_parameters.rewarded_set)?; + self.determine_rewarded_set(nodes_with_performance, rewarding_parameters.rewarded_set)?; debug!("New rewarded set: {:?}", new_rewarded_set); diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index 46f082502b..bf95c2f3a1 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -70,6 +70,8 @@ impl EpochAdvancer { Ok(()) } + // SAFETY: `EpochAdvancer` is not started until cache is properly initialised + #[allow(clippy::unwrap_used)] pub(crate) async fn nodes_to_reward( &self, ) -> Result<Vec<RewardedNodeWithParams>, RewardingError> { @@ -79,25 +81,23 @@ impl EpochAdvancer { Ok(rewarded_set) => rewarded_set, Err(err) => { warn!("failed to obtain the current rewarded set: {err}. falling back to the cached version"); - self.nym_contract_cache + self.mixnet_contract_cache .rewarded_set_owned() .await - .into_inner() + .unwrap() .into() } }; // we only need reward parameters for active set work factor and rewarded/active set sizes; // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync - let Some(reward_params) = self - .nym_contract_cache + + // SAFETY: `EpochAdvancer` is not started until cache is properly initialised + let reward_params = self + .mixnet_contract_cache .interval_reward_params() .await - .into_inner() - else { - error!("failed to obtain the current interval rewarding parameters. can't determine rewards without them"); - return Err(RewardingError::RewardingParamsRetrievalFailure); - }; + .unwrap(); Ok(self .load_nodes_for_rewarding(&rewarded_set, reward_params) diff --git a/nym-api/src/key_rotation/mod.rs b/nym-api/src/key_rotation/mod.rs new file mode 100644 index 0000000000..44f11be7ef --- /dev/null +++ b/nym-api/src/key_rotation/mod.rs @@ -0,0 +1,178 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::support::caching::refresher::{CacheUpdateWatcher, RefreshRequester}; +use nym_mixnet_contract_common::{Interval, KeyRotationState}; +use nym_task::ShutdownToken; +use time::OffsetDateTime; +use tracing::{debug, error, info, trace}; + +#[derive(Debug)] +struct ContractData { + interval: Interval, + key_rotation_state: KeyRotationState, +} + +impl ContractData { + fn rotation_id(&self) -> u32 { + self.key_rotation_state + .key_rotation_id(self.interval.current_epoch_absolute_id()) + } + + fn upcoming_rotation_id(&self) -> u32 { + self.rotation_id() + 1 + } + + fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 { + let elapsed = (now - self.interval.current_epoch_start()).as_seconds_f32(); + elapsed / self.interval.epoch_length().as_secs_f32() + } + + fn epochs_until_next_rotation(&self) -> Option<f32> { + let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc()); + + if !(0. ..=1.).contains(¤t_epoch_progress) { + error!("epoch seems to be stuck (current progress is at {:.1}%) - can't progress key rotation!", current_epoch_progress * 100.); + return None; + } + + let next_rotation_epoch = self + .key_rotation_state + .next_rotation_starting_epoch_id(self.interval.current_epoch_absolute_id()); + + let Some(full_epochs) = + (next_rotation_epoch - self.interval.current_epoch_absolute_id()).checked_sub(1) + else { + error!("CRITICAL FAILURE: invalid epoch calculation"); + return None; + }; + + Some((1. - current_epoch_progress) + full_epochs as f32) + } +} + +// 'simple' task responsible for making sure nym-api refreshes its self-described cache +// just before the next key rotation so it would have all the keys available +pub(crate) struct KeyRotationController { + pub(crate) last_described_refreshed_for: Option<u32>, + + pub(crate) describe_cache_refresher: RefreshRequester, + pub(crate) contract_cache_watcher: CacheUpdateWatcher, + pub(crate) contract_cache: MixnetContractCache, +} + +impl KeyRotationController { + pub(crate) fn new( + describe_cache_refresher: RefreshRequester, + contract_cache_watcher: CacheUpdateWatcher, + contract_cache: MixnetContractCache, + ) -> KeyRotationController { + KeyRotationController { + last_described_refreshed_for: None, + describe_cache_refresher, + contract_cache_watcher, + contract_cache, + } + } + + // SAFETY: this function is only called after cache has already been initialised + #[allow(clippy::unwrap_used)] + async fn get_contract_data(&self) -> ContractData { + let key_rotation_state = self.contract_cache.get_key_rotation_state().await.unwrap(); + let interval = self.contract_cache.current_interval().await.unwrap(); + ContractData { + interval, + key_rotation_state, + } + } + + async fn handle_contract_cache_update(&mut self) { + let updated = self.get_contract_data().await; + + debug!( + "current key rotation: {}", + updated + .key_rotation_state + .key_rotation_id(updated.interval.current_epoch_absolute_id()) + ); + + // if we're only 1/4 epoch away from the next rotation, and we haven't yet performed the refresh, + // update the self-described cache, as all nodes should have already pre-announced their new sphinx keys + if let Some(remaining) = updated.epochs_until_next_rotation() { + debug!("{remaining} epoch(s) remaining until next key rotation"); + let expected = Some(updated.upcoming_rotation_id()); + if remaining < 0.25 && self.last_described_refreshed_for != expected { + info!("{remaining} epoch(s) remaining until next key rotation - requesting full refresh of self-described cache"); + self.describe_cache_refresher.request_cache_refresh(); + self.last_described_refreshed_for = expected; + } + } + } + + async fn run(&mut self, shutdown_token: ShutdownToken) { + self.contract_cache.naive_wait_for_initial_values().await; + self.handle_contract_cache_update().await; + + while !shutdown_token.is_cancelled() { + tokio::select! { + biased; + _ = shutdown_token.cancelled() => { + trace!("KeyRotationController: Received shutdown"); + } + _ = self.contract_cache_watcher.changed() => { + self.handle_contract_cache_update().await + } + } + } + + trace!("KeyRotationController: exiting") + } + + pub(crate) fn start(mut self, shutdown_token: ShutdownToken) { + tokio::spawn(async move { self.run(shutdown_token).await }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Timestamp; + use std::time::Duration; + + // Sun Jun 15 2025 15:06:40 GMT+0000 + const DUMMY_TIMESTAMP: i64 = 1750000000; + + fn dummy_contract_data() -> ContractData { + let mut env = mock_env(); + + env.block.time = Timestamp::from_seconds(DUMMY_TIMESTAMP as u64); + ContractData { + interval: Interval::init_interval(24, Duration::from_secs(60 * 60), &env), + key_rotation_state: KeyRotationState { + validity_epochs: 0, + initial_epoch_id: 0, + }, + } + } + + #[test] + fn current_epoch_progress() { + let dummy_data = dummy_contract_data(); + + let epoch_start = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP).unwrap(); + let quarter_in = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 15 * 60).unwrap(); + let half_in = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 30 * 60).unwrap(); + let next = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 60 * 60).unwrap(); + let one_and_half = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 90 * 60).unwrap(); + let past_value = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP - 30 * 60).unwrap(); + + assert_eq!(dummy_data.current_epoch_progress(epoch_start), 0.); + assert_eq!(dummy_data.current_epoch_progress(quarter_in), 0.25); + assert_eq!(dummy_data.current_epoch_progress(half_in), 0.5); + assert_eq!(dummy_data.current_epoch_progress(next), 1.); + assert_eq!(dummy_data.current_epoch_progress(one_and_half), 1.5); + assert_eq!(dummy_data.current_epoch_progress(past_value), -0.5); + } +} diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 28cf5cd48e..a36027bd3c 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,42 +1,41 @@ // Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] - use crate::epoch_operations::EpochAdvancer; use crate::support::cli; use crate::support::storage; use ::nym_config::defaults::setup_env; use clap::Parser; +use mixnet_contract_cache::cache::MixnetContractCache; use node_status_api::NodeStatusCache; -use nym_bin_common::logging::setup_tracing_logger; -use nym_contract_cache::cache::NymContractCache; use support::nyxd; use tracing::{info, trace}; mod circulating_supply_api; mod ecash; mod epoch_operations; +mod key_rotation; +pub(crate) mod mixnet_contract_cache; pub(crate) mod network; mod network_monitor; pub(crate) mod node_describe_cache; +mod node_performance; pub(crate) mod node_status_api; -pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; +mod signers_cache; mod status; pub(crate) mod support; +mod unstable_routes; -// TODO rocket: remove all such Todos once rocket is phased out completely #[tokio::main] async fn main() -> Result<(), anyhow::Error> { cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] { // instrument tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time console_subscriber::init(); + } else { + nym_bin_common::logging::setup_tracing_logger(); }} - setup_tracing_logger(); - info!("Starting nym api..."); let args = cli::Cli::parse(); diff --git a/nym-api/src/mixnet_contract_cache/cache/data.rs b/nym-api/src/mixnet_contract_cache/cache/data.rs new file mode 100644 index 0000000000..d648c1037b --- /dev/null +++ b/nym-api/src/mixnet_contract_cache/cache/data.rs @@ -0,0 +1,43 @@ +// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::ConfigScoreDataResponse; +use nym_mixnet_contract_common::{ + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, KeyRotationState, NymNodeDetails, + RewardingParams, +}; +use nym_topology::CachedEpochRewardedSet; + +#[derive(Clone)] +pub(crate) struct ConfigScoreData { + pub(crate) config_score_params: ConfigScoreParams, + pub(crate) nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, +} + +impl From<ConfigScoreData> for ConfigScoreDataResponse { + fn from(value: ConfigScoreData) -> Self { + ConfigScoreDataResponse { + parameters: value.config_score_params.into(), + version_history: value + .nym_node_version_history + .into_iter() + .map(Into::into) + .collect(), + } + } +} + +pub(crate) struct MixnetContractCacheData { + pub(crate) rewarding_denom: String, + + pub(crate) legacy_mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, + pub(crate) legacy_gateways: Vec<LegacyGatewayBondWithId>, + pub(crate) nym_nodes: Vec<NymNodeDetails>, + pub(crate) rewarded_set: CachedEpochRewardedSet, + + pub(crate) config_score_data: ConfigScoreData, + pub(crate) current_reward_params: RewardingParams, + pub(crate) current_interval: Interval, + pub(crate) key_rotation_state: KeyRotationState, +} diff --git a/nym-api/src/mixnet_contract_cache/cache/mod.rs b/nym-api/src/mixnet_contract_cache/cache/mod.rs new file mode 100644 index 0000000000..9279c989ec --- /dev/null +++ b/nym-api/src/mixnet_contract_cache/cache/mod.rs @@ -0,0 +1,244 @@ +// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; +use crate::node_describe_cache::refresh::RefreshData; +use crate::support::caching::cache::{SharedCache, UninitialisedCache}; +use crate::support::caching::Cache; +use data::MixnetContractCacheData; +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use nym_api_requests::models::{CirculatingSupplyResponse, MixnodeStatus}; +use nym_contracts_common::truncate_decimal; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::{ + Interval, KeyRotationState, NodeId, NymNodeDetails, RewardingParams, +}; +use nym_topology::CachedEpochRewardedSet; +use nym_validator_client::nyxd::Coin; +use time::OffsetDateTime; +use tokio::sync::RwLockReadGuard; + +pub(crate) mod data; +pub(crate) mod refresher; + +const TOTAL_SUPPLY_AMOUNT: u128 = 1_000_000_000_000_000; // 1B tokens + +#[derive(Clone)] +pub struct MixnetContractCache { + pub(crate) inner: SharedCache<MixnetContractCacheData>, +} + +impl MixnetContractCache { + pub(crate) fn new() -> Self { + MixnetContractCache { + inner: SharedCache::new(), + } + } + + pub(crate) fn inner(&self) -> SharedCache<MixnetContractCacheData> { + self.inner.clone() + } + + async fn get_owned<T>( + &self, + fn_arg: impl FnOnce(&MixnetContractCacheData) -> T, + ) -> Result<T, UninitialisedCache> { + Ok(fn_arg(&**self.inner.get().await?)) + } + + async fn get<'a, T: 'a>( + &'a self, + fn_arg: impl FnOnce(&Cache<MixnetContractCacheData>) -> &T, + ) -> Result<RwLockReadGuard<'a, T>, UninitialisedCache> { + let guard = self.inner.get().await?; + Ok(RwLockReadGuard::map(guard, fn_arg)) + } + + pub async fn cache_timestamp(&self) -> OffsetDateTime { + let Ok(cache) = self.inner.get().await else { + return OffsetDateTime::UNIX_EPOCH; + }; + + cache.timestamp() + } + + pub async fn all_cached_legacy_mixnodes( + &self, + ) -> Option<RwLockReadGuard<'_, Vec<LegacyMixNodeDetailsWithLayer>>> { + self.get(|c| &c.legacy_mixnodes).await.ok() + } + + pub async fn legacy_gateway_owner(&self, node_id: NodeId) -> Option<String> { + let Ok(cache) = self.inner.get().await else { + return Default::default(); + }; + + cache + .legacy_gateways + .iter() + .find(|gateway| gateway.node_id == node_id) + .map(|gateway| gateway.owner.to_string()) + } + + pub async fn all_cached_legacy_gateways( + &self, + ) -> Option<RwLockReadGuard<'_, Vec<LegacyGatewayBondWithId>>> { + self.get(|c| &c.legacy_gateways).await.ok() + } + + pub async fn all_cached_nym_nodes(&self) -> Option<RwLockReadGuard<'_, Vec<NymNodeDetails>>> { + self.get(|c| &c.nym_nodes).await.ok() + } + + pub async fn legacy_mixnodes_all(&self) -> Vec<LegacyMixNodeDetailsWithLayer> { + self.get_owned(|c| c.legacy_mixnodes.clone()) + .await + .unwrap_or_default() + } + + pub async fn legacy_mixnodes_all_basic(&self) -> Vec<LegacyMixNodeBondWithLayer> { + self.legacy_mixnodes_all() + .await + .into_iter() + .map(|bond| bond.bond_information) + .collect() + } + + pub async fn legacy_gateways_all(&self) -> Vec<LegacyGatewayBondWithId> { + self.get_owned(|c| c.legacy_gateways.clone()) + .await + .unwrap_or_default() + } + + pub async fn nym_nodes(&self) -> Vec<NymNodeDetails> { + self.get_owned(|c| c.nym_nodes.clone()) + .await + .unwrap_or_default() + } + + pub async fn cached_rewarded_set( + &self, + ) -> Result<Cache<CachedEpochRewardedSet>, UninitialisedCache> { + let cache = self.inner.get().await?; + Ok(Cache::as_mapped(&cache, |c| c.rewarded_set.clone())) + } + + pub async fn rewarded_set(&self) -> Option<RwLockReadGuard<'_, CachedEpochRewardedSet>> { + self.get(|c| &c.rewarded_set).await.ok() + } + + pub async fn rewarded_set_owned(&self) -> Result<CachedEpochRewardedSet, UninitialisedCache> { + self.get_owned(|c| c.rewarded_set.clone()).await + } + + pub async fn maybe_config_score_data(&self) -> Result<ConfigScoreData, UninitialisedCache> { + self.get_owned(|c| c.config_score_data.clone()).await + } + + pub(crate) async fn interval_reward_params( + &self, + ) -> Result<RewardingParams, UninitialisedCache> { + self.get_owned(|c| c.current_reward_params).await + } + + pub(crate) async fn current_interval(&self) -> Result<Interval, UninitialisedCache> { + self.get_owned(|c| c.current_interval).await + } + + pub(crate) async fn get_key_rotation_state( + &self, + ) -> Result<KeyRotationState, UninitialisedCache> { + self.get_owned(|c| c.key_rotation_state).await + } + + pub(crate) async fn current_key_rotation_id(&self) -> Result<u32, UninitialisedCache> { + let guard = self.inner.get().await?; + let current_absolute_epoch_id = guard.current_interval.current_epoch_absolute_id(); + Ok(guard + .key_rotation_state + .key_rotation_id(current_absolute_epoch_id)) + } + + pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { + let Ok(cache) = self.inner.get().await else { + return Default::default(); + }; + + if cache.legacy_mixnodes.iter().any(|n| n.mix_id() == mix_id) { + MixnodeStatus::Inactive + } else { + MixnodeStatus::NotFound + } + } + + pub async fn get_node_refresh_data( + &self, + node_identity: ed25519::PublicKey, + ) -> Option<RefreshData> { + let Ok(cache) = self.inner.get().await else { + return Default::default(); + }; + + let encoded_identity = node_identity.to_base58_string(); + + // 1. check nymnodes + if let Some(nym_node) = cache + .nym_nodes + .iter() + .find(|n| n.bond_information.identity() == encoded_identity) + { + return nym_node.try_into().ok(); + } + + // 2. check legacy mixnodes + if let Some(mixnode) = cache + .legacy_mixnodes + .iter() + .find(|n| n.bond_information.identity() == encoded_identity) + { + return mixnode.try_into().ok(); + } + + // 3. check legacy gateways + if let Some(gateway) = cache + .legacy_gateways + .iter() + .find(|n| n.identity() == &encoded_identity) + { + return gateway.try_into().ok(); + } + + None + } + + pub(crate) async fn get_circulating_supply(&self) -> Option<CirculatingSupplyResponse> { + let mix_denom = self.get_owned(|c| c.rewarding_denom.clone()).await.ok()?; + let reward_pool = self + .interval_reward_params() + .await + .ok()? + .interval + .reward_pool; + + let mixmining_reserve_amount = truncate_decimal(reward_pool).u128(); + let mixmining_reserve = Coin::new(mixmining_reserve_amount, &mix_denom).into(); + + // given all tokens have already vested, the circulating supply is total supply - mixmining reserve + let circulating_supply = + Coin::new(TOTAL_SUPPLY_AMOUNT - mixmining_reserve_amount, &mix_denom).into(); + + Some(CirculatingSupplyResponse { + total_supply: Coin::new(TOTAL_SUPPLY_AMOUNT, &mix_denom).into(), + mixmining_reserve, + // everything has already vested + vesting_tokens: Coin::new(0, &mix_denom).into(), + circulating_supply, + }) + } + + pub(crate) async fn naive_wait_for_initial_values(&self) { + self.inner.naive_wait_for_initial_values().await + } +} diff --git a/nym-api/src/mixnet_contract_cache/cache/refresher.rs b/nym-api/src/mixnet_contract_cache/cache/refresher.rs new file mode 100644 index 0000000000..7e0735ca21 --- /dev/null +++ b/nym-api/src/mixnet_contract_cache/cache/refresher.rs @@ -0,0 +1,144 @@ +// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::{ConfigScoreData, MixnetContractCacheData}; +use crate::nyxd::Client; +use crate::support::caching::refresher::CacheItemProvider; +use anyhow::Result; +use async_trait::async_trait; +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use nym_mixnet_contract_common::LegacyMixLayer; +use nym_validator_client::nyxd::error::NyxdError; +use rand::prelude::SliceRandom; +use rand::rngs::OsRng; +use std::collections::HashMap; +use std::collections::HashSet; +use tracing::info; + +pub struct MixnetContractDataProvider { + nyxd_client: Client, +} + +#[async_trait] +impl CacheItemProvider for MixnetContractDataProvider { + type Item = MixnetContractCacheData; + type Error = NyxdError; + + async fn try_refresh(&mut self) -> std::result::Result<Option<Self::Item>, Self::Error> { + self.refresh().await.map(Some) + } +} + +impl MixnetContractDataProvider { + pub(crate) fn new(nyxd_client: Client) -> Self { + MixnetContractDataProvider { nyxd_client } + } + + async fn refresh(&self) -> Result<MixnetContractCacheData, NyxdError> { + let current_reward_params = self.nyxd_client.get_current_rewarding_parameters().await?; + let current_interval = self.nyxd_client.get_current_interval().await?.interval; + let contract_state = self.nyxd_client.get_mixnet_contract_state().await?; + + let nym_nodes = self.nyxd_client.get_nymnodes().await?; + let mixnode_details = self.nyxd_client.get_mixnodes().await?; + let gateway_bonds = self.nyxd_client.get_gateways().await?; + let gateway_ids: HashMap<_, _> = self + .nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect(); + + let mut legacy_gateways = Vec::with_capacity(gateway_bonds.len()); + #[allow(clippy::panic)] + for bond in gateway_bonds { + // we explicitly panic here because that value MUST exist. + // if it doesn't, we messed up the migration and we have big problems + let node_id = *gateway_ids.get(bond.identity()).unwrap_or_else(|| { + panic!( + "CONTRACT DATA INCONSISTENCY: MISSING GATEWAY ID FOR: {}", + bond.identity() + ) + }); + legacy_gateways.push(LegacyGatewayBondWithId { bond, node_id }) + } + + let rewarded_set = self.nyxd_client.get_rewarded_set_nodes().await?; + let layer1 = rewarded_set + .assignment + .layer1 + .iter() + .collect::<HashSet<_>>(); + let layer2 = rewarded_set + .assignment + .layer2 + .iter() + .collect::<HashSet<_>>(); + let layer3 = rewarded_set + .assignment + .layer3 + .iter() + .collect::<HashSet<_>>(); + + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + let mut legacy_mixnodes = Vec::with_capacity(mixnode_details.len()); + for detail in mixnode_details { + // if node is not in the rewarded set, well. + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + let layer = if layer1.contains(&detail.mix_id()) { + LegacyMixLayer::One + } else if layer2.contains(&detail.mix_id()) { + LegacyMixLayer::Two + } else if layer3.contains(&detail.mix_id()) { + LegacyMixLayer::Three + } else { + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(&mut rng).copied().unwrap() + }; + + legacy_mixnodes.push(LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: detail.bond_information, + layer, + }, + rewarding_details: detail.rewarding_details, + pending_changes: detail.pending_changes.into(), + }) + } + + let key_rotation_state = self.nyxd_client.get_key_rotation_state().await?; + let config_score_params = self.nyxd_client.get_config_score_params().await?; + let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; + + info!( + "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", + legacy_mixnodes.len(), + legacy_gateways.len(), + nym_nodes.len(), + ); + + Ok(MixnetContractCacheData { + rewarding_denom: contract_state.rewarding_denom, + legacy_mixnodes, + legacy_gateways, + nym_nodes, + rewarded_set: rewarded_set.into(), + config_score_data: ConfigScoreData { + config_score_params, + nym_node_version_history, + }, + current_reward_params, + current_interval, + key_rotation_state, + }) + } +} diff --git a/nym-api/src/mixnet_contract_cache/handlers.rs b/nym-api/src/mixnet_contract_cache/handlers.rs new file mode 100644 index 0000000000..e72182701b --- /dev/null +++ b/nym-api/src/mixnet_contract_cache/handlers.rs @@ -0,0 +1,522 @@ +// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::helpers::{ + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_rewarded_set_legacy_mixnodes_detailed, +}; +use crate::node_status_api::models::ApiResult; +use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; +use axum::extract::{Query, State}; +use axum::Router; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; +use nym_api_requests::models::{KeyRotationDetails, KeyRotationInfoResponse, MixNodeBondAnnotated}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; +use std::collections::HashSet; + +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn nym_contract_cache_routes() -> Router<AppState> { + Router::new() + .route("/mixnodes", axum::routing::get(get_mixnodes)) + .route( + "/mixnodes/detailed", + axum::routing::get(get_mixnodes_detailed), + ) + .route("/gateways", axum::routing::get(get_gateways)) + .route("/mixnodes/rewarded", axum::routing::get(get_rewarded_set)) + .route( + "/mixnodes/rewarded/detailed", + axum::routing::get(get_rewarded_set_detailed), + ) + .route("/mixnodes/active", axum::routing::get(get_active_set)) + .route( + "/mixnodes/active/detailed", + axum::routing::get(get_active_set_detailed), + ) + .route( + "/mixnodes/blacklisted", + axum::routing::get(get_blacklisted_mixnodes), + ) + .route( + "/gateways/blacklisted", + axum::routing::get(get_blacklisted_gateways), + ) + .route( + "/epoch/reward_params", + axum::routing::get(get_interval_reward_params), + ) + .route("/epoch/current", axum::routing::get(get_current_epoch)) + .route( + "/epoch/key-rotation-info", + axum::routing::get(get_current_key_rotation_info), + ) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes", + responses( + (status = 200, content( + (Vec<LegacyMixNodeDetailsWithLayer> = "application/json"), + (Vec<LegacyMixNodeDetailsWithLayer> = "application/yaml"), + (Vec<LegacyMixNodeDetailsWithLayer> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_mixnodes( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Vec<LegacyMixNodeDetailsWithLayer>> { + let output = output.output.unwrap_or_default(); + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return output.to_response(Vec::new()); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return output.to_response(Vec::new()); + }; + + let Ok(annotations) = state.node_annotations().await else { + return output.to_response(Vec::new()); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + let mut nodes = Vec::new(); + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + nodes.push(node); + } + + output.to_response(nodes) +} + +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_mixnodes_detailed() -> Redirect { +// Redirect::to(uri!("/v1/status/mixnodes/detailed")) +// } +// ``` +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/detailed", + responses( + (status = 200, content( + (Vec<MixNodeBondAnnotated> = "application/json"), + (Vec<MixNodeBondAnnotated> = "application/yaml"), + (Vec<MixNodeBondAnnotated> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_mixnodes_detailed( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/gateways", + responses( + (status = 200, content( + (Vec<GatewayBond> = "application/json"), + (Vec<GatewayBond> = "application/yaml"), + (Vec<GatewayBond> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_gateways( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Vec<GatewayBond>> { + let output = output.output.unwrap_or_default(); + + let mut nodes = Vec::new(); + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return output.to_response(nodes); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return output.to_response(nodes); + }; + + let Ok(annotations) = state.node_annotations().await else { + return output.to_response(nodes); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy gateway being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_gateway(nym_node, description); + nodes.push(node); + } + + output.to_response(nodes) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/rewarded", + responses( + (status = 200, content( + (Vec<LegacyMixNodeDetailsWithLayer> = "application/json"), + (Vec<LegacyMixNodeDetailsWithLayer> = "application/yaml"), + (Vec<LegacyMixNodeDetailsWithLayer> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_rewarded_set( + Query(output): Query<OutputParams>, + State(_state): State<AppState>, +) -> FormattedResponse<Vec<LegacyMixNodeDetailsWithLayer>> { + let output = output.output.unwrap_or_default(); + + output.to_response(Vec::new()) +} + +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_mixnodes_set_detailed() -> Redirect { +// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) +// } +// ``` +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/rewarded/detailed", + responses( + (status = 200, content( + (Vec<MixNodeBondAnnotated> = "application/json"), + (Vec<MixNodeBondAnnotated> = "application/yaml"), + (Vec<MixNodeBondAnnotated> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_rewarded_set_detailed( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response( + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/active", + responses( + (status = 200, content( + (Vec<LegacyMixNodeDetailsWithLayer> = "application/json"), + (Vec<LegacyMixNodeDetailsWithLayer> = "application/yaml"), + (Vec<LegacyMixNodeDetailsWithLayer> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_active_set( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Vec<LegacyMixNodeDetailsWithLayer>> { + let output = output.output.unwrap_or_default(); + + let mut out = Vec::new(); + + let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { + return output.to_response(out); + }; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return output.to_response(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return output.to_response(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return output.to_response(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + // if the node is not in the active set, ignore it + if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + output.to_response(out) +} + +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_active_set_detailed() -> Redirect { +// Redirect::to(uri!("/status/mixnodes/active/detailed")) +// } +// ``` + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/active/detailed", + responses( + (status = 200, content( + (Vec<MixNodeBondAnnotated> = "application/json"), + (Vec<MixNodeBondAnnotated> = "application/yaml"), + (Vec<MixNodeBondAnnotated> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_active_set_detailed( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response( + _get_active_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/mixnodes/blacklisted", + responses( + (status = 200, content( + (Option<HashSet<NodeId>> = "application/json"), + (Option<HashSet<NodeId>> = "application/yaml"), + (Option<HashSet<NodeId>> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_blacklisted_mixnodes( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Option<HashSet<NodeId>>> { + let output = output.output.unwrap_or_default(); + + let cache = state.nym_contract_cache(); + + // since blacklist has been removed, the equivalent of a blacklisted node is a legacy node + let mixnodes = cache.legacy_mixnodes_all().await; + output.to_response(Some(mixnodes.into_iter().map(|m| m.mix_id()).collect())) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/gateways/blacklisted", + responses( + (status = 200, content( + (Option<HashSet<NodeId>> = "application/json"), + (Option<HashSet<NodeId>> = "application/yaml"), + (Option<HashSet<NodeId>> = "application/bincode") + )) + ), + params(OutputParams) +)] +#[deprecated] +async fn get_blacklisted_gateways( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Option<HashSet<String>>> { + let output = output.output.unwrap_or_default(); + + let cache = state.nym_contract_cache(); + // since blacklist has been removed, the equivalent of a blacklisted node is a legacy node + let gateways = cache.legacy_gateways_all().await; + output.to_response(Some( + gateways + .into_iter() + .map(|g| g.gateway.identity_key.clone()) + .collect(), + )) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/epoch/reward_params", + responses( + (status = 200, content( + (Option<RewardingParams> = "application/json"), + (Option<RewardingParams> = "application/yaml"), + (Option<RewardingParams> = "application/bincode") + )) + ), + params(OutputParams) +)] +async fn get_interval_reward_params( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Option<RewardingParams>> { + let output = output.output.unwrap_or_default(); + + output.to_response( + state + .nym_contract_cache() + .interval_reward_params() + .await + .ok(), + ) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/v1/epoch/current", + responses( + (status = 200, content( + (Option<Interval> = "application/json"), + (Option<Interval> = "application/yaml"), + (Option<Interval> = "application/bincode") + )) + ), + params(OutputParams) +)] +async fn get_current_epoch( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<Option<Interval>> { + let output = output.output.unwrap_or_default(); + + output.to_response(state.nym_contract_cache().current_interval().await.ok()) +} + +// +#[utoipa::path( + tag = "contract-cache", + get, + path = "/key-rotation-info", + context_path = "/v1/epoch", + responses( + (status = 200, content( + (KeyRotationInfoResponse = "application/json"), + (KeyRotationInfoResponse = "application/yaml"), + (KeyRotationInfoResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +async fn get_current_key_rotation_info( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> ApiResult<FormattedResponse<KeyRotationInfoResponse>> { + let output = output.output.unwrap_or_default(); + + let contract_cache = state.nym_contract_cache(); + let current_interval = contract_cache.current_interval().await?; + let key_rotation_state = contract_cache.get_key_rotation_state().await?; + + let details = KeyRotationDetails { + key_rotation_state, + current_absolute_epoch_id: current_interval.current_epoch_absolute_id(), + current_epoch_start: current_interval.current_epoch_start(), + epoch_duration: current_interval.epoch_length(), + }; + + Ok(output.to_response(details.into())) +} diff --git a/nym-api/src/mixnet_contract_cache/mod.rs b/nym-api/src/mixnet_contract_cache/mod.rs new file mode 100644 index 0000000000..9911c55683 --- /dev/null +++ b/nym-api/src/mixnet_contract_cache/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::MixnetContractCacheData; +use crate::mixnet_contract_cache::cache::refresher::MixnetContractDataProvider; +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::support::caching::refresher::CacheRefresher; +use crate::support::{config, nyxd}; +use nym_validator_client::nyxd::error::NyxdError; + +pub(crate) mod cache; +pub(crate) mod handlers; + +pub(crate) fn build_refresher( + config: &config::MixnetContractCache, + nym_contract_cache_state: &MixnetContractCache, + nyxd_client: nyxd::Client, +) -> CacheRefresher<MixnetContractCacheData, NyxdError> { + CacheRefresher::new_with_initial_value( + Box::new(MixnetContractDataProvider::new(nyxd_client)), + config.debug.caching_interval, + nym_contract_cache_state.inner(), + ) + .named("mixnet-contract-cache-refresher") +} diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 380e78e736..ef9396beff 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -3,12 +3,19 @@ use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; +use crate::signers_cache::handlers::signers_routes; +use crate::support::config::CHAIN_STALL_THRESHOLD; use crate::support::http::state::AppState; -use axum::extract::State; -use axum::{extract, Json, Router}; -use nym_api_requests::models::ChainStatusResponse; +use axum::extract::{Query, State}; +use axum::Router; +use nym_api_requests::models::{ + ChainBlocksStatusResponse, ChainBlocksStatusResponseBody, ChainStatus, ChainStatusResponse, +}; +use nym_api_requests::signable::SignableMessageBody; use nym_contracts_common::ContractBuildInformation; +use nym_http_api_common::{FormattedResponse, OutputParams}; use std::collections::HashMap; +use time::OffsetDateTime; use tower_http::compression::CompressionLayer; use utoipa::ToSchema; @@ -16,37 +23,62 @@ pub(crate) fn nym_network_routes() -> Router<AppState> { Router::new() .route("/details", axum::routing::get(network_details)) .route("/chain-status", axum::routing::get(chain_status)) + .route( + "/chain-blocks-status", + axum::routing::get(chain_blocks_status), + ) .route("/nym-contracts", axum::routing::get(nym_contracts)) .route( "/nym-contracts-detailed", axum::routing::get(nym_contracts_detailed), ) + .nest("/signers", signers_routes()) .layer(CompressionLayer::new()) } #[utoipa::path( tag = "network", get, - path = "/v1/network/details", + context_path = "/v1/network", + path = "/details", responses( - (status = 200, body = NetworkDetails) - ) + (status = 200, content( + (NetworkDetails = "application/json"), + (NetworkDetails = "application/yaml"), + (NetworkDetails = "application/bincode") + )) + ), + params(OutputParams) )] async fn network_details( - extract::State(state): extract::State<AppState>, -) -> axum::Json<NetworkDetails> { - state.network_details().to_owned().into() + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<NetworkDetails> { + let output = output.output.unwrap_or_default(); + + output.to_response(state.network_details().to_owned()) } #[utoipa::path( tag = "network", get, - path = "/v1/network/chain-status", + context_path = "/v1/network", + path = "/chain-status", responses( - (status = 200, body = ChainStatusResponse) - ) + (status = 200, content( + (ChainStatusResponse = "application/json"), + (ChainStatusResponse = "application/yaml"), + (ChainStatusResponse = "application/bincode") + )) + ), + params(OutputParams) )] -async fn chain_status(State(state): State<AppState>) -> AxumResult<Json<ChainStatusResponse>> { +async fn chain_status( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<ChainStatusResponse>> { + let output = output.output.unwrap_or_default(); + let chain_status = state .chain_status_cache .get_or_refresh(&state.nyxd_client) @@ -54,12 +86,53 @@ async fn chain_status(State(state): State<AppState>) -> AxumResult<Json<ChainSta let connected_nyxd = state.network_details.connected_nyxd; - Ok(Json(ChainStatusResponse { + Ok(output.to_response(ChainStatusResponse { connected_nyxd, status: chain_status, })) } +#[utoipa::path( + tag = "network", + get, + context_path = "/v1/network", + path = "/chain-blocks-status", + responses( + (status = 200, content( + (ChainBlocksStatusResponse = "application/json"), + (ChainBlocksStatusResponse = "application/yaml"), + (ChainBlocksStatusResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +async fn chain_blocks_status( + Query(params): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<ChainBlocksStatusResponse> { + let output = params.get_output(); + + let current_time = OffsetDateTime::now_utc(); + let latest_cached_block = state + .chain_status_cache + .get_or_refresh(&state.nyxd_client) + .await + .ok(); + let chain_status = latest_cached_block + .as_ref() + .map(|detailed| detailed.stall_status(current_time, CHAIN_STALL_THRESHOLD)) + .unwrap_or(ChainStatus::Unknown); + + output.to_response( + ChainBlocksStatusResponseBody { + current_time, + latest_cached_block, + chain_status, + } + .sign(state.private_signing_key()), + ) +} + // it's used for schema generation so dead_code is fine #[allow(dead_code)] #[derive(ToSchema)] @@ -71,7 +144,7 @@ pub(crate) struct ContractVersionSchemaResponse { /// version is any string that this implementation knows. It may be simple counter "1", "2". /// or semantic version on release tags "v0.7.0", or some custom feature flag list. /// the only code that needs to understand the version parsing is code that knows how to - /// migrate from the given contract (and is tied to it's implementation somehow) + /// migrate from the given contract (and is tied to its implementation somehow) pub version: String, } @@ -85,27 +158,42 @@ pub struct ContractInformationContractVersion { #[utoipa::path( tag = "network", get, - path = "/v1/network/nym-contracts", + context_path = "/v1/network", + path = "/nym-contracts", responses( - (status = 200, body = HashMap<String, ContractInformationContractVersion>) - ) + (status = 200, content( + (HashMap<String, ContractInformationContractVersion> = "application/json"), + (HashMap<String, ContractInformationContractVersion> = "application/yaml"), + (HashMap<String, ContractInformationContractVersion> = "application/bincode") + )) + ), + params(OutputParams) )] async fn nym_contracts( - extract::State(state): extract::State<AppState>, -) -> axum::Json<HashMap<String, ContractInformation<cw2::ContractVersion>>> { - let info = state.nym_contract_cache().contract_details().await; - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.base.clone(), - }, - ) - }) - .collect::<HashMap<_, _>>() - .into() + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<HashMap<String, ContractInformation<cw2::ContractVersion>>>> { + let output = output.output.unwrap_or_default(); + + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() + .map(|(contract, info)| { + ( + contract.to_owned(), + ContractInformation { + address: info.address.as_ref().map(|a| a.to_string()), + details: info.base.clone(), + }, + ) + }) + .collect::<HashMap<_, _>>(), + )) } #[allow(dead_code)] // not dead, used in OpenAPI docs @@ -118,25 +206,40 @@ pub struct ContractInformationBuildInformation { #[utoipa::path( tag = "network", get, - path = "/v1/network/nym-contracts-detailed", + context_path = "/v1/network", + path = "/nym-contracts-detailed", responses( - (status = 200, body = HashMap<String, ContractInformationBuildInformation>) - ) + (status = 200, content( + (HashMap<String, ContractInformationBuildInformation> = "application/json"), + (HashMap<String, ContractInformationBuildInformation> = "application/yaml"), + (HashMap<String, ContractInformationBuildInformation> = "application/bincode") + )) + ), + params(OutputParams) )] async fn nym_contracts_detailed( - extract::State(state): extract::State<AppState>, -) -> axum::Json<HashMap<String, ContractInformation<ContractBuildInformation>>> { - let info = state.nym_contract_cache().contract_details().await; - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.detailed.clone(), - }, - ) - }) - .collect::<HashMap<_, _>>() - .into() + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<HashMap<String, ContractInformation<ContractBuildInformation>>>> { + let output = output.output.unwrap_or_default(); + + let contract_info = state + .contract_info_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + Ok(output.to_response( + contract_info + .iter() + .map(|(contract, info)| { + ( + contract.to_owned(), + ContractInformation { + address: info.address.as_ref().map(|a| a.to_string()), + details: info.detailed.clone(), + }, + ) + }) + .collect::<HashMap<_, _>>(), + )) } diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index 32028b263a..eaa6f8667e 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::{ ReceivedProcessor, ReceivedProcessorReceiver, ReceivedProcessorSender, @@ -11,9 +12,8 @@ use crate::network_monitor::monitor::receiver::{ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; -use crate::node_describe_cache::DescribedNodes; +use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; use crate::support::caching::cache::SharedCache; use crate::support::config::Config; @@ -25,7 +25,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; -use nym_task::TaskManager; +use nym_task::ShutdownManager; use std::sync::Arc; use tracing::info; @@ -38,7 +38,7 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, storage: &NymApiStorage, @@ -58,7 +58,7 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, } @@ -68,7 +68,7 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, ) -> Self { @@ -167,18 +167,18 @@ impl<R: MessageReceiver + Send + Sync + 'static> NetworkMonitorRunnables<R> { // TODO: note, that is not exactly doing what we want, because when // `ReceivedProcessor` is constructed, it already spawns a future // this needs to be refactored! - pub(crate) fn spawn_tasks(self, shutdown: &TaskManager) { + pub(crate) fn spawn_tasks(self, shutdown: &ShutdownManager) { let mut packet_receiver = self.packet_receiver; let mut monitor = self.monitor; - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown.clone_token("NM-packet-receiver"); tokio::spawn(async move { packet_receiver.run(shutdown_listener).await }); - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown.clone_token("NM-main"); tokio::spawn(async move { monitor.run(shutdown_listener).await }); } } fn new_packet_preparer( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, per_node_test_packets: usize, @@ -236,12 +236,12 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start<R: MessageReceiver + Send + Sync + 'static>( config: &Config, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, - shutdown: &TaskManager, + shutdown: &ShutdownManager, ) { let monitor_builder = setup( config, diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 360a946638..d51793acb9 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -12,7 +12,7 @@ use crate::support::config; use nym_mixnet_contract_common::NodeId; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::collections::{HashMap, HashSet}; use tokio::time::{sleep, Duration, Instant}; use tracing::{debug, error, info, trace}; @@ -325,7 +325,7 @@ impl<R: MessageReceiver + Send + Sync> Monitor<R> { self.test_nonce += 1; } - pub(crate) async fn run(&mut self, mut shutdown: TaskClient) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { self.received_processor.start_receiving(); // wait for validator cache to be ready @@ -334,18 +334,18 @@ impl<R: MessageReceiver + Send + Sync> Monitor<R> { .await; let mut run_interval = tokio::time::interval(self.run_interval); - while !shutdown.is_shutdown() { + while !shutdown_token.is_cancelled() { tokio::select! { _ = run_interval.tick() => { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); } _ = self.test_run() => (), } } - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); } } diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 69c509b8c7..49d48ab783 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -1,11 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; -use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_describe_cache::NodeDescriptionTopologyExt; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::SharedCache; use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; @@ -77,7 +78,7 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, @@ -95,7 +96,7 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, node_status_cache: NodeStatusCache, per_node_test_packets: usize, @@ -155,16 +156,9 @@ impl PacketPreparer { pub(crate) async fn wait_for_validator_cache_initial_values(&self, minimum_full_routes: usize) { // wait for the caches to get initialised - self.contract_cache.wait_for_initial_values().await; + self.contract_cache.naive_wait_for_initial_values().await; self.described_cache.naive_wait_for_initial_values().await; - #[allow(clippy::expect_used)] - let described_nodes = self - .described_cache - .get() - .await - .expect("the self-describe cache should have been initialised!"); - // now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online info!("Waiting for minimal topology to be online"); let initialisation_backoff = Duration::from_secs(30); @@ -173,6 +167,13 @@ impl PacketPreparer { let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; let nym_nodes = self.contract_cache.nym_nodes().await; + #[allow(clippy::expect_used)] + let described_nodes = self + .described_cache + .get() + .await + .expect("the self-describe cache should have been initialised!"); + let mut gateways_count = gateways.len(); let mut mixnodes_count = mixnodes.len(); @@ -285,13 +286,16 @@ impl PacketPreparer { fn to_legacy_layered_mixes<'a, R: Rng>( &self, rng: &mut R, + current_rotation_id: u32, node_statuses: &HashMap<NodeId, NodeAnnotation>, mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a, ) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> { let mut layered_mixes = HashMap::new(); for mixing_nym_node in mixing_nym_nodes { - let Some(parsed_node) = self.nym_node_to_routing_node(mixing_nym_node) else { + let Some(parsed_node) = + self.nym_node_to_routing_node(current_rotation_id, mixing_nym_node) + else { continue; }; // if the node is not present, default to 0.5 @@ -309,13 +313,16 @@ impl PacketPreparer { fn to_legacy_gateway_nodes<'a>( &self, + current_rotation_id: u32, node_statuses: &HashMap<NodeId, NodeAnnotation>, gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a, ) -> Vec<(RoutingNode, f64)> { let mut gateways = Vec::new(); for gateway_capable_node in gateway_capable_nym_nodes { - let Some(parsed_node) = self.nym_node_to_routing_node(gateway_capable_node) else { + let Some(parsed_node) = + self.nym_node_to_routing_node(current_rotation_id, gateway_capable_node) + else { continue; }; // if the node is not present, default to 0.5 @@ -341,11 +348,21 @@ impl PacketPreparer { // last I checked `gatewaying` wasn't a word : ) let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); + // SAFETY: cache has already been initialised + #[allow(clippy::unwrap_used)] + let current_rotation_id = self.contract_cache.current_key_rotation_id().await.unwrap(); + let mut rng = thread_rng(); // separate mixes into layers for easier selection alongside the selection weights - let layered_mixes = self.to_legacy_layered_mixes(&mut rng, &statuses, mixing_nym_nodes); - let gateways = self.to_legacy_gateway_nodes(&statuses, gateway_capable_nym_nodes); + let layered_mixes = self.to_legacy_layered_mixes( + &mut rng, + current_rotation_id, + &statuses, + mixing_nym_nodes, + ); + let gateways = + self.to_legacy_gateway_nodes(current_rotation_id, &statuses, gateway_capable_nym_nodes); // get all nodes from each layer... let l1 = layered_mixes.get(&LegacyMixLayer::One)?; @@ -399,7 +416,14 @@ impl PacketPreparer { let node_3 = rand_l3[i].clone(); let gateway = rand_gateways[i].clone(); - routes.push(TestRoute::new(rng.gen(), node_1, node_2, node_3, gateway)) + routes.push(TestRoute::new( + rng.gen(), + current_rotation_id, + node_1, + node_2, + node_3, + gateway, + )) } info!("The following routes will be used for testing: {routes:#?}"); Some(routes) @@ -482,8 +506,12 @@ impl PacketPreparer { (parsed_nodes, invalid_nodes) } - fn nym_node_to_routing_node(&self, description: &NymNodeDescription) -> Option<RoutingNode> { - description.try_to_topology_node().ok() + fn nym_node_to_routing_node( + &self, + current_rotation_id: u32, + description: &NymNodeDescription, + ) -> Option<RoutingNode> { + description.try_to_topology_node(current_rotation_id).ok() } pub(super) async fn prepare_test_packets( @@ -495,6 +523,10 @@ impl PacketPreparer { ) -> PreparedPackets { let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; + // SAFETY: cache has already been initialised + #[allow(clippy::unwrap_used)] + let current_rotation_id = self.contract_cache.current_key_rotation_id().await.unwrap(); + #[allow(clippy::expect_used)] let descriptions = self .described_cache @@ -521,7 +553,7 @@ impl PacketPreparer { // try to add nym-nodes into the fold for mix in mixing_nym_nodes { - if let Some(parsed) = self.nym_node_to_routing_node(mix) { + if let Some(parsed) = self.nym_node_to_routing_node(current_rotation_id, mix) { mixnodes_under_test.push(TestableNode::new_routing(&parsed, NodeType::Mixnode)); mixnodes_to_test_details.push(parsed); } @@ -535,7 +567,7 @@ impl PacketPreparer { .collect::<Vec<_>>(); for gateway in gateway_capable_nym_nodes { - if let Some(parsed) = self.nym_node_to_routing_node(gateway) { + if let Some(parsed) = self.nym_node_to_routing_node(current_rotation_id, gateway) { gateways_under_test.push(TestableNode::new_routing(&parsed, NodeType::Gateway)); gateways_to_test_details.push(parsed); } diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index e84398b9b4..e5f27a1c0a 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -7,7 +7,7 @@ use futures::channel::mpsc; use futures::StreamExt; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use tracing::{error, trace}; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender<GatewayClientUpdate>; @@ -57,11 +57,11 @@ impl PacketReceiver { } } - pub(crate) async fn run(&mut self, mut shutdown: TaskClient) { - while !shutdown.is_shutdown() { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { + while !shutdown_token.is_cancelled() { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); } // unwrap here is fine as it can only return a `None` if the PacketSender has died diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index fd59a6419b..7e31aaba62 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -7,8 +7,9 @@ use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, RewardedSet}; use nym_topology::node::RoutingNode; -use nym_topology::{NymRouteProvider, NymTopology}; +use nym_topology::{NymRouteProvider, NymTopology, NymTopologyMetadata}; use std::fmt::{Debug, Formatter}; +use time::OffsetDateTime; #[derive(Clone)] pub(crate) struct TestRoute { @@ -19,6 +20,7 @@ pub(crate) struct TestRoute { impl TestRoute { pub(crate) fn new( id: u64, + key_rotation_id: u32, l1_mix: RoutingNode, l2_mix: RoutingNode, l3_mix: RoutingNode, @@ -40,7 +42,11 @@ impl TestRoute { TestRoute { id, - nodes: NymTopology::new(fake_rewarded_set, nodes), + nodes: NymTopology::new( + NymTopologyMetadata::new(key_rotation_id, 0, OffsetDateTime::now_utc()), + fake_rewarded_set, + nodes, + ), } } diff --git a/nym-api/src/node_describe_cache/cache.rs b/nym-api/src/node_describe_cache/cache.rs new file mode 100644 index 0000000000..3f3de776d6 --- /dev/null +++ b/nym-api/src/node_describe_cache/cache.rs @@ -0,0 +1,65 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; +use nym_mixnet_contract_common::NodeId; +use std::collections::HashMap; +use std::net::IpAddr; + +#[derive(Debug, Clone)] +pub struct DescribedNodes { + pub(crate) nodes: HashMap<NodeId, NymNodeDescription>, + pub(crate) addresses_cache: HashMap<IpAddr, NodeId>, +} + +impl DescribedNodes { + pub fn force_update(&mut self, node: NymNodeDescription) { + for ip in &node.description.host_information.ip_address { + self.addresses_cache.insert(*ip, node.node_id); + } + self.nodes.insert(node.node_id, node); + } + + pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> { + self.nodes.get(node_id).map(|n| &n.description) + } + + pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> { + self.nodes.get(node_id) + } + + pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { + self.nodes.values() + } + + pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + } + + pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.mixnode) + } + + pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.entry) + } + + pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_exit_gateway()) + } + + pub fn node_with_address(&self, address: IpAddr) -> Option<NodeId> { + self.addresses_cache.get(&address).copied() + } +} diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index cdb9ab67b8..096555aa26 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -1,28 +1,19 @@ // Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::node_describe_cache::query_helpers::query_for_described_data; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::support::caching::cache::{SharedCache, UninitialisedCache}; -use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; -use crate::support::config; -use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; -use async_trait::async_trait; -use futures::{stream, StreamExt}; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; +use crate::support::caching::cache::UninitialisedCache; +use nym_api_requests::models::NymNodeDescription; use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_crypto::asymmetric::ed25519; -use nym_mixnet_contract_common::{NodeId, NymNodeDetails}; -use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; -use nym_topology::node::{RoutingNode, RoutingNodeError}; -use std::collections::HashMap; -use std::net::IpAddr; -use std::time::Duration; +use nym_mixnet_contract_common::NodeId; +use nym_node_requests::api::client::NymNodeApiClientError; +use nym_topology::node::RoutingNodeError; +use nym_topology::RoutingNode; use thiserror::Error; -use tracing::{debug, error, info}; +pub(crate) mod cache; +pub(crate) mod provider; mod query_helpers; +pub(crate) mod refresh; #[derive(Debug, Error)] pub enum NodeDescribeCacheError { @@ -71,390 +62,20 @@ pub enum NodeDescribeCacheError { // this exists because I've been moving things around quite a lot and now the place that holds the type // doesn't have relevant dependencies for proper impl pub(crate) trait NodeDescriptionTopologyExt { - fn try_to_topology_node(&self) -> Result<RoutingNode, RoutingNodeError>; + fn try_to_topology_node( + &self, + current_rotation_id: u32, + ) -> Result<RoutingNode, RoutingNodeError>; } impl NodeDescriptionTopologyExt for NymNodeDescription { - fn try_to_topology_node(&self) -> Result<RoutingNode, RoutingNodeError> { + fn try_to_topology_node( + &self, + current_rotation_id: u32, + ) -> Result<RoutingNode, RoutingNodeError> { // for the purposes of routing, performance is completely ignored, // so add dummy value and piggyback on existing conversion - (&self.to_skimmed_node(Default::default(), Default::default())).try_into() + (&self.to_skimmed_node(current_rotation_id, Default::default(), Default::default())) + .try_into() } } - -#[derive(Debug, Clone)] -pub struct DescribedNodes { - nodes: HashMap<NodeId, NymNodeDescription>, - addresses_cache: HashMap<IpAddr, NodeId>, -} - -impl DescribedNodes { - pub fn force_update(&mut self, node: NymNodeDescription) { - for ip in &node.description.host_information.ip_address { - self.addresses_cache.insert(*ip, node.node_id); - } - self.nodes.insert(node.node_id, node); - } - - pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> { - self.nodes.get(node_id).map(|n| &n.description) - } - - pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> { - self.nodes.get(node_id) - } - - pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { - self.nodes.values() - } - - pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { - self.nodes - .values() - .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) - } - - pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { - self.nodes - .values() - .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) - .filter(|n| n.description.declared_role.mixnode) - } - - pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { - self.nodes - .values() - .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) - .filter(|n| n.description.declared_role.entry) - } - - pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> { - self.nodes - .values() - .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) - .filter(|n| n.description.declared_role.can_operate_exit_gateway()) - } - - pub fn node_with_address(&self, address: IpAddr) -> Option<NodeId> { - self.addresses_cache.get(&address).copied() - } -} - -pub struct NodeDescriptionProvider { - contract_cache: NymContractCache, - - allow_all_ips: bool, - batch_size: usize, -} - -impl NodeDescriptionProvider { - pub(crate) fn new( - contract_cache: NymContractCache, - allow_all_ips: bool, - ) -> NodeDescriptionProvider { - NodeDescriptionProvider { - contract_cache, - allow_all_ips, - batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, - } - } - - #[must_use] - pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self { - self.batch_size = batch_size; - self - } -} - -async fn try_get_client( - host: &str, - node_id: NodeId, - custom_port: Option<u16>, -) -> Result<nym_node_requests::api::Client, NodeDescribeCacheError> { - // first try the standard port in case the operator didn't put the node behind the proxy, - // then default https (443) - // finally default http (80) - let mut addresses_to_try = vec![ - format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node - format!("https://{host}"), // node behind https proxy (443) - format!("http://{host}"), // node behind http proxy (80) - ]; - - // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via - // the 'custom_port' since it should have been present in the contract. - - if let Some(port) = custom_port { - addresses_to_try.insert(0, format!("http://{host}:{port}")); - } - - for address in addresses_to_try { - // if provided host was malformed, no point in continuing - let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { - b.with_timeout(Duration::from_secs(5)) - .no_hickory_dns() - .with_user_agent("nym-api-describe-cache") - .build() - }) { - Ok(client) => client, - Err(err) => { - return Err(NodeDescribeCacheError::MalformedHost { - host: host.to_string(), - node_id, - source: err, - }); - } - }; - - if let Ok(health) = client.get_health().await { - if health.status.is_up() { - return Ok(client); - } - } - } - - Err(NodeDescribeCacheError::NoHttpPortsAvailable { - host: host.to_string(), - node_id, - }) -} - -async fn try_get_description( - data: RefreshData, - allow_all_ips: bool, -) -> Result<NymNodeDescription, NodeDescribeCacheError> { - let client = try_get_client(&data.host, data.node_id, data.port).await?; - - let map_query_err = |err| NodeDescribeCacheError::ApiFailure { - node_id: data.node_id, - source: err, - }; - - let host_info = client.get_host_information().await.map_err(map_query_err)?; - - // check if the identity key matches the information provided during bonding - if data.expected_identity != host_info.keys.ed25519_identity { - return Err(NodeDescribeCacheError::MismatchedIdentity { - node_id: data.node_id, - expected: data.expected_identity.to_base58_string(), - got: host_info.keys.ed25519_identity.to_base58_string(), - }); - } - - if !host_info.verify_host_information() { - return Err(NodeDescribeCacheError::MissignedHostInformation { - node_id: data.node_id, - }); - } - - if !allow_all_ips && !host_info.data.check_ips() { - return Err(NodeDescribeCacheError::IllegalIpAddress { - node_id: data.node_id, - }); - } - - let node_info = query_for_described_data(&client, data.node_id).await?; - let description = node_info.into_node_description(host_info.data); - - Ok(NymNodeDescription { - node_id: data.node_id, - contract_node_type: data.node_type, - description, - }) -} - -#[derive(Debug)] -pub(crate) struct RefreshData { - host: String, - node_id: NodeId, - expected_identity: ed25519::PublicKey, - node_type: DescribedNodeType, - - port: Option<u16>, -} - -impl<'a> TryFrom<&'a LegacyMixNodeDetailsWithLayer> for RefreshData { - type Error = ed25519::Ed25519RecoveryError; - - fn try_from(node: &'a LegacyMixNodeDetailsWithLayer) -> Result<Self, Self::Error> { - Ok(RefreshData::new( - &node.bond_information.mix_node.host, - node.bond_information.identity().parse()?, - DescribedNodeType::LegacyMixnode, - node.mix_id(), - Some(node.bond_information.mix_node.http_api_port), - )) - } -} - -impl<'a> TryFrom<&'a LegacyGatewayBondWithId> for RefreshData { - type Error = ed25519::Ed25519RecoveryError; - - fn try_from(node: &'a LegacyGatewayBondWithId) -> Result<Self, Self::Error> { - Ok(RefreshData::new( - &node.bond.gateway.host, - node.bond.identity().parse()?, - DescribedNodeType::LegacyGateway, - node.node_id, - None, - )) - } -} - -impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData { - type Error = ed25519::Ed25519RecoveryError; - - fn try_from(node: &'a NymNodeDetails) -> Result<Self, Self::Error> { - Ok(RefreshData::new( - &node.bond_information.node.host, - node.bond_information.identity().parse()?, - DescribedNodeType::NymNode, - node.node_id(), - node.bond_information.node.custom_http_port, - )) - } -} - -impl RefreshData { - pub fn new( - host: impl Into<String>, - expected_identity: ed25519::PublicKey, - node_type: DescribedNodeType, - node_id: NodeId, - port: Option<u16>, - ) -> Self { - RefreshData { - host: host.into(), - node_id, - expected_identity, - node_type, - port, - } - } - - pub(crate) fn node_id(&self) -> NodeId { - self.node_id - } - - pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescription> { - match try_get_description(self, allow_all_ips).await { - Ok(description) => Some(description), - Err(err) => { - debug!("failed to obtain node self-described data: {err}"); - None - } - } - } -} - -#[async_trait] -impl CacheItemProvider for NodeDescriptionProvider { - type Item = DescribedNodes; - type Error = NodeDescribeCacheError; - - async fn wait_until_ready(&self) { - self.contract_cache.wait_for_initial_values().await - } - - async fn try_refresh(&self) -> Result<Self::Item, Self::Error> { - // we need to query: - // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) - // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) - // - nym-nodes - - let mut nodes_to_query: Vec<RefreshData> = Vec::new(); - - match self.contract_cache.all_cached_legacy_mixnodes().await { - None => error!("failed to obtain mixnodes information from the cache"), - Some(legacy_mixnodes) => { - for node in &**legacy_mixnodes { - if let Ok(data) = node.try_into() { - nodes_to_query.push(data); - } - } - } - } - - match self.contract_cache.all_cached_legacy_gateways().await { - None => error!("failed to obtain gateways information from the cache"), - Some(legacy_gateways) => { - for node in &**legacy_gateways { - if let Ok(data) = node.try_into() { - nodes_to_query.push(data); - } - } - } - } - - match self.contract_cache.all_cached_nym_nodes().await { - None => error!("failed to obtain nym-nodes information from the cache"), - Some(nym_nodes) => { - for node in &**nym_nodes { - if let Ok(data) = node.try_into() { - nodes_to_query.push(data); - } - } - } - } - - let nodes = stream::iter( - nodes_to_query - .into_iter() - .map(|n| n.try_refresh(self.allow_all_ips)), - ) - .buffer_unordered(self.batch_size) - .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) - .collect::<HashMap<_, _>>() - .await; - - let mut addresses_cache = HashMap::new(); - for node in nodes.values() { - for ip in &node.description.host_information.ip_address { - addresses_cache.insert(*ip, node.node_id); - } - } - - info!("refreshed self described data for {} nodes", nodes.len()); - info!("with {} unique ip addresses", addresses_cache.len()); - - Ok(DescribedNodes { - nodes, - addresses_cache, - }) - } -} - -// currently dead code : ( -#[allow(dead_code)] -pub(crate) fn new_refresher( - config: &config::TopologyCacher, - contract_cache: NymContractCache, -) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { - CacheRefresher::new( - Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), - ), - config.debug.node_describe_caching_interval, - ) -} - -pub(crate) fn new_refresher_with_initial_value( - config: &config::TopologyCacher, - contract_cache: NymContractCache, - initial: SharedCache<DescribedNodes>, -) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { - CacheRefresher::new_with_initial_value( - Box::new( - NodeDescriptionProvider::new( - contract_cache, - config.debug.node_describe_allow_illegal_ips, - ) - .with_batch_size(config.debug.node_describe_batch_size), - ), - config.debug.node_describe_caching_interval, - initial, - ) -} diff --git a/nym-api/src/node_describe_cache/provider.rs b/nym-api/src/node_describe_cache/provider.rs new file mode 100644 index 0000000000..259b14ba1e --- /dev/null +++ b/nym-api/src/node_describe_cache/provider.rs @@ -0,0 +1,146 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_describe_cache::refresh::RefreshData; +use crate::node_describe_cache::NodeDescribeCacheError; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; +use crate::support::config; +use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; +use async_trait::async_trait; +use futures::{stream, StreamExt}; +use std::collections::HashMap; +use tracing::{error, info}; + +pub struct NodeDescriptionProvider { + contract_cache: MixnetContractCache, + + allow_all_ips: bool, + batch_size: usize, +} + +impl NodeDescriptionProvider { + pub(crate) fn new( + contract_cache: MixnetContractCache, + allow_all_ips: bool, + ) -> NodeDescriptionProvider { + NodeDescriptionProvider { + contract_cache, + allow_all_ips, + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + } + } + + #[must_use] + pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self { + self.batch_size = batch_size; + self + } +} + +#[async_trait] +impl CacheItemProvider for NodeDescriptionProvider { + type Item = DescribedNodes; + type Error = NodeDescribeCacheError; + + async fn wait_until_ready(&self) { + self.contract_cache.naive_wait_for_initial_values().await + } + + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { + // we need to query: + // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) + // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) + // - nym-nodes + + let mut nodes_to_query: Vec<RefreshData> = Vec::new(); + + match self.contract_cache.all_cached_legacy_mixnodes().await { + None => error!("failed to obtain mixnodes information from the cache"), + Some(legacy_mixnodes) => { + for node in &**legacy_mixnodes { + if let Ok(data) = node.try_into() { + nodes_to_query.push(data); + } + } + } + } + + match self.contract_cache.all_cached_legacy_gateways().await { + None => error!("failed to obtain gateways information from the cache"), + Some(legacy_gateways) => { + for node in &**legacy_gateways { + if let Ok(data) = node.try_into() { + nodes_to_query.push(data); + } + } + } + } + + match self.contract_cache.all_cached_nym_nodes().await { + None => error!("failed to obtain nym-nodes information from the cache"), + Some(nym_nodes) => { + for node in &**nym_nodes { + if let Ok(data) = node.try_into() { + nodes_to_query.push(data); + } + } + } + } + + let nodes = stream::iter( + nodes_to_query + .into_iter() + .map(|n| n.try_refresh(self.allow_all_ips)), + ) + .buffer_unordered(self.batch_size) + .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) + .collect::<HashMap<_, _>>() + .await; + + let mut addresses_cache = HashMap::new(); + for node in nodes.values() { + for ip in &node.description.host_information.ip_address { + addresses_cache.insert(*ip, node.node_id); + } + } + + info!("refreshed self described data for {} nodes", nodes.len()); + info!("with {} unique ip addresses", addresses_cache.len()); + + Ok(Some(DescribedNodes { + nodes, + addresses_cache, + })) + } +} + +// currently dead code : ( +#[allow(dead_code)] +pub(crate) fn new_refresher( + config: &config::DescribeCache, + contract_cache: MixnetContractCache, +) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { + CacheRefresher::new( + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), + config.debug.caching_interval, + ) +} + +pub(crate) fn new_provider_with_initial_value( + config: &config::DescribeCache, + contract_cache: MixnetContractCache, + initial: SharedCache<DescribedNodes>, +) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> { + CacheRefresher::new_with_initial_value( + Box::new( + NodeDescriptionProvider::new(contract_cache, config.debug.allow_illegal_ips) + .with_batch_size(config.debug.batch_size), + ), + config.debug.caching_interval, + initial, + ) +} diff --git a/nym-api/src/node_describe_cache/refresh.rs b/nym-api/src/node_describe_cache/refresh.rs new file mode 100644 index 0000000000..6f3949c6c6 --- /dev/null +++ b/nym-api/src/node_describe_cache/refresh.rs @@ -0,0 +1,195 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_describe_cache::query_helpers::query_for_described_data; +use crate::node_describe_cache::NodeDescribeCacheError; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{DescribedNodeType, NymNodeDescription}; +use nym_bin_common::bin_info; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails}; +use nym_node_requests::api::client::NymNodeApiClientExt; +use nym_validator_client::UserAgent; +use std::time::Duration; +use tracing::debug; + +#[derive(Debug)] +pub(crate) struct RefreshData { + host: String, + node_id: NodeId, + expected_identity: ed25519::PublicKey, + node_type: DescribedNodeType, + + port: Option<u16>, +} + +impl<'a> TryFrom<&'a LegacyMixNodeDetailsWithLayer> for RefreshData { + type Error = ed25519::Ed25519RecoveryError; + + fn try_from(node: &'a LegacyMixNodeDetailsWithLayer) -> Result<Self, Self::Error> { + Ok(RefreshData::new( + &node.bond_information.mix_node.host, + node.bond_information.identity().parse()?, + DescribedNodeType::LegacyMixnode, + node.mix_id(), + Some(node.bond_information.mix_node.http_api_port), + )) + } +} + +impl<'a> TryFrom<&'a LegacyGatewayBondWithId> for RefreshData { + type Error = ed25519::Ed25519RecoveryError; + + fn try_from(node: &'a LegacyGatewayBondWithId) -> Result<Self, Self::Error> { + Ok(RefreshData::new( + &node.bond.gateway.host, + node.bond.identity().parse()?, + DescribedNodeType::LegacyGateway, + node.node_id, + None, + )) + } +} + +impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData { + type Error = ed25519::Ed25519RecoveryError; + + fn try_from(node: &'a NymNodeDetails) -> Result<Self, Self::Error> { + Ok(RefreshData::new( + &node.bond_information.node.host, + node.bond_information.identity().parse()?, + DescribedNodeType::NymNode, + node.node_id(), + node.bond_information.node.custom_http_port, + )) + } +} + +impl RefreshData { + pub fn new( + host: impl Into<String>, + expected_identity: ed25519::PublicKey, + node_type: DescribedNodeType, + node_id: NodeId, + port: Option<u16>, + ) -> Self { + RefreshData { + host: host.into(), + node_id, + expected_identity, + node_type, + port, + } + } + + pub(crate) fn node_id(&self) -> NodeId { + self.node_id + } + + pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescription> { + match try_get_description(self, allow_all_ips).await { + Ok(description) => Some(description), + Err(err) => { + debug!("failed to obtain node self-described data: {err}"); + None + } + } + } +} + +async fn try_get_client( + host: &str, + node_id: NodeId, + custom_port: Option<u16>, +) -> Result<nym_node_requests::api::Client, NodeDescribeCacheError> { + // first try the standard port in case the operator didn't put the node behind the proxy, + // then default https (443) + // finally default http (80) + let mut addresses_to_try = vec![ + format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node + format!("https://{host}"), // node behind https proxy (443) + format!("http://{host}"), // node behind http proxy (80) + ]; + + // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via + // the 'custom_port' since it should have been present in the contract. + + if let Some(port) = custom_port { + addresses_to_try.insert(0, format!("http://{host}:{port}")); + } + + for address in addresses_to_try { + // if provided host was malformed, no point in continuing + let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { + b.with_timeout(Duration::from_secs(5)) + .no_hickory_dns() + .with_user_agent(UserAgent::from(bin_info!())) + .build() + }) { + Ok(client) => client, + Err(err) => { + return Err(NodeDescribeCacheError::MalformedHost { + host: host.to_string(), + node_id, + source: err, + }); + } + }; + + if let Ok(health) = client.get_health().await { + if health.status.is_up() { + return Ok(client); + } + } + } + + Err(NodeDescribeCacheError::NoHttpPortsAvailable { + host: host.to_string(), + node_id, + }) +} + +async fn try_get_description( + data: RefreshData, + allow_all_ips: bool, +) -> Result<NymNodeDescription, NodeDescribeCacheError> { + let client = try_get_client(&data.host, data.node_id, data.port).await?; + + let map_query_err = |err| NodeDescribeCacheError::ApiFailure { + node_id: data.node_id, + source: err, + }; + + let host_info = client.get_host_information().await.map_err(map_query_err)?; + + // check if the identity key matches the information provided during bonding + if data.expected_identity != host_info.keys.ed25519_identity { + return Err(NodeDescribeCacheError::MismatchedIdentity { + node_id: data.node_id, + expected: data.expected_identity.to_base58_string(), + got: host_info.keys.ed25519_identity.to_base58_string(), + }); + } + + if !host_info.verify_host_information() { + return Err(NodeDescribeCacheError::MissignedHostInformation { + node_id: data.node_id, + }); + } + + if !allow_all_ips && !host_info.data.check_ips() { + return Err(NodeDescribeCacheError::IllegalIpAddress { + node_id: data.node_id, + }); + } + + let node_info = query_for_described_data(&client, data.node_id).await?; + let description = node_info.into_node_description(host_info.data); + + Ok(NymNodeDescription { + node_id: data.node_id, + contract_node_type: data.node_type, + description, + }) +} diff --git a/nym-api/src/node_performance/contract_cache/data.rs b/nym-api/src/node_performance/contract_cache/data.rs new file mode 100644 index 0000000000..985ee0a4d7 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/data.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::PerformanceRetrievalFailure; +use nym_api_requests::models::RoutingScore; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use nym_validator_client::nyxd::contract_traits::performance_query_client::NodePerformance; +use std::collections::{BTreeMap, HashMap}; + +pub(crate) struct PerformanceContractEpochCacheData { + pub(crate) epoch_id: EpochId, + pub(crate) median_performance: HashMap<NodeId, Performance>, +} + +impl PerformanceContractEpochCacheData { + pub(crate) fn from_node_performance( + performance: Vec<NodePerformance>, + epoch_id: EpochId, + ) -> Self { + let median_performance = performance + .into_iter() + .map(|node_performance| (node_performance.node_id, node_performance.performance)) + .collect(); + PerformanceContractEpochCacheData { + epoch_id, + median_performance, + } + } +} + +pub(crate) struct PerformanceContractCacheData { + pub(crate) epoch_performance: BTreeMap<EpochId, PerformanceContractEpochCacheData>, +} + +impl PerformanceContractCacheData { + pub(crate) fn update( + &mut self, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, + ) { + self.epoch_performance.insert(update.epoch_id, update); + if self.epoch_performance.len() > values_to_retain { + // remove the oldest entry, i.e. one with the lowest epoch id + self.epoch_performance.pop_first(); + } + } + + pub(crate) fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + // TODO: somehow send a signal to refresh this epoch + let epoch_scores = self.epoch_performance.get(&epoch_id).ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!("no cached performance results for epoch {epoch_id}"), + ) + })?; + + let node_score = epoch_scores + .median_performance + .get(&node_id) + .ok_or_else(|| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + format!( + "no cached performance results for node {node_id} for epoch {epoch_id}" + ), + ) + })?; + + Ok(RoutingScore::new(node_score.naive_to_f64())) + } +} + +// needed for cache initialisation +impl From<PerformanceContractEpochCacheData> for PerformanceContractCacheData { + fn from(cache_data: PerformanceContractEpochCacheData) -> Self { + let mut epoch_performance = BTreeMap::new(); + epoch_performance.insert(cache_data.epoch_id, cache_data); + PerformanceContractCacheData { epoch_performance } + } +} diff --git a/nym-api/src/node_performance/contract_cache/mod.rs b/nym-api/src/node_performance/contract_cache/mod.rs new file mode 100644 index 0000000000..be430935a5 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::contract_cache::refresher::{ + refresher_update_fn, PerformanceContractDataProvider, +}; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::CacheRefresher; +use crate::support::{config, nyxd}; +use anyhow::bail; +use nym_task::ShutdownManager; + +pub(crate) mod data; +pub(crate) mod refresher; + +pub(crate) async fn start_cache_refresher( + config: &config::PerformanceProvider, + nyxd_client: nyxd::Client, + mixnet_contract_cache: MixnetContractCache, + shutdown_manager: &ShutdownManager, +) -> anyhow::Result<SharedCache<PerformanceContractCacheData>> { + let values_to_retain = config.debug.max_epoch_entries_to_retain; + + let mut item_provider = + PerformanceContractDataProvider::new(nyxd_client, mixnet_contract_cache); + + if !item_provider.cache_has_values().await { + bail!("performance contract is empty - can't use it as source of node performance") + } + + let warmed_up_cache = SharedCache::new_with_value( + item_provider + .provide_initial_warmed_up_cache(values_to_retain) + .await?, + ); + + CacheRefresher::new_with_initial_value( + Box::new(item_provider), + config.debug.contract_polling_interval, + warmed_up_cache.clone(), + ) + .named("performance-contract-cache-refresher") + .with_update_fn(move |main_cache, update| { + refresher_update_fn(main_cache, update, values_to_retain) + }) + .start(shutdown_manager.clone_token("performance-contract-cache-refresher")); + + Ok(warmed_up_cache) +} diff --git a/nym-api/src/node_performance/contract_cache/refresher.rs b/nym-api/src/node_performance/contract_cache/refresher.rs new file mode 100644 index 0000000000..887374b018 --- /dev/null +++ b/nym-api/src/node_performance/contract_cache/refresher.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::contract_cache::data::{ + PerformanceContractCacheData, PerformanceContractEpochCacheData, +}; +use crate::support::caching::refresher::CacheItemProvider; +use crate::support::nyxd::Client; +use async_trait::async_trait; +use nym_validator_client::nyxd::contract_traits::performance_query_client::LastSubmission; +use nym_validator_client::nyxd::error::NyxdError; +use std::collections::BTreeMap; + +pub struct PerformanceContractDataProvider { + nyxd_client: Client, + mixnet_contract_cache: MixnetContractCache, + last_submission: Option<LastSubmission>, +} + +pub(crate) fn refresher_update_fn( + main_cache: &mut PerformanceContractCacheData, + update: PerformanceContractEpochCacheData, + values_to_retain: usize, +) { + main_cache.update(update, values_to_retain); +} + +#[async_trait] +impl CacheItemProvider for PerformanceContractDataProvider { + type Item = PerformanceContractEpochCacheData; + type Error = NyxdError; + + async fn wait_until_ready(&self) { + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await + } + + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { + self.refresh().await + } +} + +impl PerformanceContractDataProvider { + pub(crate) fn new(nyxd_client: Client, mixnet_contract_cache: MixnetContractCache) -> Self { + PerformanceContractDataProvider { + nyxd_client, + mixnet_contract_cache, + last_submission: None, + } + } + + pub(crate) async fn cache_has_values(&self) -> bool { + let Ok(last_submitted) = self + .nyxd_client + .get_last_performance_contract_submission() + .await + else { + return false; + }; + last_submitted.data.is_some() + } + + pub(crate) async fn provide_initial_warmed_up_cache( + &mut self, + values_to_keep: usize, + ) -> Result<PerformanceContractCacheData, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + self.mixnet_contract_cache + .naive_wait_for_initial_values() + .await; + + // SAFETY: we just waited for cache to be available + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let last = current_epoch.saturating_sub(values_to_keep as u32); + + let mut epoch_performance = BTreeMap::default(); + for epoch in current_epoch..last { + let performance = self.nyxd_client.get_full_epoch_performance(epoch).await?; + let per_epoch_performance = + PerformanceContractEpochCacheData::from_node_performance(performance, epoch); + epoch_performance.insert(epoch, per_epoch_performance); + } + + self.last_submission = Some(last_submitted); + + Ok(PerformanceContractCacheData { epoch_performance }) + } + + async fn refresh(&mut self) -> Result<Option<PerformanceContractEpochCacheData>, NyxdError> { + let last_submitted = self + .nyxd_client + .get_last_performance_contract_submission() + .await?; + + // no updates + if let Some(prior_submission) = &self.last_submission { + if prior_submission == &last_submitted { + return Ok(None); + } + } + + // SAFETY: refresher is not started until the mixnet contract cache had been initialised + #[allow(clippy::unwrap_used)] + let current_epoch = self + .mixnet_contract_cache + .current_interval() + .await + .unwrap() + .current_epoch_absolute_id(); + + let performance = self + .nyxd_client + .get_full_epoch_performance(current_epoch) + .await?; + + self.last_submission = Some(last_submitted); + + Ok(Some( + PerformanceContractEpochCacheData::from_node_performance(performance, current_epoch), + )) + } +} diff --git a/nym-api/src/node_performance/mod.rs b/nym-api/src/node_performance/mod.rs new file mode 100644 index 0000000000..f7f5eb5dd1 --- /dev/null +++ b/nym-api/src/node_performance/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod contract_cache; +pub(crate) mod provider; diff --git a/nym-api/src/node_performance/provider/contract_provider.rs b/nym-api/src/node_performance/provider/contract_provider.rs new file mode 100644 index 0000000000..29923ccd20 --- /dev/null +++ b/nym-api/src/node_performance/provider/contract_provider.rs @@ -0,0 +1,94 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::contract_cache::data::PerformanceContractCacheData; +use crate::node_performance::provider::{NodesRoutingScores, PerformanceRetrievalFailure}; +use crate::support::caching::cache::SharedCache; +use crate::support::config; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use tracing::warn; + +pub(crate) struct ContractPerformanceProvider { + cached: SharedCache<PerformanceContractCacheData>, + max_epochs_fallback: u32, +} + +impl ContractPerformanceProvider { + pub(crate) fn new( + config: &config::PerformanceProvider, + contract_cache: SharedCache<PerformanceContractCacheData>, + ) -> Self { + ContractPerformanceProvider { + cached: contract_cache, + max_epochs_fallback: config.debug.max_performance_fallback_epochs, + } + } + + fn node_routing_score_with_fallback( + &self, + contract_cache: &PerformanceContractCacheData, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let err = match contract_cache.node_routing_score(node_id, epoch_id) { + Ok(res) => return Ok(res), + Err(err) => err, + }; + + warn!("failed to retrieve performance score of node {node_id} for epoch {epoch_id}. falling back to at most {} past epochs", self.max_epochs_fallback); + + let threshold = epoch_id.saturating_sub(self.max_epochs_fallback); + let start = epoch_id.saturating_sub(1); + for epoch_id in start..threshold { + if let Ok(res) = contract_cache.node_routing_score(node_id, epoch_id) { + return Ok(res); + } + } + + Err(err) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + node_id, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id) + } + + pub(crate) async fn node_routing_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + let Some(first) = node_ids.first() else { + return Ok(NodesRoutingScores::empty()); + }; + + let contract_cache = self.cached.get().await.map_err(|_| { + PerformanceRetrievalFailure::new( + *first, + epoch_id, + "performance contract cache has not been initialised yet", + ) + })?; + + let mut scores = HashMap::new(); + for node_id in node_ids { + let score = self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id); + scores.insert(node_id, score); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_performance/provider/legacy_storage_provider.rs b/nym-api/src/node_performance/provider/legacy_storage_provider.rs new file mode 100644 index 0000000000..99a886a19b --- /dev/null +++ b/nym-api/src/node_performance/provider/legacy_storage_provider.rs @@ -0,0 +1,91 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::node_performance::provider::PerformanceRetrievalFailure; +use crate::support::caching::cache::UninitialisedCache; +use crate::support::storage::NymApiStorage; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; + +pub(crate) struct LegacyStoragePerformanceProvider { + storage: NymApiStorage, + mixnet_contract_cache: MixnetContractCache, +} + +impl LegacyStoragePerformanceProvider { + pub(crate) fn new(storage: NymApiStorage, mixnet_contract_cache: MixnetContractCache) -> Self { + LegacyStoragePerformanceProvider { + storage, + mixnet_contract_cache, + } + } + + async fn map_epoch_id_to_end_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result<i64, UninitialisedCache> { + let interval_details = self.mixnet_contract_cache.current_interval().await?; + let duration = interval_details.epoch_length(); + let current_end = interval_details.current_epoch_end(); + let current_id = interval_details.current_epoch_absolute_id(); + + if current_id == epoch_id { + return Ok(current_end.unix_timestamp()); + } + + if current_id < epoch_id { + let diff = epoch_id - current_id; + let end = current_end + diff * duration; + return Ok(end.unix_timestamp()); + } + + // epoch_id > current_id + let diff = current_id - epoch_id; + let end = current_end - diff * duration; + Ok(end.unix_timestamp()) + } + + pub(crate) async fn epoch_id_unix_timestamp( + &self, + epoch_id: EpochId, + ) -> Result<i64, PerformanceRetrievalFailure> { + self.map_epoch_id_to_end_unix_timestamp(epoch_id) + .await + .map_err(|_| { + PerformanceRetrievalFailure::new( + 0, + epoch_id, + "mixnet contract cache has not been initialised yet", + ) + }) + } + + pub(crate) async fn node_routing_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let end_ts = self.epoch_id_unix_timestamp(epoch_id).await?; + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, end_ts) + .await + } + + pub(crate) async fn get_node_routing_score_with_unix_timestamp( + &self, + node_id: NodeId, + epoch_id: EpochId, + end_ts: i64, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + let reliability = self + .storage + .get_average_node_reliability_in_the_last_24hrs(node_id, end_ts) + .await + .map_err(|err| PerformanceRetrievalFailure::new(node_id, epoch_id, err.to_string()))?; + + // reliability: 0-100 + // score: 0-1 + let score = reliability / 100.; + Ok(RoutingScore::new(score as f64)) + } +} diff --git a/nym-api/src/node_performance/provider/mod.rs b/nym-api/src/node_performance/provider/mod.rs new file mode 100644 index 0000000000..aadc61a1d5 --- /dev/null +++ b/nym-api/src/node_performance/provider/mod.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use async_trait::async_trait; +use legacy_storage_provider::LegacyStoragePerformanceProvider; +use nym_api_requests::models::RoutingScore; +use nym_mixnet_contract_common::{EpochId, NodeId}; +use std::collections::HashMap; +use thiserror::Error; +use tracing::{debug, error}; + +pub(crate) mod contract_provider; +pub(crate) mod legacy_storage_provider; + +#[derive(Debug, Error)] +#[error("failed to retrieve performance score for node {node_id} for epoch {epoch_id}: {error}")] +pub(crate) struct PerformanceRetrievalFailure { + pub(crate) node_id: NodeId, + pub(crate) epoch_id: EpochId, + pub(crate) error: String, +} + +impl PerformanceRetrievalFailure { + pub(crate) fn new(node_id: NodeId, epoch_id: EpochId, error: impl Into<String>) -> Self { + PerformanceRetrievalFailure { + node_id, + epoch_id, + error: error.into(), + } + } +} + +pub(crate) struct NodesRoutingScores { + inner: HashMap<NodeId, Result<RoutingScore, PerformanceRetrievalFailure>>, +} + +impl NodesRoutingScores { + pub(crate) fn empty() -> Self { + NodesRoutingScores { + inner: HashMap::new(), + } + } + pub(crate) fn get_or_log(&self, node_id: NodeId) -> RoutingScore { + match self.inner.get(&node_id) { + Some(Ok(score)) => *score, + Some(Err(err)) => { + debug!("{err}"); + RoutingScore::zero() + } + None => RoutingScore::zero(), + } + } +} + +#[async_trait] +pub(crate) trait NodePerformanceProvider { + /// Obtain a performance/routing score of a particular node for given epoch + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure>; + + /// An optimisation for obtaining node scores of multiple nodes at once + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure>; +} + +#[async_trait] +impl NodePerformanceProvider for ContractPerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + self.node_routing_scores(node_ids, epoch_id).await + } +} + +#[async_trait] +impl NodePerformanceProvider for LegacyStoragePerformanceProvider { + #[allow(unused)] + async fn get_node_score( + &self, + node_id: NodeId, + epoch_id: EpochId, + ) -> Result<RoutingScore, PerformanceRetrievalFailure> { + self.node_routing_score(node_id, epoch_id).await + } + + async fn get_batch_node_scores( + &self, + node_ids: Vec<NodeId>, + epoch_id: EpochId, + ) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> { + let mut scores = HashMap::new(); + + let epoch_timestamp = self.epoch_id_unix_timestamp(epoch_id).await?; + for node_id in node_ids { + scores.insert( + node_id, + self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, epoch_timestamp) + .await, + ); + } + + Ok(NodesRoutingScores { inner: scores }) + } +} diff --git a/nym-api/src/node_status_api/cache/config_score.rs b/nym-api/src/node_status_api/cache/config_score.rs new file mode 100644 index 0000000000..30f24d0b40 --- /dev/null +++ b/nym-api/src/node_status_api/cache/config_score.rs @@ -0,0 +1,63 @@ +// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; +use nym_api_requests::models::{ConfigScore, NymNodeDescription}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::VersionScoreFormulaParams; + +fn versions_behind_factor_to_config_score( + versions_behind: u32, + params: VersionScoreFormulaParams, +) -> f64 { + let penalty = params.penalty.naive_to_f64(); + let scaling = params.penalty_scaling.naive_to_f64(); + + // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) + penalty.powf((versions_behind as f64).powf(scaling)) +} + +pub(crate) fn calculate_config_score( + config_score_data: &ConfigScoreData, + described_data: Option<&NymNodeDescription>, +) -> ConfigScore { + let Some(described) = described_data else { + return ConfigScore::unavailable(); + }; + + let node_version = &described.description.build_information.build_version; + let Ok(reported_semver) = node_version.parse::<semver::Version>() else { + return ConfigScore::bad_semver(); + }; + let versions_behind = config_score_data + .config_score_params + .version_weights + .versions_behind_factor( + &reported_semver, + &config_score_data.nym_node_version_history, + ); + + let runs_nym_node = described.description.build_information.binary_name == "nym-node"; + let accepted_terms_and_conditions = described + .description + .auxiliary_details + .accepted_operator_terms_and_conditions; + + let version_score = if !runs_nym_node || !accepted_terms_and_conditions { + 0. + } else { + versions_behind_factor_to_config_score( + versions_behind, + config_score_data + .config_score_params + .version_score_formula_params, + ) + }; + + ConfigScore::new( + version_score, + versions_behind, + accepted_terms_and_conditions, + runs_nym_node, + ) +} diff --git a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs index ab54c30782..3cc9abc807 100644 --- a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs +++ b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs @@ -5,14 +5,9 @@ use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::InclusionProbability; -use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::{NodeId, RewardingParams}; +use nym_mixnet_contract_common::NodeId; use serde::Serialize; use std::time::Duration; -use tracing::error; - -const MAX_SIMULATION_SAMPLES: u64 = 5000; -const MAX_SIMULATION_TIME_SEC: u64 = 15; #[deprecated] #[derive(Clone, Default, Serialize, schemars::JsonSchema)] @@ -25,11 +20,24 @@ pub(crate) struct InclusionProbabilities { } impl InclusionProbabilities { - pub(crate) fn compute( + pub(crate) fn legacy_zero( mixnodes: &[LegacyMixNodeDetailsWithLayer], - params: RewardingParams, - ) -> Option<InclusionProbabilities> { - compute_inclusion_probabilities(mixnodes, params) + ) -> InclusionProbabilities { + // (all legacy mixnodes have 0% chance of being selected) + InclusionProbabilities { + inclusion_probabilities: mixnodes + .iter() + .map(|m| InclusionProbability { + mix_id: m.mix_id(), + in_active: 0.0, + in_reserve: 0.0, + }) + .collect(), + samples: 0, + elapsed: Default::default(), + delta_max: 0.0, + delta_l2: 0.0, + } } pub(crate) fn node(&self, mix_id: NodeId) -> Option<&InclusionProbability> { @@ -38,63 +46,3 @@ impl InclusionProbabilities { .find(|x| x.mix_id == mix_id) } } - -#[deprecated] -fn compute_inclusion_probabilities( - mixnodes: &[LegacyMixNodeDetailsWithLayer], - params: RewardingParams, -) -> Option<InclusionProbabilities> { - let active_set_size = params.active_set_size(); - let standby_set_size = params.rewarded_set.standby; - - // Unzip list of total bonds into ids and bonds. - // We need to go through this zip/unzip procedure to make sure we have matching identities - // for the input to the simulator, which assumes the identity is the position in the vec - let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnodes); - - // Compute inclusion probabilitites and keep track of how long time it took. - let mut rng = rand::thread_rng(); - let results = nym_inclusion_probability::simulate_selection_probability_mixnodes( - &mixnode_total_bonds, - active_set_size as usize, - standby_set_size as usize, - MAX_SIMULATION_SAMPLES, - Duration::from_secs(MAX_SIMULATION_TIME_SEC), - &mut rng, - ) - .inspect_err(|err| error!("{err}")) - .ok()?; - - Some(InclusionProbabilities { - inclusion_probabilities: zip_ids_together_with_results(&ids, &results), - samples: results.samples, - elapsed: results.time, - delta_max: results.delta_max, - delta_l2: results.delta_l2, - }) -} - -fn unzip_into_mixnode_ids_and_total_bonds( - mixnodes: &[LegacyMixNodeDetailsWithLayer], -) -> (Vec<NodeId>, Vec<u128>) { - mixnodes - .iter() - .map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128())) - .unzip() -} - -#[deprecated] -fn zip_ids_together_with_results( - ids: &[NodeId], - results: &nym_inclusion_probability::SelectionProbability, -) -> Vec<InclusionProbability> { - ids.iter() - .zip(results.active_set_probability.iter()) - .zip(results.reserve_set_probability.iter()) - .map(|((&mix_id, a), r)| InclusionProbability { - mix_id, - in_active: *a, - in_reserve: *r, - }) - .collect() -} diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 6cd25189af..8791341b7a 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use self::data::NodeStatusCacheData; +use crate::node_performance::provider::PerformanceRetrievalFailure; +use crate::support::caching::cache::UninitialisedCache; use crate::support::caching::Cache; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; use nym_contracts_common::IdentityKey; @@ -15,21 +17,27 @@ use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; +mod config_score; pub mod data; mod inclusion_probabilities; -mod node_sets; pub mod refresher; #[derive(Debug, Error)] enum NodeStatusCacheError { - #[error("failed to simulate selection probabilities for mixnodes, not updating cache")] - SimulationFailed, - #[error("the current interval information is not available at the moment")] SourceDataMissing, #[error("the self-described cache data is not available")] UnavailableDescribedCache, + + #[error(transparent)] + PerformanceRetrievalFailure(#[from] PerformanceRetrievalFailure), +} + +impl From<UninitialisedCache> for NodeStatusCacheError { + fn from(_: UninitialisedCache) -> Self { + NodeStatusCacheError::SourceDataMissing + } } /// A node status cache suitable for caching values computed in one sweep, such as active set @@ -104,7 +112,7 @@ impl NodeStatusCache { pub(crate) async fn node_annotations( &self, - ) -> Option<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>> { + ) -> Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>> { self.get(|c| &c.node_annotations).await } @@ -119,7 +127,7 @@ impl NodeStatusCache { pub(crate) async fn annotated_legacy_mixnodes( &self, - ) -> Option<RwLockReadGuard<Cache<HashMap<NodeId, MixNodeBondAnnotated>>>> { + ) -> Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, MixNodeBondAnnotated>>>> { self.get(|c| &c.mixnodes_annotated).await } @@ -142,7 +150,7 @@ impl NodeStatusCache { pub(crate) async fn annotated_legacy_gateways( &self, - ) -> Option<RwLockReadGuard<Cache<HashMap<NodeId, GatewayBondAnnotated>>>> { + ) -> Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, GatewayBondAnnotated>>>> { self.get(|c| &c.gateways_annotated).await } diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs deleted file mode 100644 index a0c4df4ee9..0000000000 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::helpers::RewardedSetStatus; -use crate::node_status_api::models::Uptime; -use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; -use crate::nym_contract_cache::cache::data::ConfigScoreData; -use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; -use crate::support::storage::NymApiStorage; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::DescribedNodeType::{LegacyGateway, LegacyMixnode, NymNode}; -use nym_api_requests::models::{ - ConfigScore, DescribedNodeType, DetailedNodePerformance, GatewayBondAnnotated, - MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore, -}; -use nym_contracts_common::NaiveFloat; -use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; -use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; -use nym_topology::CachedEpochRewardedSet; -use std::collections::{HashMap, HashSet}; -use tracing::trace; - -pub(super) async fn get_mixnode_reliability_from_storage( - storage: &NymApiStorage, - mix_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_mixnode_reliability_in_the_last_24hrs( - mix_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_gateway_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_gateway_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -pub(super) async fn get_node_reliability_from_storage( - storage: &NymApiStorage, - node_id: NodeId, - epoch: Interval, -) -> Option<f32> { - storage - .get_average_node_reliability_in_the_last_24hrs( - node_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() -} - -async fn get_routing_score( - storage: &NymApiStorage, - node_id: NodeId, - typ: DescribedNodeType, - epoch: Interval, -) -> RoutingScore { - let maybe_reliability = match typ { - DescribedNodeType::LegacyMixnode => { - get_mixnode_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::LegacyGateway => { - get_gateway_reliability_from_storage(storage, node_id, epoch).await - } - DescribedNodeType::NymNode => { - get_node_reliability_from_storage(storage, node_id, epoch).await - } - }; - // reliability: 0-100 - // score: 0-1 - let reliability = maybe_reliability.unwrap_or_default(); - let score = reliability / 100.; - - trace!("reliability for {node_id}: {maybe_reliability:?}. routing score: {score}"); - RoutingScore::new(score as f64) -} - -fn versions_behind_factor_to_config_score( - versions_behind: u32, - params: VersionScoreFormulaParams, -) -> f64 { - let penalty = params.penalty.naive_to_f64(); - let scaling = params.penalty_scaling.naive_to_f64(); - - // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) - penalty.powf((versions_behind as f64).powf(scaling)) -} - -fn calculate_config_score( - config_score_data: &ConfigScoreData, - described_data: Option<&NymNodeDescription>, -) -> ConfigScore { - let Some(described) = described_data else { - return ConfigScore::unavailable(); - }; - - let node_version = &described.description.build_information.build_version; - let Ok(reported_semver) = node_version.parse::<semver::Version>() else { - return ConfigScore::bad_semver(); - }; - let versions_behind = config_score_data - .config_score_params - .version_weights - .versions_behind_factor( - &reported_semver, - &config_score_data.nym_node_version_history, - ); - - let runs_nym_node = described.description.build_information.binary_name == "nym-node"; - let accepted_terms_and_conditions = described - .description - .auxiliary_details - .accepted_operator_terms_and_conditions; - - let version_score = if !runs_nym_node || !accepted_terms_and_conditions { - 0. - } else { - versions_behind_factor_to_config_score( - versions_behind, - config_score_data - .config_score_params - .version_score_formula_params, - ) - }; - - ConfigScore::new( - version_score, - versions_behind, - accepted_terms_and_conditions, - runs_nym_node, - ) -} - -// TODO: this might have to be moved to a different file if other places also rely on this functionality -fn get_rewarded_set_status( - rewarded_set: &CachedEpochRewardedSet, - node_id: NodeId, -) -> RewardedSetStatus { - if rewarded_set.is_standby(&node_id) { - RewardedSetStatus::Standby - } else if rewarded_set.is_active_mixnode(&node_id) { - RewardedSetStatus::Active - } else { - RewardedSetStatus::Inactive - } -} - -#[deprecated] -pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( - storage: &NymApiStorage, - mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, - interval_reward_params: RewardingParams, - current_interval: Interval, - rewarded_set: &CachedEpochRewardedSet, - blacklist: &HashSet<NodeId>, -) -> HashMap<NodeId, MixNodeBondAnnotated> { - let mut annotated = HashMap::new(); - for mixnode in mixnodes { - let stake_saturation = mixnode - .rewarding_details - .bond_saturation(&interval_reward_params); - - let uncapped_stake_saturation = mixnode - .rewarding_details - .uncapped_bond_saturation(&interval_reward_params); - - let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); - - // If the performance can't be obtained, because the nym-api was not started with - // the monitoring (and hence, storage), then reward estimates will be all zero - let performance = - get_mixnode_reliability_from_storage(storage, mixnode.mix_id(), current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let reward_estimate = compute_reward_estimate( - &mixnode, - performance, - rewarded_set_status, - interval_reward_params, - current_interval, - ); - - let node_performance = storage - .construct_mixnode_report(mixnode.mix_id()) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) - else { - continue; - }; - - let (estimated_operator_apy, estimated_delegators_apy) = - compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - - annotated.insert( - mixnode.mix_id(), - MixNodeBondAnnotated { - blacklisted: blacklist.contains(&mixnode.mix_id()), - mixnode_details: mixnode, - stake_saturation, - uncapped_stake_saturation, - performance, - node_performance, - estimated_operator_apy, - estimated_delegators_apy, - ip_addresses, - }, - ); - } - annotated -} - -#[deprecated] -pub(crate) async fn annotate_legacy_gateways_with_details( - storage: &NymApiStorage, - gateway_bonds: Vec<LegacyGatewayBondWithId>, - current_interval: Interval, - blacklist: &HashSet<NodeId>, -) -> HashMap<NodeId, GatewayBondAnnotated> { - let mut annotated = HashMap::new(); - for gateway_bond in gateway_bonds { - let performance = - get_gateway_reliability_from_storage(storage, gateway_bond.node_id, current_interval) - .await - .map(Uptime::new) - .map(Into::into) - .unwrap_or_default(); - - let node_performance = storage - .construct_gateway_report(gateway_bond.node_id) - .await - .map(NodePerformance::from) - .ok() - .unwrap_or_default(); - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) - else { - continue; - }; - - annotated.insert( - gateway_bond.node_id, - GatewayBondAnnotated { - blacklisted: blacklist.contains(&gateway_bond.node_id), - gateway_bond, - self_described: None, - performance, - node_performance, - ip_addresses, - }, - ); - } - annotated -} - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn produce_node_annotations( - storage: &NymApiStorage, - config_score_data: &ConfigScoreData, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], - nym_nodes: &[NymNodeDetails], - rewarded_set: &CachedEpochRewardedSet, - current_interval: Interval, - described_nodes: &DescribedNodes, -) -> HashMap<NodeId, NodeAnnotation> { - let mut annotations = HashMap::new(); - - for legacy_mix in legacy_mixnodes { - let node_id = legacy_mix.mix_id(); - - let routing_score = - get_routing_score(storage, node_id, LegacyMixnode, current_interval).await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_mix.mix_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for legacy_gateway in legacy_gateways { - let node_id = legacy_gateway.node_id; - let routing_score = - get_routing_score(storage, node_id, LegacyGateway, current_interval).await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_gateway.node_id, - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for nym_node in nym_nodes { - let node_id = nym_node.node_id(); - let routing_score = get_routing_score(storage, node_id, NymNode, current_interval).await; - let config_score = - calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); - - let performance = routing_score.score * config_score.score; - // map it from 0-1 range into 0-100 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - nym_node.node_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - annotations -} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index b9f4aed507..ab323f918f 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,22 +2,32 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::cache::node_sets::produce_node_annotations; +use crate::mixnet_contract_cache::cache::data::ConfigScoreData; +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::{NodePerformanceProvider, NodesRoutingScores}; +use crate::node_status_api::cache::config_score::calculate_config_score; +use crate::node_status_api::models::Uptime; use crate::support::caching::cache::SharedCache; +use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use crate::{ - node_status_api::cache::{inclusion_probabilities, NodeStatusCacheError}, - nym_contract_cache::cache::NymContractCache, - storage::NymApiStorage, - support::caching::CacheNotification, + mixnet_contract_cache::cache::MixnetContractCache, + node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification, }; use ::time::OffsetDateTime; -use nym_task::TaskClient; +use cosmwasm_std::Decimal; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + DetailedNodePerformance, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, + NodePerformance, +}; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails, RewardingParams}; +use nym_task::ShutdownToken; +use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; use tokio::time; -use tracing::{error, info, trace, warn}; +use tracing::{info, trace, warn}; // Long running task responsible for keeping the node status cache up-to-date. pub struct NodeStatusCacheRefresher { @@ -26,49 +36,50 @@ pub struct NodeStatusCacheRefresher { fallback_caching_interval: Duration, // Sources for when refreshing data - contract_cache: NymContractCache, + mixnet_contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, - contract_cache_listener: watch::Receiver<CacheNotification>, + mixnet_contract_cache_listener: watch::Receiver<CacheNotification>, describe_cache_listener: watch::Receiver<CacheNotification>, - storage: NymApiStorage, + + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, } impl NodeStatusCacheRefresher { pub(crate) fn new( cache: NodeStatusCache, fallback_caching_interval: Duration, - contract_cache: NymContractCache, + contract_cache: MixnetContractCache, described_cache: SharedCache<DescribedNodes>, contract_cache_listener: watch::Receiver<CacheNotification>, describe_cache_listener: watch::Receiver<CacheNotification>, - storage: NymApiStorage, + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, ) -> Self { Self { cache, fallback_caching_interval, - contract_cache, + mixnet_contract_cache: contract_cache, described_cache, - contract_cache_listener, + mixnet_contract_cache_listener: contract_cache_listener, describe_cache_listener, - storage, + performance_provider, } } /// Runs the node status cache refresher task. - pub async fn run(&mut self, mut shutdown: TaskClient) { + pub async fn run(&mut self, shutdown_token: ShutdownToken) { let mut last_update = OffsetDateTime::now_utc(); let mut fallback_interval = time::interval(self.fallback_caching_interval); - while !shutdown.is_shutdown() { + while !shutdown_token.is_cancelled() { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / describe cache is updated - Ok(_) = self.contract_cache_listener.changed() => { + Ok(_) = self.mixnet_contract_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); } } @@ -76,7 +87,7 @@ impl NodeStatusCacheRefresher { Ok(_) = self.describe_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); } } @@ -86,7 +97,7 @@ impl NodeStatusCacheRefresher { _ = fallback_interval.tick() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); } } @@ -97,7 +108,8 @@ impl NodeStatusCacheRefresher { } fn caches_available(&self) -> bool { - let contract_cache = *self.contract_cache_listener.borrow() != CacheNotification::Start; + let contract_cache = + *self.mixnet_contract_cache_listener.borrow() != CacheNotification::Start; let describe_cache = *self.describe_cache_listener.borrow() != CacheNotification::Start; let available = contract_cache && describe_cache; @@ -132,42 +144,205 @@ impl NodeStatusCacheRefresher { fallback_interval.reset(); } + #[allow(clippy::too_many_arguments)] + pub(crate) async fn produce_node_annotations( + &self, + config_score_data: &ConfigScoreData, + routing_scores: &NodesRoutingScores, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + rewarded_set: &CachedEpochRewardedSet, + described_nodes: &DescribedNodes, + ) -> HashMap<NodeId, NodeAnnotation> { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let node_id = legacy_mix.mix_id(); + let routing_score = routing_scores.get_or_log(node_id); + + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for legacy_gateway in legacy_gateways { + let node_id = legacy_gateway.node_id; + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let routing_score = routing_scores.get_or_log(node_id); + let config_score = + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); + + let performance = routing_score.score * config_score.score; + // map it from 0-1 range into 0-100 + let scaled_performance = performance * 100.; + let legacy_performance = Uptime::new(scaled_performance as f32).into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: legacy_performance, + current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), + detailed_performance: DetailedNodePerformance::new( + performance, + routing_score, + config_score, + ), + }, + ); + } + + annotations + } + + #[deprecated] + pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( + &self, + mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, + routing_scores: &NodesRoutingScores, + interval_reward_params: RewardingParams, + ) -> HashMap<NodeId, MixNodeBondAnnotated> { + let mut annotated = HashMap::new(); + for mixnode in mixnodes { + let stake_saturation = mixnode + .rewarding_details + .bond_saturation(&interval_reward_params); + + let uncapped_stake_saturation = mixnode + .rewarding_details + .uncapped_bond_saturation(&interval_reward_params); + + let score = routing_scores.get_or_log(mixnode.mix_id()); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) + else { + continue; + }; + + // legacy node will never get rewarded + let estimated_operator_apy = Decimal::zero(); + let estimated_delegators_apy = Decimal::zero(); + + annotated.insert( + mixnode.mix_id(), + MixNodeBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + mixnode_details: mixnode, + stake_saturation, + uncapped_stake_saturation, + performance: score.legacy_performance(), + node_performance: legacy_report, + estimated_operator_apy, + estimated_delegators_apy, + ip_addresses, + }, + ); + } + annotated + } + + #[deprecated] + pub(crate) async fn annotate_legacy_gateways_with_details( + &self, + gateway_bonds: Vec<LegacyGatewayBondWithId>, + routing_scores: &NodesRoutingScores, + ) -> HashMap<NodeId, GatewayBondAnnotated> { + let mut annotated = HashMap::new(); + for gateway_bond in gateway_bonds { + let score = routing_scores.get_or_log(gateway_bond.node_id); + let legacy_report = NodePerformance { + most_recent: score.legacy_performance(), + last_hour: score.legacy_performance(), + last_24h: score.legacy_performance(), + }; + + let Some((ip_addresses, _)) = + legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) + else { + continue; + }; + + annotated.insert( + gateway_bond.node_id, + GatewayBondAnnotated { + // all legacy nodes are always blacklisted + blacklisted: true, + gateway_bond, + self_described: None, + performance: score.legacy_performance(), + node_performance: legacy_report, + ip_addresses, + }, + ); + } + annotated + } + /// Refreshes the node status cache by fetching the latest data from the contract cache #[allow(deprecated)] async fn refresh(&self) -> Result<(), NodeStatusCacheError> { info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; - let interval_reward_params = self.contract_cache.interval_reward_params().await; - let current_interval = self.contract_cache.current_interval().await; - let rewarded_set = self.contract_cache.rewarded_set_owned().await; - let gateway_bonds = self.contract_cache.legacy_gateways_all().await; - let nym_nodes = self.contract_cache.nym_nodes().await; - let config_score_data = self - .contract_cache - .config_score_data_owned() - .await - .into_inner() - .ok_or(NodeStatusCacheError::SourceDataMissing)?; - - // get blacklists - let mixnodes_blacklist = self.contract_cache.mixnodes_blacklist().await; - let gateways_blacklist = self.contract_cache.gateways_blacklist().await; - - let interval_reward_params = - interval_reward_params.ok_or(NodeStatusCacheError::SourceDataMissing)?; - let current_interval = current_interval.ok_or(NodeStatusCacheError::SourceDataMissing)?; + let mixnode_details = self.mixnet_contract_cache.legacy_mixnodes_all().await; + let interval_reward_params = self.mixnet_contract_cache.interval_reward_params().await?; + let current_interval = self.mixnet_contract_cache.current_interval().await?; + let rewarded_set = self.mixnet_contract_cache.rewarded_set_owned().await?; + let gateway_bonds = self.mixnet_contract_cache.legacy_gateways_all().await; + let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; + let config_score_data = self.mixnet_contract_cache.maybe_config_score_data().await?; // Compute inclusion probabilities - let inclusion_probabilities = inclusion_probabilities::InclusionProbabilities::compute( - &mixnode_details, - interval_reward_params, - ) - .ok_or_else(|| { - error!("Failed to simulate selection probabilities for mixnodes, not updating cache"); - NodeStatusCacheError::SimulationFailed - })?; + // (all legacy mixnodes have 0% chance of being selected) + let inclusion_probabilities = crate::node_status_api::cache::inclusion_probabilities::InclusionProbabilities::legacy_zero(&mixnode_details); let Ok(described) = self.described_cache.get().await else { return Err(NodeStatusCacheError::UnavailableDescribedCache); @@ -178,39 +353,48 @@ impl NodeStatusCacheRefresher { legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); } - // Create annotated data - let node_annotations = produce_node_annotations( - &self.storage, - &config_score_data, - &mixnode_details, - &gateway_bonds, - &nym_nodes, - &rewarded_set, - current_interval, - &described, - ) - .await; + let all_ids = mixnode_details + .iter() + .map(|m| m.bond_information.mix_id) + .chain( + gateway_bonds + .iter() + .map(|g| g.node_id) + .chain(nym_nodes.iter().map(|n| n.bond_information.node_id)), + ) + .collect::<Vec<_>>(); - let mixnodes_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_mixnodes_nodes_with_details( - &self.storage, - mixnode_details, - interval_reward_params, - current_interval, + // note: any internal errors imply failures for that node in particular + let routing_scores = self + .performance_provider + .get_batch_node_scores(all_ids, current_interval.current_epoch_absolute_id()) + .await?; + + // Create annotated data + let node_annotations = self + .produce_node_annotations( + &config_score_data, + &routing_scores, + &mixnode_details, + &gateway_bonds, + &nym_nodes, &rewarded_set, - &mixnodes_blacklist, + &described, ) .await; - let gateways_annotated = - crate::node_status_api::cache::node_sets::annotate_legacy_gateways_with_details( - &self.storage, - gateway_bonds, - current_interval, - &gateways_blacklist, + let mixnodes_annotated = self + .annotate_legacy_mixnodes_nodes_with_details( + mixnode_details, + &routing_scores, + interval_reward_params, ) .await; + let gateways_annotated = self + .annotate_legacy_gateways_with_details(gateway_bonds, &routing_scores) + .await; + // Update the cache self.cache .update( diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index b720294af9..f83dd5a12e 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::AxumResult; -use crate::support::caching::cache::UninitialisedCache; use crate::support::http::state::AppState; -use axum::extract::State; +use axum::extract::{Query, State}; use axum::routing::get; -use axum::{Json, Router}; +use axum::Router; use nym_api_requests::models::ConfigScoreDataResponse; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -43,17 +43,21 @@ struct MixIdParam { path = "/config-score-details", context_path = "/v1/status", responses( - (status = 200, body = ConfigScoreDataResponse) + (status = 200, content( + (ConfigScoreDataResponse = "application/json"), + (ConfigScoreDataResponse = "application/yaml"), + (ConfigScoreDataResponse = "application/bincode") + )) ), + params(OutputParams) )] async fn config_score_details( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<ConfigScoreDataResponse>> { - let data = state - .nym_contract_cache() - .maybe_config_score_data_owned() - .await - .ok_or(UninitialisedCache)?; +) -> AxumResult<FormattedResponse<ConfigScoreDataResponse>> { + let output = output.output.unwrap_or_default(); - Ok(Json(data.into_inner().into())) + let data = state.nym_contract_cache().maybe_config_score_data().await?; + + Ok(output.to_response(data.into())) } diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index dbd3720089..559ca11182 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -22,6 +22,7 @@ use nym_api_requests::models::{ MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, UptimeResponse, }; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use serde::Deserialize; use utoipa::IntoParams; @@ -103,17 +104,23 @@ pub(super) fn network_monitor_routes() -> Router<AppState> { get, path = "/v1/status/gateway/{identity}/report", responses( - (status = 200, body = GatewayStatusReportResponse) - ) + (status = 200, content( + (GatewayStatusReportResponse = "application/json"), + (GatewayStatusReportResponse = "application/yaml"), + (GatewayStatusReportResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn gateway_report( Path(identity): Path<String>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<GatewayStatusReportResponse>> { - Ok(Json( - _gateway_report(state.node_status_cache(), &identity).await?, - )) +) -> AxumResult<FormattedResponse<GatewayStatusReportResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_gateway_report(state.node_status_cache(), &identity).await?)) } #[utoipa::path( @@ -121,15 +128,23 @@ async fn gateway_report( get, path = "/v1/status/gateway/{identity}/history", responses( - (status = 200, body = GatewayUptimeHistoryResponse) - ) + (status = 200, content( + (GatewayUptimeHistoryResponse = "application/json"), + (GatewayUptimeHistoryResponse = "application/yaml"), + (GatewayUptimeHistoryResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn gateway_uptime_history( Path(identity): Path<String>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<GatewayUptimeHistoryResponse>> { - Ok(Json( +) -> AxumResult<FormattedResponse<GatewayUptimeHistoryResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _gateway_uptime_history(state.storage(), state.nym_contract_cache(), &identity).await?, )) } @@ -138,6 +153,7 @@ async fn gateway_uptime_history( #[into_params(parameter_in = Query)] struct SinceQueryParams { since: Option<i64>, + output: Option<Output>, } #[utoipa::path( @@ -148,18 +164,22 @@ struct SinceQueryParams { ), path = "/v1/status/gateway/{identity}/core-status-count", responses( - (status = 200, body = GatewayCoreStatusResponse) - ) + (status = 200, content( + (GatewayCoreStatusResponse = "application/json"), + (GatewayCoreStatusResponse = "application/yaml"), + (GatewayCoreStatusResponse = "application/bincode") + )) + ), )] #[deprecated] async fn gateway_core_status_count( Path(identity): Path<String>, - Query(SinceQueryParams { since }): Query<SinceQueryParams>, + Query(SinceQueryParams { since, output }): Query<SinceQueryParams>, State(state): State<AppState>, -) -> AxumResult<Json<GatewayCoreStatusResponse>> { - Ok(Json( - _gateway_core_status_count(state.storage(), &identity, since).await?, - )) +) -> AxumResult<FormattedResponse<GatewayCoreStatusResponse>> { + Ok(output + .unwrap_or_default() + .to_response(_gateway_core_status_count(state.storage(), &identity, since).await?)) } #[utoipa::path( @@ -167,57 +187,71 @@ async fn gateway_core_status_count( get, path = "/v1/status/gateway/{identity}/avg_uptime", responses( - (status = 200, body = GatewayUptimeResponse) - ) + (status = 200, content( + (GatewayUptimeResponse = "application/json"), + (GatewayUptimeResponse = "application/yaml"), + (GatewayUptimeResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_gateway_avg_uptime( Path(identity): Path<String>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<GatewayUptimeResponse>> { - Ok(Json( - _get_gateway_avg_uptime(state.node_status_cache(), &identity).await?, - )) +) -> AxumResult<FormattedResponse<GatewayUptimeResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_get_gateway_avg_uptime(state.node_status_cache(), &identity).await?)) } #[utoipa::path( tag = "network-monitor-status", get, - params( - MixIdParam - ), path = "/v1/status/mixnode/{mix_id}/report", responses( - (status = 200, body = MixnodeStatusReportResponse) - ) + (status = 200, content( + (MixnodeStatusReportResponse = "application/json"), + (MixnodeStatusReportResponse = "application/yaml"), + (MixnodeStatusReportResponse = "application/bincode") + )) + ), + params(OutputParams, MixIdParam) )] #[deprecated] async fn mixnode_report( Path(MixIdParam { mix_id }): Path<MixIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<MixnodeStatusReportResponse>> { - Ok(Json( - _mixnode_report(state.node_status_cache(), mix_id).await?, - )) +) -> AxumResult<FormattedResponse<MixnodeStatusReportResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_mixnode_report(state.node_status_cache(), mix_id).await?)) } #[utoipa::path( tag = "network-monitor-status", get, - params( - MixIdParam - ), path = "/v1/status/mixnode/{mix_id}/history", responses( - (status = 200, body = MixnodeUptimeHistoryResponse) - ) + (status = 200, content( + (MixnodeUptimeHistoryResponse = "application/json"), + (MixnodeUptimeHistoryResponse = "application/yaml"), + (MixnodeUptimeHistoryResponse = "application/bincode") + )) + ), + params(MixIdParam, OutputParams) )] #[deprecated] async fn mixnode_uptime_history( Path(MixIdParam { mix_id }): Path<MixIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<MixnodeUptimeHistoryResponse>> { - Ok(Json( +) -> AxumResult<FormattedResponse<MixnodeUptimeHistoryResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _mixnode_uptime_history(state.storage(), state.nym_contract_cache(), mix_id).await?, )) } @@ -230,37 +264,48 @@ async fn mixnode_uptime_history( ), path = "/v1/status/mixnode/{mix_id}/core-status-count", responses( - (status = 200, body = MixnodeCoreStatusResponse) - ) + (status = 200, content( + (MixnodeCoreStatusResponse = "application/json"), + (MixnodeCoreStatusResponse = "application/yaml"), + (MixnodeCoreStatusResponse = "application/bincode") + )) + ), )] #[deprecated] async fn mixnode_core_status_count( Path(MixIdParam { mix_id }): Path<MixIdParam>, - Query(SinceQueryParams { since }): Query<SinceQueryParams>, + Query(SinceQueryParams { since, output }): Query<SinceQueryParams>, State(state): State<AppState>, -) -> AxumResult<Json<MixnodeCoreStatusResponse>> { - Ok(Json( - _mixnode_core_status_count(state.storage(), mix_id, since).await?, - )) +) -> AxumResult<FormattedResponse<MixnodeCoreStatusResponse>> { + let output = output.unwrap_or_default(); + + Ok(output.to_response(_mixnode_core_status_count(state.storage(), mix_id, since).await?)) } #[utoipa::path( tag = "network-monitor-status", get, params( - MixIdParam + MixIdParam, OutputParams ), path = "/v1/status/mixnode/{mix_id}/reward-estimation", responses( - (status = 200, body = RewardEstimationResponse) - ) + (status = 200, content( + (RewardEstimationResponse = "application/json"), + (RewardEstimationResponse = "application/yaml"), + (RewardEstimationResponse = "application/bincode") + )) + ), )] #[deprecated] async fn get_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path<MixIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<RewardEstimationResponse>> { - Ok(Json( +) -> AxumResult<FormattedResponse<RewardEstimationResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _get_mixnode_reward_estimation( state.node_status_cache(), state.nym_contract_cache(), @@ -274,21 +319,28 @@ async fn get_mixnode_reward_estimation( tag = "network-monitor-status", post, params( - ComputeRewardEstParam, MixIdParam + OutputParams, MixIdParam ), path = "/v1/status/mixnode/{mix_id}/compute-reward-estimation", request_body = ComputeRewardEstParam, responses( - (status = 200, body = RewardEstimationResponse) - ) + (status = 200, content( + (RewardEstimationResponse = "application/json"), + (RewardEstimationResponse = "application/yaml"), + (RewardEstimationResponse = "application/bincode") + )) + ), )] #[deprecated] async fn compute_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path<MixIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, Json(user_reward_param): Json<ComputeRewardEstParam>, -) -> AxumResult<Json<RewardEstimationResponse>> { - Ok(Json( +) -> AxumResult<FormattedResponse<RewardEstimationResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _compute_mixnode_reward_estimation( &user_reward_param, state.node_status_cache(), @@ -303,21 +355,26 @@ async fn compute_mixnode_reward_estimation( tag = "network-monitor-status", get, params( - MixIdParam + MixIdParam, OutputParams ), path = "/v1/status/mixnode/{mix_id}/avg_uptime", responses( - (status = 200, body = UptimeResponse) - ) + (status = 200, content( + (UptimeResponse = "application/json"), + (UptimeResponse = "application/yaml"), + (UptimeResponse = "application/bincode") + )) + ), )] #[deprecated] async fn get_mixnode_avg_uptime( Path(MixIdParam { mix_id }): Path<MixIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<UptimeResponse>> { - Ok(Json( - _get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?, - )) +) -> AxumResult<FormattedResponse<UptimeResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?)) } #[utoipa::path( @@ -325,14 +382,22 @@ async fn get_mixnode_avg_uptime( get, path = "/v1/status/mixnodes/detailed-unfiltered", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_mixnodes_detailed_unfiltered( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<MixNodeBondAnnotated>> { - Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) } #[utoipa::path( @@ -340,14 +405,22 @@ pub async fn get_mixnodes_detailed_unfiltered( get, path = "/v1/status/gateways/detailed", responses( - (status = 200, body = GatewayBondAnnotated) - ) + (status = 200, content( + (GatewayBondAnnotated = "application/json"), + (GatewayBondAnnotated = "application/yaml"), + (GatewayBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_gateways_detailed( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<GatewayBondAnnotated>> { - Json(_get_legacy_gateways_detailed(state.node_status_cache()).await) +) -> FormattedResponse<Vec<GatewayBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_gateways_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -355,12 +428,20 @@ pub async fn get_gateways_detailed( get, path = "/v1/status/gateways/detailed-unfiltered", responses( - (status = 200, body = GatewayBondAnnotated) - ) + (status = 200, content( + (GatewayBondAnnotated = "application/json"), + (GatewayBondAnnotated = "application/yaml"), + (GatewayBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_gateways_detailed_unfiltered( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<GatewayBondAnnotated>> { - Json(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) +) -> FormattedResponse<Vec<GatewayBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) } diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index 76dcf912cd..747ec4620b 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -8,12 +8,12 @@ use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; use anyhow::bail; use axum::extract::{Path, Query, State}; -use axum::Json; use nym_api_requests::models::{ GatewayTestResultResponse, MixnodeTestResultResponse, NetworkMonitorRunDetailsResponse, PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::NodeId; use std::cmp::min; use std::collections::{BTreeMap, HashMap}; @@ -173,14 +173,18 @@ async fn _mixnode_test_results( ), path = "/v1/status/mixnodes/unstable/{mix_id}/test-results", responses( - (status = 200, body = MixnodeTestResultResponse) - ) + (status = 200, content( + (MixnodeTestResultResponse = "application/json"), + (MixnodeTestResultResponse = "application/yaml"), + (MixnodeTestResultResponse = "application/bincode") + )) + ), )] pub async fn mixnode_test_results( Path(mix_id): Path<NodeId>, Query(pagination): Query<PaginationRequest>, State(state): State<AppState>, -) -> AxumResult<Json<MixnodeTestResultResponse>> { +) -> AxumResult<FormattedResponse<MixnodeTestResultResponse>> { let page = pagination.page.unwrap_or_default(); let per_page = min( pagination @@ -188,6 +192,7 @@ pub async fn mixnode_test_results( .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), MAX_TEST_RESULTS_PAGE_SIZE, ); + let output = pagination.output.unwrap_or_default(); match _mixnode_test_results( mix_id, @@ -198,7 +203,7 @@ pub async fn mixnode_test_results( ) .await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve mixnode test results for node {mix_id}: {err}" ))), @@ -272,14 +277,18 @@ async fn _gateway_test_results( ), path = "/v1/status/gateways/unstable/{identity}/test-results", responses( - (status = 200, body = GatewayTestResultResponse) - ) + (status = 200, content( + (GatewayTestResultResponse = "application/json"), + (GatewayTestResultResponse = "application/yaml"), + (GatewayTestResultResponse = "application/bincode") + )) + ), )] pub async fn gateway_test_results( Path(gateway_identity): Path<String>, Query(pagination): Query<PaginationRequest>, State(state): State<AppState>, -) -> AxumResult<Json<GatewayTestResultResponse>> { +) -> AxumResult<FormattedResponse<GatewayTestResultResponse>> { let page = pagination.page.unwrap_or_default(); let per_page = min( pagination @@ -287,6 +296,7 @@ pub async fn gateway_test_results( .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), MAX_TEST_RESULTS_PAGE_SIZE, ); + let output = pagination.output.unwrap_or_default(); match _gateway_test_results( &gateway_identity, @@ -297,7 +307,7 @@ pub async fn gateway_test_results( ) .await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve mixnode test results for gateway {gateway_identity}: {err}" ))), @@ -349,15 +359,23 @@ async fn _latest_monitor_run_report( get, path = "/v1/status/network-monitor/unstable/run/{monitor_run_id}/details", responses( - (status = 200, body = NetworkMonitorRunDetailsResponse) - ) + (status = 200, content( + (NetworkMonitorRunDetailsResponse = "application/json"), + (NetworkMonitorRunDetailsResponse = "application/yaml"), + (NetworkMonitorRunDetailsResponse = "application/bincode") + )) + ), + params(OutputParams) )] pub async fn monitor_run_report( Path(monitor_run_id): Path<i64>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<NetworkMonitorRunDetailsResponse>> { +) -> AxumResult<FormattedResponse<NetworkMonitorRunDetailsResponse>> { + let output = output.output.unwrap_or_default(); + match _monitor_run_report(monitor_run_id, state.storage()).await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve monitor run report for run {monitor_run_id}: {err}" ))), @@ -369,14 +387,22 @@ pub async fn monitor_run_report( get, path = "/v1/status/network-monitor/unstable/run/latest/details", responses( - (status = 200, body = NetworkMonitorRunDetailsResponse) - ) + (status = 200, content( + (NetworkMonitorRunDetailsResponse = "application/json"), + (NetworkMonitorRunDetailsResponse = "application/yaml"), + (NetworkMonitorRunDetailsResponse = "application/bincode") + )) + ), + params(OutputParams) )] pub async fn latest_monitor_run_report( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<NetworkMonitorRunDetailsResponse>> { +) -> AxumResult<FormattedResponse<NetworkMonitorRunDetailsResponse>> { + let output = output.output.unwrap_or_default(); + match _latest_monitor_run_report(state.storage()).await { - Ok(res) => Ok(Json(res)), + Ok(res) => Ok(output.to_response(res)), Err(err) => Err(AxumErrorResponse::internal_msg(format!( "failed to retrieve the latest monitor run report: {err}" ))), diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index e364e1ee31..d66addae61 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -11,13 +11,14 @@ use crate::node_status_api::helpers::{ }; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::{Path, State}; +use axum::extract::{Path, Query, State}; use axum::routing::{get, post}; use axum::Json; use axum::Router; use nym_api_requests::models::{ MixNodeBondAnnotated, MixnodeStatusResponse, StakeSaturationResponse, }; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::MonitorMessage; use tracing::error; @@ -153,15 +154,22 @@ pub(crate) async fn submit_node_monitoring_results( ), path = "/v1/status/mixnode/{mix_id}/status", responses( - (status = 200, body = MixnodeStatusResponse) - ) + (status = 200, content( + (MixnodeStatusResponse = "application/json"), + (MixnodeStatusResponse = "application/yaml"), + (MixnodeStatusResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_mixnode_status( Path(MixIdParam { mix_id }): Path<MixIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<MixnodeStatusResponse> { - Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) +) -> FormattedResponse<MixnodeStatusResponse> { + let output = output.output.unwrap_or_default(); + output.to_response(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) } #[utoipa::path( @@ -172,15 +180,23 @@ async fn get_mixnode_status( ), path = "/v1/status/mixnode/{mix_id}/stake-saturation", responses( - (status = 200, body = StakeSaturationResponse) - ) + (status = 200, content( + (StakeSaturationResponse = "application/json"), + (StakeSaturationResponse = "application/yaml"), + (StakeSaturationResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_mixnode_stake_saturation( Path(mix_id): Path<NodeId>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<StakeSaturationResponse>> { - Ok(Json( +) -> AxumResult<FormattedResponse<StakeSaturationResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response( _get_mixnode_stake_saturation( state.node_status_cache(), state.nym_contract_cache(), @@ -194,22 +210,28 @@ async fn get_mixnode_stake_saturation( tag = "status", get, params( - MixIdParam + MixIdParam, OutputParams ), path = "/v1/status/mixnode/{mix_id}/inclusion-probability", responses( - (status = 200, body = nym_api_requests::models::InclusionProbabilityResponse) - ) + (status = 200, content( + (nym_api_requests::models::InclusionProbabilityResponse = "application/json"), + (nym_api_requests::models::InclusionProbabilityResponse = "application/yaml"), + (nym_api_requests::models::InclusionProbabilityResponse = "application/bincode") + )) + ), )] #[deprecated] #[allow(deprecated)] async fn get_mixnode_inclusion_probability( Path(mix_id): Path<NodeId>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<nym_api_requests::models::InclusionProbabilityResponse>> { - Ok(Json( - _get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?, - )) +) -> AxumResult<FormattedResponse<nym_api_requests::models::InclusionProbabilityResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output + .to_response(_get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?)) } #[utoipa::path( @@ -217,17 +239,23 @@ async fn get_mixnode_inclusion_probability( get, path = "/v1/status/mixnodes/inclusion-probability", responses( - (status = 200, body = nym_api_requests::models::AllInclusionProbabilitiesResponse) - ) + (status = 200, content( + (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/json"), + (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/yaml"), + (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] #[allow(deprecated)] async fn get_mixnode_inclusion_probabilities( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<nym_api_requests::models::AllInclusionProbabilitiesResponse>> { - Ok(Json( - _get_mixnode_inclusion_probabilities(state.node_status_cache()).await?, - )) +) -> AxumResult<FormattedResponse<nym_api_requests::models::AllInclusionProbabilitiesResponse>> { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(_get_mixnode_inclusion_probabilities(state.node_status_cache()).await?)) } #[utoipa::path( @@ -235,14 +263,22 @@ async fn get_mixnode_inclusion_probabilities( get, path = "/v1/status/mixnodes/detailed", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_mixnodes_detailed( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<MixNodeBondAnnotated>> { - Json(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -250,14 +286,22 @@ pub async fn get_mixnodes_detailed( get, path = "/v1/status/mixnodes/rewarded/detailed", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_rewarded_set_detailed( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<MixNodeBondAnnotated>> { - Json( +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response( _get_rewarded_set_legacy_mixnodes_detailed( state.node_status_cache(), state.nym_contract_cache(), @@ -271,14 +315,22 @@ pub async fn get_rewarded_set_detailed( get, path = "/v1/status/mixnodes/active/detailed", responses( - (status = 200, body = MixNodeBondAnnotated) - ) + (status = 200, content( + (MixNodeBondAnnotated = "application/json"), + (MixNodeBondAnnotated = "application/yaml"), + (MixNodeBondAnnotated = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] pub async fn get_active_set_detailed( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<MixNodeBondAnnotated>> { - Json( +) -> FormattedResponse<Vec<MixNodeBondAnnotated>> { + let output = output.output.unwrap_or_default(); + + output.to_response( _get_active_set_legacy_mixnodes_detailed( state.node_status_cache(), state.nym_contract_cache(), diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 363c332c93..e4d6257ace 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -1,39 +1,20 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use super::reward_estimate::compute_reward_estimate; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::storage::NymApiStorage; use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; +use crate::{MixnetContractCache, NodeStatusCache}; use nym_api_requests::models::{ ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusReportResponse, + MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; +use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::NodeId; -pub(crate) enum RewardedSetStatus { - Active, - Standby, - Inactive, -} - -impl From<MixnodeStatus> for RewardedSetStatus { - fn from(value: MixnodeStatus) -> Self { - match value { - MixnodeStatus::Active => RewardedSetStatus::Active, - MixnodeStatus::Standby => RewardedSetStatus::Standby, - // for all intents and purposes, missing node is treated as inactive for rewarding (since it wouldn't get anything - MixnodeStatus::Inactive => RewardedSetStatus::Inactive, - MixnodeStatus::NotFound => RewardedSetStatus::Inactive, - } - } -} - async fn gateway_identity_to_node_id( cache: &NodeStatusCache, identity: &str, @@ -90,7 +71,7 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, identity: &str, ) -> AxumResult<GatewayUptimeHistoryResponse> { let history = storage @@ -144,7 +125,7 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &NymContractCache, + nym_contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<MixnodeUptimeHistoryResponse> { let history = storage @@ -179,7 +160,7 @@ pub(crate) async fn _mixnode_core_status_count( } pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, + cache: &MixnetContractCache, mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { @@ -189,33 +170,22 @@ pub(crate) async fn _get_mixnode_status( pub(crate) async fn _get_mixnode_reward_estimation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<RewardEstimationResponse> { - let status = contract_cache.mixnode_status(mix_id).await; - let mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + // legacy mixnode will never get any rewards + let reward_estimation = RewardEstimate::zero(); - let reward_params = contract_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = contract_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let reward_params = contract_cache.interval_reward_params().await?; + let current_interval = contract_cache.current_interval().await?; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); + // in some very rare edge cases this value might be off (as internals might have got updated between + // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) + let as_at = contract_cache.cache_timestamp().await; Ok(RewardEstimationResponse { estimation: reward_estimation, @@ -226,80 +196,24 @@ pub(crate) async fn _get_mixnode_reward_estimation( } pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: &ComputeRewardEstParam, + _: &ComputeRewardEstParam, status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<RewardEstimationResponse> { - let mut mixnode = status_cache + let _ = status_cache .mixnode_annotated(mix_id) .await .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - let reward_params = contract_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = contract_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let reward_estimation = RewardEstimate::zero(); - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); + let reward_params = contract_cache.interval_reward_params().await?; + let current_interval = contract_cache.current_interval().await?; - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => RewardedSetStatus::Active, - Some(false) => RewardedSetStatus::Standby, - None => { - let actual_status = contract_cache.mixnode_status(mix_id).await; - actual_status.into() - } - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); + // in some very rare edge cases this value might be off (as internals might have got updated between + // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) + let as_at = contract_cache.cache_timestamp().await; Ok(RewardEstimationResponse { estimation: reward_estimation, @@ -311,7 +225,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation( pub(crate) async fn _get_mixnode_stake_saturation( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult<StakeSaturationResponse> { let mixnode = status_cache @@ -321,11 +235,8 @@ pub(crate) async fn _get_mixnode_stake_saturation( // Recompute the stake saturation just so that we can confidently state that the `as_at` // field is consistent and correct. Luckily this is very cheap. - let reward_params = contract_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; + let rewarding_params = contract_cache.interval_reward_params().await?; + let as_at = contract_cache.cache_timestamp().await; Ok(StakeSaturationResponse { saturation: mixnode @@ -422,7 +333,7 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec<MixNodeBondAnnotated> { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); @@ -440,7 +351,7 @@ pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( status_cache: &NodeStatusCache, - contract_cache: &NymContractCache, + contract_cache: &MixnetContractCache, ) -> Vec<MixNodeBondAnnotated> { let Some(rewarded_set) = contract_cache.rewarded_set().await else { return Vec::new(); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index b7b02c327c..4473e0c155 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -2,15 +2,16 @@ // SPDX-License-Identifier: GPL-3.0-only use self::cache::refresher::NodeStatusCacheRefresher; -use crate::node_describe_cache::DescribedNodes; +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::NodePerformanceProvider; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::{ - nym_contract_cache::cache::NymContractCache, - support::{self, storage}, + mixnet_contract_cache::cache::MixnetContractCache, + support::{self}, }; pub(crate) use cache::NodeStatusCache; -use nym_task::TaskManager; +use nym_task::ShutdownManager; use std::time::Duration; use tokio::sync::watch; @@ -18,7 +19,6 @@ pub(crate) mod cache; pub(crate) mod handlers; pub(crate) mod helpers; pub(crate) mod models; -pub(crate) mod reward_estimate; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -33,13 +33,13 @@ pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); #[allow(clippy::too_many_arguments)] pub(crate) fn start_cache_refresh( config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache_state: &MixnetContractCache, described_cache: &SharedCache<DescribedNodes>, node_status_cache_state: &NodeStatusCache, - storage: storage::NymApiStorage, + performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>, nym_contract_cache_listener: watch::Receiver<support::caching::CacheNotification>, described_cache_cache_listener: watch::Receiver<support::caching::CacheNotification>, - shutdown: &TaskManager, + shutdown_manager: &ShutdownManager, ) { let mut nym_api_cache_refresher = NodeStatusCacheRefresher::new( node_status_cache_state.to_owned(), @@ -48,8 +48,8 @@ pub(crate) fn start_cache_refresh( described_cache.clone(), nym_contract_cache_listener, described_cache_cache_listener, - storage, + performance_provider, ); - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown_manager.clone_token("node-status-refresher"); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); } diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index b2af9a6c52..76a9d0199b 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -322,12 +322,12 @@ impl From<HistoricalUptime> for OldHistoricalUptimeResponse { // TODO rocket remove smurf name after eliminating `rocket` pub(crate) type AxumResult<T> = Result<T, AxumErrorResponse>; +pub(crate) type ApiResult<T> = AxumResult<T>; // #[derive(ToSchema, ToResponse)] // #[schema(title = "ErrorResponse")] pub(crate) struct AxumErrorResponse { message: RequestError, - // #[schema(value_type = u16)] status: StatusCode, } diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs deleted file mode 100644 index 6b1798227d..0000000000 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::helpers::RewardedSetStatus; -use cosmwasm_std::Decimal; -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_mixnet_contract_common::reward_params::{ - NodeRewardingParameters, Performance, RewardingParams, -}; -use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::Interval; - -fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { - if pledge_amount.is_zero() { - return Decimal::zero(); - } - let hundred = Decimal::from_ratio(100u32, 1u32); - - epochs_in_year * hundred * reward / pledge_amount -} - -pub fn compute_reward_estimate( - mixnode: &LegacyMixNodeDetailsWithLayer, - performance: Performance, - rewarded_set_status: RewardedSetStatus, - rewarding_params: RewardingParams, - interval: Interval, -) -> RewardEstimate { - if mixnode.is_unbonding() { - return Default::default(); - } - - if performance.is_zero() { - return Default::default(); - } - - let is_active = match rewarded_set_status { - RewardedSetStatus::Active => true, - RewardedSetStatus::Standby => false, - RewardedSetStatus::Inactive => return Default::default(), - }; - - let work_factor = if is_active { - rewarding_params.active_node_work() - } else { - rewarding_params.standby_node_work() - }; - - let node_reward_params = NodeRewardingParameters { - performance, - work_factor, - }; - let node_reward = mixnode - .rewarding_details - .node_reward(&rewarding_params, node_reward_params); - - let node_cost = mixnode - .rewarding_details - .cost_params - .epoch_operating_cost(interval.epochs_in_interval()) - * performance; - - let reward_distribution = mixnode.rewarding_details.determine_reward_split( - node_reward, - performance, - interval.epochs_in_interval(), - ); - - RewardEstimate { - total_node_reward: node_reward, - operator: reward_distribution.operator, - delegates: reward_distribution.delegates, - operating_cost: node_cost, - } -} - -pub fn compute_apy_from_reward( - mixnode: &LegacyMixNodeDetailsWithLayer, - reward_estimate: RewardEstimate, - interval: Interval, -) -> (Decimal, Decimal) { - let epochs_in_year = Decimal::from_ratio(interval.epoch_length_secs(), 3600u64 * 24 * 365); - - let operator = mixnode.rewarding_details.operator; - let total_delegations = mixnode.rewarding_details.delegates; - let estimated_operator_apy = compute_apy(epochs_in_year, reward_estimate.operator, operator); - let estimated_delegators_apy = - compute_apy(epochs_in_year, reward_estimate.delegates, total_delegations); - (estimated_operator_apy, estimated_delegators_apy) -} diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index a68979ced1..c39d4785b5 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -6,7 +6,7 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::ONE_DAY; use crate::storage::NymApiStorage; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{ShutdownManager, ShutdownToken}; use std::time::Duration; use time::macros::time; use time::{OffsetDateTime, PrimitiveDateTime}; @@ -70,7 +70,7 @@ impl HistoricalUptimeUpdater { Ok(()) } - pub(crate) async fn run(&self, mut shutdown: TaskClient) { + pub(crate) async fn run(&self, shutdown_token: ShutdownToken) { // update uptimes at 23:00 UTC each day so that we'd have data from the actual [almost] whole day // and so that we would avoid the edge case of starting validator API at 23:59 and having some // nodes update for different days @@ -98,10 +98,10 @@ impl HistoricalUptimeUpdater { let start = Instant::now() + time_left; let mut interval = interval_at(start, ONE_DAY); - while !shutdown.is_shutdown() { + while !shutdown_token.is_cancelled() { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); } _ = interval.tick() => { @@ -116,9 +116,9 @@ impl HistoricalUptimeUpdater { } } - pub(crate) fn start(storage: NymApiStorage, shutdown: &TaskManager) { + pub(crate) fn start(storage: NymApiStorage, shutdown: &ShutdownManager) { let uptime_updater = HistoricalUptimeUpdater::new(storage); - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown.child_token("uptime-updater"); tokio::spawn(async move { uptime_updater.run(shutdown_listener).await }); } } diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs deleted file mode 100644 index ba2c7d9115..0000000000 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::caching::Cache; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::ConfigScoreDataResponse; -use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::{ - ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, - RewardingParams, -}; -use nym_topology::CachedEpochRewardedSet; -use nym_validator_client::nyxd::AccountId; -use std::collections::{HashMap, HashSet}; - -#[derive(Clone)] -pub(crate) struct ConfigScoreData { - pub(crate) config_score_params: ConfigScoreParams, - pub(crate) nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, -} - -impl From<ConfigScoreData> for ConfigScoreDataResponse { - fn from(value: ConfigScoreData) -> Self { - ConfigScoreDataResponse { - parameters: value.config_score_params.into(), - version_history: value - .nym_node_version_history - .into_iter() - .map(Into::into) - .collect(), - } - } -} - -pub(crate) struct ContractCacheData { - pub(crate) legacy_mixnodes: Cache<Vec<LegacyMixNodeDetailsWithLayer>>, - pub(crate) legacy_gateways: Cache<Vec<LegacyGatewayBondWithId>>, - pub(crate) nym_nodes: Cache<Vec<NymNodeDetails>>, - pub(crate) rewarded_set: Cache<CachedEpochRewardedSet>, - - // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. - // instead clients are meant to be filtering out them themselves based on the provided scores. - pub(crate) legacy_mixnodes_blacklist: Cache<HashSet<NodeId>>, - pub(crate) legacy_gateways_blacklist: Cache<HashSet<NodeId>>, - - pub(crate) config_score_data: Cache<Option<ConfigScoreData>>, - pub(crate) current_reward_params: Cache<Option<RewardingParams>>, - pub(crate) current_interval: Cache<Option<Interval>>, - - pub(crate) contracts_info: Cache<CachedContractsInfo>, -} - -impl ContractCacheData { - pub(crate) fn new() -> Self { - ContractCacheData { - legacy_mixnodes: Cache::default(), - legacy_gateways: Cache::default(), - nym_nodes: Default::default(), - rewarded_set: Cache::default(), - - legacy_mixnodes_blacklist: Cache::default(), - legacy_gateways_blacklist: Cache::default(), - current_interval: Cache::default(), - current_reward_params: Cache::default(), - contracts_info: Cache::default(), - config_score_data: Default::default(), - } - } -} - -type ContractAddress = String; -pub type CachedContractsInfo = HashMap<ContractAddress, CachedContractInfo>; - -#[derive(Clone)] -pub struct CachedContractInfo { - pub(crate) address: Option<AccountId>, - pub(crate) base: Option<cw2::ContractVersion>, - pub(crate) detailed: Option<ContractBuildInformation>, -} - -impl CachedContractInfo { - pub fn new( - address: Option<&AccountId>, - base: Option<cw2::ContractVersion>, - detailed: Option<ContractBuildInformation>, - ) -> Self { - Self { - address: address.cloned(), - base, - detailed, - } - } -} diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs deleted file mode 100644 index 147fe60406..0000000000 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ /dev/null @@ -1,426 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::RefreshData; -use crate::nym_contract_cache::cache::data::{CachedContractsInfo, ConfigScoreData}; -use crate::support::caching::Cache; -use data::ContractCacheData; -use nym_api_requests::legacy::{ - LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, -}; -use nym_api_requests::models::MixnodeStatus; -use nym_crypto::asymmetric::ed25519; -use nym_mixnet_contract_common::{ - ConfigScoreParams, EpochRewardedSet, HistoricalNymNodeVersionEntry, Interval, NodeId, - NymNodeDetails, RewardingParams, -}; -use nym_topology::CachedEpochRewardedSet; -use std::{ - collections::HashSet, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - time::Duration, -}; -use tokio::sync::{RwLock, RwLockReadGuard}; -use tokio::time; -use tracing::{debug, error}; - -pub(crate) mod data; -pub(crate) mod refresher; - -const CACHE_TIMEOUT_MS: u64 = 100; - -#[derive(Clone)] -pub struct NymContractCache { - pub(crate) initialised: Arc<AtomicBool>, - pub(crate) inner: Arc<RwLock<ContractCacheData>>, -} - -impl NymContractCache { - pub(crate) fn new() -> Self { - NymContractCache { - initialised: Arc::new(AtomicBool::new(false)), - inner: Arc::new(RwLock::new(ContractCacheData::new())), - } - } - - /// Returns a copy of the current cache data. - async fn get_owned<T>( - &self, - fn_arg: impl FnOnce(RwLockReadGuard<'_, ContractCacheData>) -> Cache<T>, - ) -> Option<Cache<T>> { - match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { - Ok(cache) => Some(fn_arg(cache)), - Err(e) => { - error!("{e}"); - None - } - } - } - - async fn get<'a, T: 'a>( - &'a self, - fn_arg: impl FnOnce(&ContractCacheData) -> &Cache<T>, - ) -> Option<RwLockReadGuard<'a, Cache<T>>> { - match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { - Ok(cache) => Some(RwLockReadGuard::map(cache, |item| fn_arg(item))), - Err(e) => { - error!("{e}"); - None - } - } - } - - #[allow(clippy::too_many_arguments)] - pub(crate) async fn update( - &self, - mixnodes: Vec<LegacyMixNodeDetailsWithLayer>, - gateways: Vec<LegacyGatewayBondWithId>, - nym_nodes: Vec<NymNodeDetails>, - rewarded_set: EpochRewardedSet, - config_score_params: ConfigScoreParams, - nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, - rewarding_params: RewardingParams, - current_interval: Interval, - nym_contracts_info: CachedContractsInfo, - ) { - match time::timeout(Duration::from_millis(100), self.inner.write()).await { - Ok(mut cache) => { - let config_score_data = ConfigScoreData { - config_score_params, - nym_node_version_history, - }; - - cache.legacy_mixnodes.unchecked_update(mixnodes); - cache.legacy_gateways.unchecked_update(gateways); - cache.nym_nodes.unchecked_update(nym_nodes); - cache.rewarded_set.unchecked_update(rewarded_set); - cache.config_score_data.unchecked_update(config_score_data); - cache - .current_reward_params - .unchecked_update(Some(rewarding_params)); - cache - .current_interval - .unchecked_update(Some(current_interval)); - cache.contracts_info.unchecked_update(nym_contracts_info) - } - Err(err) => { - error!("{err}"); - } - } - } - - pub async fn mixnodes_blacklist(&self) -> Cache<HashSet<NodeId>> { - self.get_owned(|cache| cache.legacy_mixnodes_blacklist.clone_cache()) - .await - .unwrap_or_default() - } - - pub async fn gateways_blacklist(&self) -> Cache<HashSet<NodeId>> { - self.get_owned(|cache| cache.legacy_gateways_blacklist.clone_cache()) - .await - .unwrap_or_default() - } - - pub async fn update_mixnodes_blacklist(&self, add: HashSet<NodeId>, remove: HashSet<NodeId>) { - let blacklist = self.mixnodes_blacklist().await; - let mut blacklist = blacklist.union(&add).cloned().collect::<HashSet<NodeId>>(); - let to_remove = blacklist - .intersection(&remove) - .cloned() - .collect::<HashSet<NodeId>>(); - for key in to_remove { - blacklist.remove(&key); - } - match time::timeout(Duration::from_millis(100), self.inner.write()).await { - Ok(mut cache) => { - cache.legacy_mixnodes_blacklist.unchecked_update(blacklist); - } - Err(err) => { - error!("Failed to update mixnodes blacklist: {err}"); - } - } - } - - pub async fn update_gateways_blacklist(&self, add: HashSet<NodeId>, remove: HashSet<NodeId>) { - let blacklist = self.gateways_blacklist().await; - let mut blacklist = blacklist.union(&add).cloned().collect::<HashSet<_>>(); - let to_remove = blacklist - .intersection(&remove) - .cloned() - .collect::<HashSet<_>>(); - for key in to_remove { - blacklist.remove(&key); - } - match time::timeout(Duration::from_millis(100), self.inner.write()).await { - Ok(mut cache) => { - cache.legacy_gateways_blacklist.unchecked_update(blacklist); - } - Err(err) => { - error!("Failed to update gateways blacklist: {err}"); - } - } - } - - pub async fn legacy_mixnodes_filtered(&self) -> Vec<LegacyMixNodeDetailsWithLayer> { - let mixnodes = self.legacy_mixnodes_all().await; - if mixnodes.is_empty() { - return Vec::new(); - } - let blacklist = self.mixnodes_blacklist().await; - - if !blacklist.is_empty() { - mixnodes - .into_iter() - .filter(|mix| !blacklist.contains(&mix.mix_id())) - .collect() - } else { - mixnodes - } - } - - pub async fn all_cached_legacy_mixnodes( - &self, - ) -> Option<RwLockReadGuard<Cache<Vec<LegacyMixNodeDetailsWithLayer>>>> { - self.get(|c| &c.legacy_mixnodes).await - } - - pub async fn legacy_gateway_owner(&self, node_id: NodeId) -> Option<String> { - self.get(|c| &c.legacy_gateways) - .await? - .iter() - .find(|g| g.node_id == node_id) - .map(|g| g.owner.to_string()) - } - - #[allow(dead_code)] - pub async fn legacy_mixnode_owner(&self, node_id: NodeId) -> Option<String> { - self.get(|c| &c.legacy_mixnodes) - .await? - .iter() - .find(|m| m.mix_id() == node_id) - .map(|m| m.bond_information.owner.to_string()) - } - - pub async fn all_cached_legacy_gateways( - &self, - ) -> Option<RwLockReadGuard<Cache<Vec<LegacyGatewayBondWithId>>>> { - self.get(|c| &c.legacy_gateways).await - } - - pub async fn all_cached_nym_nodes( - &self, - ) -> Option<RwLockReadGuard<Cache<Vec<NymNodeDetails>>>> { - self.get(|c| &c.nym_nodes).await - } - - pub async fn legacy_mixnodes_all(&self) -> Vec<LegacyMixNodeDetailsWithLayer> { - self.get_owned(|cache| cache.legacy_mixnodes.clone_cache()) - .await - .unwrap_or_default() - .into_inner() - } - - pub async fn legacy_mixnodes_all_basic(&self) -> Vec<LegacyMixNodeBondWithLayer> { - self.legacy_mixnodes_all() - .await - .into_iter() - .map(|bond| bond.bond_information) - .collect() - } - - pub async fn legacy_gateways_filtered(&self) -> Vec<LegacyGatewayBondWithId> { - let gateways = self.legacy_gateways_all().await; - if gateways.is_empty() { - return Vec::new(); - } - - let blacklist = self.gateways_blacklist().await; - - if !blacklist.is_empty() { - gateways - .into_iter() - .filter(|gw| !blacklist.contains(&gw.node_id)) - .collect() - } else { - gateways - } - } - - pub async fn legacy_gateways_all(&self) -> Vec<LegacyGatewayBondWithId> { - self.get_owned(|cache| cache.legacy_gateways.clone_cache()) - .await - .unwrap_or_default() - .into_inner() - } - - pub async fn nym_nodes(&self) -> Vec<NymNodeDetails> { - self.get_owned(|cache| cache.nym_nodes.clone_cache()) - .await - .unwrap_or_default() - .into_inner() - } - - pub async fn rewarded_set(&self) -> Option<RwLockReadGuard<Cache<CachedEpochRewardedSet>>> { - self.get(|cache| &cache.rewarded_set).await - } - - pub async fn rewarded_set_owned(&self) -> Cache<CachedEpochRewardedSet> { - self.get_owned(|cache| cache.rewarded_set.clone_cache()) - .await - .unwrap_or_default() - } - - pub async fn maybe_config_score_data_owned(&self) -> Option<Cache<ConfigScoreData>> { - self.config_score_data_owned().await.transpose() - } - - pub async fn config_score_data_owned(&self) -> Cache<Option<ConfigScoreData>> { - self.get_owned(|cache| cache.config_score_data.clone_cache()) - .await - .unwrap_or_default() - } - - pub async fn legacy_v1_rewarded_set_mixnodes(&self) -> Vec<LegacyMixNodeDetailsWithLayer> { - let Some(rewarded_set) = self.rewarded_set().await else { - return Vec::new(); - }; - - let mut rewarded_nodes = rewarded_set - .active_mixnodes() - .into_iter() - .collect::<HashSet<_>>(); - - // rewarded mixnode = active or standby - for standby in &rewarded_set.standby { - rewarded_nodes.insert(*standby); - } - - self.legacy_mixnodes_all() - .await - .into_iter() - .filter(|m| rewarded_nodes.contains(&m.mix_id())) - .collect() - } - - pub async fn legacy_v1_active_set_mixnodes(&self) -> Vec<LegacyMixNodeDetailsWithLayer> { - let Some(rewarded_set) = self.rewarded_set().await else { - return Vec::new(); - }; - - let active_nodes = rewarded_set - .active_mixnodes() - .into_iter() - .collect::<HashSet<_>>(); - - self.legacy_mixnodes_all() - .await - .into_iter() - .filter(|m| active_nodes.contains(&m.mix_id())) - .collect() - } - - pub(crate) async fn interval_reward_params(&self) -> Cache<Option<RewardingParams>> { - self.get_owned(|cache| cache.current_reward_params.clone_cache()) - .await - .unwrap_or_default() - } - - pub(crate) async fn current_interval(&self) -> Cache<Option<Interval>> { - self.get_owned(|cache| cache.current_interval.clone_cache()) - .await - .unwrap_or_default() - } - - pub(crate) async fn contract_details(&self) -> Cache<CachedContractsInfo> { - self.get_owned(|cache| cache.contracts_info.clone_cache()) - .await - .unwrap_or_default() - } - - pub async fn legacy_mixnode_details( - &self, - mix_id: NodeId, - ) -> (Option<LegacyMixNodeDetailsWithLayer>, MixnodeStatus) { - // the old behaviour was to get the nodes from the filtered list, so let's not change it here - let rewarded_set = self.rewarded_set_owned().await; - let all_bonded = &self.legacy_mixnodes_filtered().await; - let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) else { - return (None, MixnodeStatus::NotFound); - }; - - if rewarded_set.is_active_mixnode(&mix_id) { - return (Some(bond.clone()), MixnodeStatus::Active); - } - - if rewarded_set.is_standby(&mix_id) { - return (Some(bond.clone()), MixnodeStatus::Standby); - } - - (Some(bond.clone()), MixnodeStatus::Inactive) - } - - pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { - self.legacy_mixnode_details(mix_id).await.1 - } - - pub async fn get_node_refresh_data( - &self, - node_identity: ed25519::PublicKey, - ) -> Option<RefreshData> { - if !self.initialised() { - return None; - } - - let inner = self.inner.read().await; - - let encoded_identity = node_identity.to_base58_string(); - - // 1. check nymnodes - if let Some(nym_node) = inner - .nym_nodes - .iter() - .find(|n| n.bond_information.identity() == encoded_identity) - { - return nym_node.try_into().ok(); - } - - // 2. check legacy mixnodes - if let Some(mixnode) = inner - .legacy_mixnodes - .iter() - .find(|n| n.bond_information.identity() == encoded_identity) - { - return mixnode.try_into().ok(); - } - - // 3. check legacy gateways - if let Some(gateway) = inner - .legacy_gateways - .iter() - .find(|n| n.identity() == &encoded_identity) - { - return gateway.try_into().ok(); - } - - None - } - - pub fn initialised(&self) -> bool { - self.initialised.load(Ordering::Relaxed) - } - - pub(crate) async fn wait_for_initial_values(&self) { - let initialisation_backoff = Duration::from_secs(5); - loop { - if self.initialised() { - break; - } else { - debug!("Validator cache hasn't been initialised yet - waiting for {:?} before trying again", initialisation_backoff); - tokio::time::sleep(initialisation_backoff).await; - } - } - } -} diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs deleted file mode 100644 index 787d299de5..0000000000 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use super::NymContractCache; -use crate::nym_contract_cache::cache::data::{CachedContractInfo, CachedContractsInfo}; -use crate::nyxd::Client; -use crate::support::caching::CacheNotification; -use anyhow::Result; -use nym_api_requests::legacy::{ - LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, -}; -use nym_mixnet_contract_common::{EpochRewardedSet, LegacyMixLayer}; -use nym_task::TaskClient; -use nym_validator_client::nyxd::contract_traits::{ - MixnetQueryClient, NymContractsProvider, VestingQueryClient, -}; -use rand::prelude::SliceRandom; -use rand::rngs::OsRng; -use std::collections::HashSet; -use std::{collections::HashMap, sync::atomic::Ordering, time::Duration}; -use tokio::sync::watch; -use tokio::time; -use tracing::{error, info, trace, warn}; - -pub struct NymContractCacheRefresher { - nyxd_client: Client, - cache: NymContractCache, - caching_interval: Duration, - - // Notify listeners that the cache has been updated - update_notifier: watch::Sender<CacheNotification>, -} - -impl NymContractCacheRefresher { - pub(crate) fn new( - nyxd_client: Client, - caching_interval: Duration, - cache: NymContractCache, - ) -> Self { - let (tx, _) = watch::channel(CacheNotification::Start); - NymContractCacheRefresher { - nyxd_client, - cache, - caching_interval, - update_notifier: tx, - } - } - - pub fn subscribe(&self) -> watch::Receiver<CacheNotification> { - self.update_notifier.subscribe() - } - - async fn get_nym_contracts_info(&self) -> Result<CachedContractsInfo> { - use crate::query_guard; - - let mut updated = HashMap::new(); - - let client_guard = self.nyxd_client.read().await; - - let mixnet = query_guard!(client_guard, mixnet_contract_address()); - let vesting = query_guard!(client_guard, vesting_contract_address()); - let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); - let group = query_guard!(client_guard, group_contract_address()); - let multisig = query_guard!(client_guard, multisig_contract_address()); - let ecash = query_guard!(client_guard, ecash_contract_address()); - - for (address, name) in [ - (mixnet, "nym-mixnet-contract"), - (vesting, "nym-vesting-contract"), - (coconut_dkg, "nym-coconut-dkg-contract"), - (group, "nym-cw4-group-contract"), - (multisig, "nym-cw3-multisig-contract"), - (ecash, "nym-ecash-contract"), - ] { - let (cw2, build_info) = if let Some(address) = address { - let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); - let mut build_info = query_guard!( - client_guard, - try_get_contract_build_information(address).await - ); - - // for backwards compatibility until we migrate the contracts - if build_info.is_none() { - match name { - "nym-mixnet-contract" => { - build_info = Some(query_guard!( - client_guard, - get_mixnet_contract_version().await - )?) - } - "nym-vesting-contract" => { - build_info = Some(query_guard!( - client_guard, - get_vesting_contract_version().await - )?) - } - _ => (), - } - } - - (cw2, build_info) - } else { - (None, None) - }; - - updated.insert( - name.to_string(), - CachedContractInfo::new(address, cw2, build_info), - ); - } - - Ok(updated) - } - - async fn refresh(&self) -> Result<()> { - let rewarding_params = self.nyxd_client.get_current_rewarding_parameters().await?; - let current_interval = self.nyxd_client.get_current_interval().await?.interval; - - let nym_nodes = self.nyxd_client.get_nymnodes().await?; - let mixnode_details = self.nyxd_client.get_mixnodes().await?; - let gateway_bonds = self.nyxd_client.get_gateways().await?; - let gateway_ids: HashMap<_, _> = self - .nyxd_client - .get_gateway_ids() - .await? - .into_iter() - .map(|id| (id.identity, id.node_id)) - .collect(); - - let mut gateways = Vec::with_capacity(gateway_bonds.len()); - #[allow(clippy::panic)] - for bond in gateway_bonds { - // we explicitly panic here because that value MUST exist. - // if it doesn't, we messed up the migration and we have big problems - let node_id = *gateway_ids.get(bond.identity()).unwrap_or_else(|| { - panic!( - "CONTRACT DATA INCONSISTENCY: MISSING GATEWAY ID FOR: {}", - bond.identity() - ) - }); - gateways.push(LegacyGatewayBondWithId { bond, node_id }) - } - - let rewarded_set = self.get_rewarded_set().await; - let layer1 = rewarded_set - .assignment - .layer1 - .iter() - .collect::<HashSet<_>>(); - let layer2 = rewarded_set - .assignment - .layer2 - .iter() - .collect::<HashSet<_>>(); - let layer3 = rewarded_set - .assignment - .layer3 - .iter() - .collect::<HashSet<_>>(); - - let layer_choices = [ - LegacyMixLayer::One, - LegacyMixLayer::Two, - LegacyMixLayer::Three, - ]; - let mut rng = OsRng; - let mut mixnodes = Vec::with_capacity(mixnode_details.len()); - for detail in mixnode_details { - // if node is not in the rewarded set, well. - // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes - let layer = if layer1.contains(&detail.mix_id()) { - LegacyMixLayer::One - } else if layer2.contains(&detail.mix_id()) { - LegacyMixLayer::Two - } else if layer3.contains(&detail.mix_id()) { - LegacyMixLayer::Three - } else { - // SAFETY: the slice is not empty so the unwrap is fine - #[allow(clippy::unwrap_used)] - layer_choices.choose(&mut rng).copied().unwrap() - }; - - mixnodes.push(LegacyMixNodeDetailsWithLayer { - bond_information: LegacyMixNodeBondWithLayer { - bond: detail.bond_information, - layer, - }, - rewarding_details: detail.rewarding_details, - pending_changes: detail.pending_changes.into(), - }) - } - - let config_score_params = self.nyxd_client.get_config_score_params().await?; - let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; - let contract_info = self.get_nym_contracts_info().await?; - - info!( - "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", - mixnodes.len(), - gateways.len(), - nym_nodes.len(), - ); - - self.cache - .update( - mixnodes, - gateways, - nym_nodes, - rewarded_set, - config_score_params, - nym_node_version_history, - rewarding_params, - current_interval, - contract_info, - ) - .await; - - if let Err(err) = self.update_notifier.send(CacheNotification::Updated) { - warn!("Failed to notify validator cache refresh: {err}"); - } - - Ok(()) - } - - async fn get_rewarded_set(&self) -> EpochRewardedSet { - self.nyxd_client - .get_rewarded_set_nodes() - .await - .unwrap_or_default() - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - let mut interval = time::interval(self.caching_interval); - while !shutdown.is_shutdown() { - tokio::select! { - _ = interval.tick() => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("ValidatorCacheRefresher: Received shutdown"); - } - ret = self.refresh() => { - if let Err(err) = ret { - error!("Failed to refresh validator cache - {err}"); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.initialised.store(true, Ordering::Relaxed) - } - } - } - } - _ = shutdown.recv() => { - trace!("ValidatorCacheRefresher: Received shutdown"); - } - } - } - } -} diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs deleted file mode 100644 index 00f02e1fe2..0000000000 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::helpers::{ - _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, - _get_rewarded_set_legacy_mixnodes_detailed, -}; -use crate::support::http::state::AppState; -use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; -use axum::extract::State; -use axum::{Json, Router}; -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; -use std::collections::HashSet; - -// we want to mark the routes as deprecated in swagger, but still expose them -#[allow(deprecated)] -pub(crate) fn nym_contract_cache_routes() -> Router<AppState> { - Router::new() - .route("/mixnodes", axum::routing::get(get_mixnodes)) - .route( - "/mixnodes/detailed", - axum::routing::get(get_mixnodes_detailed), - ) - .route("/gateways", axum::routing::get(get_gateways)) - .route("/mixnodes/rewarded", axum::routing::get(get_rewarded_set)) - .route( - "/mixnodes/rewarded/detailed", - axum::routing::get(get_rewarded_set_detailed), - ) - .route("/mixnodes/active", axum::routing::get(get_active_set)) - .route( - "/mixnodes/active/detailed", - axum::routing::get(get_active_set_detailed), - ) - .route( - "/mixnodes/blacklisted", - axum::routing::get(get_blacklisted_mixnodes), - ) - .route( - "/gateways/blacklisted", - axum::routing::get(get_blacklisted_gateways), - ) - .route( - "/epoch/reward_params", - axum::routing::get(get_interval_reward_params), - ) - .route("/epoch/current", axum::routing::get(get_current_epoch)) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes", - responses( - (status = 200, body = Vec<LegacyMixNodeDetailsWithLayer>) - ) -)] -#[deprecated] -async fn get_mixnodes(State(state): State<AppState>) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> { - let mut out = state.nym_contract_cache().legacy_mixnodes_filtered().await; - - let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return Json(out); - }; - - let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return Json(out); - }; - - let Ok(annotations) = state.node_annotations().await else { - return Json(out); - }; - - // safety: valid percentage value - #[allow(clippy::unwrap_used)] - let p50 = Performance::from_percentage_value(50).unwrap(); - - for nym_node in &**migrated_nymnodes { - // if we can't get it self-described data, ignore it - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a mixnode, ignore it - if !description.declared_role.mixnode { - continue; - } - // if we don't have annotation for this node, ignore it - let Some(annotation) = annotations.get(&nym_node.node_id()) else { - continue; - }; - // equivalent of legacy mixnode being blacklisted - if annotation.last_24h_performance < p50 { - continue; - } - - let node = to_legacy_mixnode(nym_node, description); - out.push(node); - } - - Json(out) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/detailed")) -// } -// ``` -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/detailed", - responses( - (status = 200, body = Vec<MixNodeBondAnnotated>) - ) -)] -#[deprecated] -async fn get_mixnodes_detailed(State(state): State<AppState>) -> Json<Vec<MixNodeBondAnnotated>> { - _get_legacy_mixnodes_detailed(state.node_status_cache()) - .await - .into() -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/gateways", - responses( - (status = 200, body = Vec<GatewayBond>) - ) -)] -#[deprecated] -async fn get_gateways(State(state): State<AppState>) -> Json<Vec<GatewayBond>> { - // legacy - let mut out: Vec<GatewayBond> = state - .nym_contract_cache() - .legacy_gateways_filtered() - .await - .into_iter() - .map(Into::into) - .collect(); - - let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return Json(out); - }; - - let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return Json(out); - }; - - let Ok(annotations) = state.node_annotations().await else { - return Json(out); - }; - - // safety: valid percentage value - #[allow(clippy::unwrap_used)] - let p50 = Performance::from_percentage_value(50).unwrap(); - - for nym_node in &**migrated_nymnodes { - // if we can't get it self-described data, ignore it - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a gateway, ignore it - if !description.declared_role.entry { - continue; - } - // if we don't have annotation for this node, ignore it - let Some(annotation) = annotations.get(&nym_node.node_id()) else { - continue; - }; - // equivalent of legacy gateway being blacklisted - if annotation.last_24h_performance < p50 { - continue; - } - - let node = to_legacy_gateway(nym_node, description); - out.push(node); - } - - Json(out) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/rewarded", - responses( - (status = 200, body = Vec<LegacyMixNodeDetailsWithLayer>) - ) -)] -#[deprecated] -async fn get_rewarded_set( - State(state): State<AppState>, -) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> { - Json( - state - .nym_contract_cache() - .legacy_v1_rewarded_set_mixnodes() - .await - .clone(), - ) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_set_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) -// } -// ``` -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/rewarded/detailed", - responses( - (status = 200, body = Vec<MixNodeBondAnnotated>) - ) -)] -#[deprecated] -async fn get_rewarded_set_detailed( - State(state): State<AppState>, -) -> Json<Vec<MixNodeBondAnnotated>> { - _get_rewarded_set_legacy_mixnodes_detailed( - state.node_status_cache(), - state.nym_contract_cache(), - ) - .await - .into() -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/active", - responses( - (status = 200, body = Vec<LegacyMixNodeDetailsWithLayer>) - ) -)] -#[deprecated] -async fn get_active_set(State(state): State<AppState>) -> Json<Vec<LegacyMixNodeDetailsWithLayer>> { - let mut out = state - .nym_contract_cache() - .legacy_v1_active_set_mixnodes() - .await - .clone(); - - let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { - return Json(out); - }; - - let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return Json(out); - }; - - let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return Json(out); - }; - - let Ok(annotations) = state.node_annotations().await else { - return Json(out); - }; - - // safety: valid percentage value - #[allow(clippy::unwrap_used)] - let p50 = Performance::from_percentage_value(50).unwrap(); - - for nym_node in &**migrated_nymnodes { - // if we can't get it self-described data, ignore it - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a mixnode, ignore it - if !description.declared_role.mixnode { - continue; - } - // if we don't have annotation for this node, ignore it - let Some(annotation) = annotations.get(&nym_node.node_id()) else { - continue; - }; - // equivalent of legacy mixnode being blacklisted - if annotation.last_24h_performance < p50 { - continue; - } - // if the node is not in the active set, ignore it - if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { - continue; - } - - let node = to_legacy_mixnode(nym_node, description); - out.push(node); - } - - Json(out) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_active_set_detailed() -> Redirect { -// Redirect::to(uri!("/status/mixnodes/active/detailed")) -// } -// ``` - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/active/detailed", - responses( - (status = 200, body = Vec<MixNodeBondAnnotated>) - ) -)] -#[deprecated] -async fn get_active_set_detailed(State(state): State<AppState>) -> Json<Vec<MixNodeBondAnnotated>> { - _get_active_set_legacy_mixnodes_detailed(state.node_status_cache(), state.nym_contract_cache()) - .await - .into() -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/blacklisted", - responses( - (status = 200, body = Option<HashSet<NodeId>>) - ) -)] -#[deprecated] -async fn get_blacklisted_mixnodes(State(state): State<AppState>) -> Json<Option<HashSet<NodeId>>> { - let blacklist = state - .nym_contract_cache() - .mixnodes_blacklist() - .await - .to_owned(); - if blacklist.is_empty() { - None - } else { - Some(blacklist) - } - .into() -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/gateways/blacklisted", - responses( - (status = 200, body = Option<HashSet<String>>) - ) -)] -#[deprecated] -async fn get_blacklisted_gateways(State(state): State<AppState>) -> Json<Option<HashSet<String>>> { - let cache = state.nym_contract_cache(); - let blacklist = cache.gateways_blacklist().await.clone(); - if blacklist.is_empty() { - Json(None) - } else { - let gateways = cache.legacy_gateways_all().await; - Json(Some( - gateways - .into_iter() - .filter(|g| blacklist.contains(&g.node_id)) - .map(|g| g.gateway.identity_key.clone()) - .collect(), - )) - } -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/epoch/reward_params", - responses( - (status = 200, body = Option<RewardingParams>) - ) -)] -async fn get_interval_reward_params( - State(state): State<AppState>, -) -> Json<Option<RewardingParams>> { - state - .nym_contract_cache() - .interval_reward_params() - .await - .to_owned() - .into() -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/epoch/current", - responses( - (status = 200, body = Option<Interval>) - ) -)] -async fn get_current_epoch(State(state): State<AppState>) -> Json<Option<Interval>> { - state - .nym_contract_cache() - .current_interval() - .await - .to_owned() - .into() -} diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/nym_contract_cache/mod.rs deleted file mode 100644 index 6ca509ef1e..0000000000 --- a/nym-api/src/nym_contract_cache/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::nym_contract_cache::cache::NymContractCache; -use crate::support::{self, config, nyxd}; -use nym_task::TaskManager; - -use self::cache::refresher::NymContractCacheRefresher; - -pub(crate) mod cache; -pub(crate) mod handlers; - -pub(crate) fn start_refresher( - config: &config::NodeStatusAPI, - nym_contract_cache_state: &NymContractCache, - nyxd_client: nyxd::Client, - shutdown: &TaskManager, -) -> tokio::sync::watch::Receiver<support::caching::CacheNotification> { - let nym_contract_cache_refresher = NymContractCacheRefresher::new( - nyxd_client, - config.debug.caching_interval, - nym_contract_cache_state.to_owned(), - ); - let nym_contract_cache_listener = nym_contract_cache_refresher.subscribe(); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { nym_contract_cache_refresher.run(shutdown_listener).await }); - - nym_contract_cache_listener -} diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs index adb96f2f76..bc36227c93 100644 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -3,10 +3,10 @@ use crate::support::http::state::AppState; use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; -use axum::extract::State; -use axum::{Json, Router}; -use nym_api_requests::legacy::LegacyMixNodeBondWithLayer; +use axum::extract::{Query, State}; +use axum::Router; use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; +use nym_http_api_common::{FormattedResponse, OutputParams}; use tower_http::compression::CompressionLayer; // we want to mark the routes as deprecated in swagger, but still expose them @@ -29,35 +29,28 @@ pub(crate) fn legacy_nym_node_routes() -> Router<AppState> { get, path = "/v1/gateways/described", responses( - (status = 200, body = Vec<LegacyDescribedGateway>) - ) + (status = 200, content( + (Vec<LegacyDescribedGateway> = "application/json"), + (Vec<LegacyDescribedGateway> = "application/yaml"), + (Vec<LegacyDescribedGateway> = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_gateways_described( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<LegacyDescribedGateway>> { - let contract_cache = state.nym_contract_cache(); +) -> FormattedResponse<Vec<LegacyDescribedGateway>> { let describe_cache = state.described_nodes_cache(); + let output = output.output.unwrap_or_default(); - // legacy - let legacy = contract_cache.legacy_gateways_filtered().await; - - // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways let Ok(describe_cache) = describe_cache.get().await else { - return Json(legacy.into_iter().map(Into::into).collect()); + return output.to_response(Vec::new()); }; let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; - let mut out = Vec::new(); - - for legacy_bond in legacy { - out.push(LegacyDescribedGateway { - self_described: describe_cache - .get_description(&legacy_bond.node_id) - .cloned(), - bond: legacy_bond.bond, - }) - } + let mut described = Vec::new(); for nym_node in migrated_nymnodes { // we ALWAYS need description to set legacy fields @@ -69,13 +62,13 @@ async fn get_gateways_described( continue; } - out.push(LegacyDescribedGateway { + described.push(LegacyDescribedGateway { bond: to_legacy_gateway(&nym_node, description), self_described: Some(description.clone()), }) } - Json(out) + output.to_response(described) } #[utoipa::path( @@ -83,37 +76,28 @@ async fn get_gateways_described( get, path = "/v1/mixnodes/described", responses( - (status = 200, body = Vec<LegacyDescribedMixNode>) - ) + (status = 200, content( + (Vec<LegacyDescribedMixNode> = "application/json"), + (Vec<LegacyDescribedMixNode> = "application/yaml"), + (Vec<LegacyDescribedMixNode> = "application/bincode") + )) + ), + params(OutputParams) )] #[deprecated] async fn get_mixnodes_described( + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> Json<Vec<LegacyDescribedMixNode>> { - let contract_cache = state.nym_contract_cache(); +) -> FormattedResponse<Vec<LegacyDescribedMixNode>> { let describe_cache = state.described_nodes_cache(); + let output = output.output.unwrap_or_default(); - let legacy: Vec<LegacyMixNodeBondWithLayer> = contract_cache - .legacy_mixnodes_filtered() - .await - .into_iter() - .map(|m| m.bond_information) - .collect::<Vec<_>>(); - - // if the self describe cache is unavailable, well, don't attach describe data and only return legacy mixnodes let Ok(describe_cache) = describe_cache.get().await else { - return Json(legacy.into_iter().map(Into::into).collect()); + return output.to_response(Vec::new()); }; let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; - let mut out = Vec::new(); - - for legacy_bond in legacy { - out.push(LegacyDescribedMixNode { - self_described: describe_cache.get_description(&legacy_bond.mix_id).cloned(), - bond: legacy_bond, - }) - } + let mut described = Vec::new(); for nym_node in migrated_nymnodes { // we ALWAYS need description to set legacy fields @@ -125,11 +109,11 @@ async fn get_mixnodes_described( continue; } - out.push(LegacyDescribedMixNode { + described.push(LegacyDescribedMixNode { bond: to_legacy_mixnode(&nym_node, description).bond_information, self_described: Some(description.clone()), }) } - Json(out) + output.to_response(described) } diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index da6ef82c7b..6fed23d30a 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::support::caching::cache::UninitialisedCache; use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; @@ -15,9 +14,9 @@ use nym_api_requests::models::{ }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; +use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NymNodeDetails; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::time::Duration; use time::{Date, OffsetDateTime}; @@ -25,7 +24,6 @@ use tower_http::compression::CompressionLayer; use utoipa::{IntoParams, ToSchema}; pub(crate) mod legacy; -pub(crate) mod unstable; pub(crate) fn nym_node_routes() -> Router<AppState> { Router::new() @@ -55,21 +53,23 @@ pub(crate) fn nym_node_routes() -> Router<AppState> { path = "/rewarded-set", context_path = "/v1/nym-nodes", responses( - (status = 200, body = RewardedSetResponse) + (status = 200, content( + (RewardedSetResponse = "application/json"), + (RewardedSetResponse = "application/yaml"), + (RewardedSetResponse = "application/bincode") + )) ), + params(OutputParams) )] -async fn rewarded_set(State(state): State<AppState>) -> AxumResult<Json<RewardedSetResponse>> { - let cached_rewarded_set = state - .nym_contract_cache() - .rewarded_set() - .await - .map(|cache| cache.clone_cache()) - .ok_or(UninitialisedCache)? - .into_inner(); +async fn rewarded_set( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> AxumResult<FormattedResponse<RewardedSetResponse>> { + let output = output.output.unwrap_or_default(); - Ok(Json( - nym_mixnet_contract_common::EpochRewardedSet::from(cached_rewarded_set).into(), - )) + let rewarded_set = state.nym_contract_cache().rewarded_set_owned().await?; + + Ok(output.to_response(nym_mixnet_contract_common::EpochRewardedSet::from(rewarded_set).into())) } #[utoipa::path( @@ -136,16 +136,21 @@ async fn refresh_described( path = "/noise", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PaginatedResponse<NoiseDetails>) + (status = 200, content( + (PaginatedResponse<NoiseDetails> = "application/json"), + (PaginatedResponse<NoiseDetails> = "application/yaml"), + (PaginatedResponse<NoiseDetails> = "application/bincode") + )) ), params(PaginationRequest) )] async fn nodes_noise( State(state): State<AppState>, Query(pagination): Query<PaginationRequest>, -) -> AxumResult<Json<PaginatedResponse<NoiseDetails>>> { +) -> AxumResult<FormattedResponse<PaginatedResponse<NoiseDetails>>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let describe_cache = state.describe_nodes_cache_data().await?; @@ -155,11 +160,11 @@ async fn nodes_noise( n.description .host_information .keys - .x25519_noise + .x25519_versioned_noise .map(|noise_key| (noise_key, n)) }) .map(|(noise_key, node)| NoiseDetails { - x25119_pubkey: noise_key, + key: noise_key, mixnet_port: node.description.mix_port(), ip_addresses: node.description.host_information.ip_address.clone(), }) @@ -167,7 +172,7 @@ async fn nodes_noise( let total = nodes.len(); - Ok(Json(PaginatedResponse { + Ok(output.to_response(PaginatedResponse { pagination: Pagination { total, page: 0, @@ -183,21 +188,26 @@ async fn nodes_noise( path = "/bonded", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PaginatedResponse<NymNodeDetails>) + (status = 200, content( + (PaginatedResponse<NymNodeDetails> = "application/json"), + (PaginatedResponse<NymNodeDetails> = "application/yaml"), + (PaginatedResponse<NymNodeDetails> = "application/bincode") + )) ), params(PaginationRequest) )] async fn get_bonded_nodes( State(state): State<AppState>, Query(pagination): Query<PaginationRequest>, -) -> Json<PaginatedResponse<NymNodeDetails>> { +) -> FormattedResponse<PaginatedResponse<NymNodeDetails>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let details = state.nym_contract_cache().nym_nodes().await; let total = details.len(); - Json(PaginatedResponse { + output.to_response(PaginatedResponse { pagination: Pagination { total, page: 0, @@ -213,21 +223,26 @@ async fn get_bonded_nodes( path = "/described", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PaginatedResponse<NymNodeDescription>) + (status = 200, content( + (PaginatedResponse<NymNodeDescription> = "application/json"), + (PaginatedResponse<NymNodeDescription> = "application/yaml"), + (PaginatedResponse<NymNodeDescription> = "application/bincode") + )) ), params(PaginationRequest) )] async fn get_described_nodes( State(state): State<AppState>, Query(pagination): Query<PaginationRequest>, -) -> AxumResult<Json<PaginatedResponse<NymNodeDescription>>> { +) -> AxumResult<FormattedResponse<PaginatedResponse<NymNodeDescription>>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let cache = state.described_nodes_cache.get().await?; let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>(); - Ok(Json(PaginatedResponse { + Ok(output.to_response(PaginatedResponse { pagination: Pagination { total: descriptions.len(), page: 0, @@ -243,21 +258,28 @@ async fn get_described_nodes( path = "/annotation/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = AnnotationResponse) + (status = 200, content( + (AnnotationResponse = "application/json"), + (AnnotationResponse = "application/yaml"), + (AnnotationResponse = "application/bincode") + )) ), - params(NodeIdParam), + params(NodeIdParam, OutputParams), )] async fn get_node_annotation( Path(NodeIdParam { node_id }): Path<NodeIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<AnnotationResponse>> { +) -> AxumResult<FormattedResponse<AnnotationResponse>> { + let output = output.output.unwrap_or_default(); + let annotations = state .node_status_cache .node_annotations() .await .ok_or_else(AxumErrorResponse::internal)?; - Ok(Json(AnnotationResponse { + Ok(output.to_response(AnnotationResponse { node_id, annotation: annotations.get(&node_id).cloned(), })) @@ -269,21 +291,28 @@ async fn get_node_annotation( path = "/performance/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = NodePerformanceResponse) + (status = 200, content( + (NodePerformanceResponse = "application/json"), + (NodePerformanceResponse = "application/yaml"), + (NodePerformanceResponse = "application/bincode") + )) ), - params(NodeIdParam), + params(NodeIdParam, OutputParams), )] async fn get_current_node_performance( Path(NodeIdParam { node_id }): Path<NodeIdParam>, + Query(output): Query<OutputParams>, State(state): State<AppState>, -) -> AxumResult<Json<NodePerformanceResponse>> { +) -> AxumResult<FormattedResponse<NodePerformanceResponse>> { + let output = output.output.unwrap_or_default(); + let annotations = state .node_status_cache .node_annotations() .await .ok_or_else(AxumErrorResponse::internal)?; - Ok(Json(NodePerformanceResponse { + Ok(output.to_response(NodePerformanceResponse { node_id, performance: annotations .get(&node_id) @@ -292,12 +321,13 @@ async fn get_current_node_performance( } // todo; probably extract it to requests crate -#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] #[into_params(parameter_in = Query)] pub(crate) struct DateQuery { #[schema(value_type = String, example = "1970-01-01")] - #[schemars(with = "String")] pub(crate) date: Date, + + pub(crate) output: Option<Output>, } #[utoipa::path( @@ -306,21 +336,27 @@ pub(crate) struct DateQuery { path = "/historical-performance/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = NodeDatePerformanceResponse) + (status = 200, content( + (NodeDatePerformanceResponse = "application/json"), + (NodeDatePerformanceResponse = "application/yaml"), + (NodeDatePerformanceResponse = "application/bincode") + )) ), params(DateQuery, NodeIdParam) )] async fn get_historical_performance( Path(NodeIdParam { node_id }): Path<NodeIdParam>, - Query(DateQuery { date }): Query<DateQuery>, + Query(DateQuery { date, output }): Query<DateQuery>, State(state): State<AppState>, -) -> AxumResult<Json<NodeDatePerformanceResponse>> { +) -> AxumResult<FormattedResponse<NodeDatePerformanceResponse>> { + let output = output.unwrap_or_default(); + let uptime = state .storage() .get_historical_node_uptime_on(node_id, date) .await?; - Ok(Json(NodeDatePerformanceResponse { + Ok(output.to_response(NodeDatePerformanceResponse { node_id, date, performance: uptime.and_then(|u| { @@ -337,7 +373,11 @@ async fn get_historical_performance( path = "/performance-history/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PerformanceHistoryResponse) + (status = 200, content( + (PerformanceHistoryResponse = "application/json"), + (PerformanceHistoryResponse = "application/yaml"), + (PerformanceHistoryResponse = "application/bincode") + )) ), params(PaginationRequest, NodeIdParam) )] @@ -345,9 +385,10 @@ async fn get_node_performance_history( Path(NodeIdParam { node_id }): Path<NodeIdParam>, State(state): State<AppState>, Query(pagination): Query<PaginationRequest>, -) -> AxumResult<Json<PerformanceHistoryResponse>> { +) -> AxumResult<FormattedResponse<PerformanceHistoryResponse>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let history = state .storage() @@ -358,7 +399,7 @@ async fn get_node_performance_history( .collect::<Vec<_>>(); let total = history.len(); - Ok(Json(PerformanceHistoryResponse { + Ok(output.to_response(PerformanceHistoryResponse { node_id, history: PaginatedResponse { pagination: Pagination { @@ -377,7 +418,11 @@ async fn get_node_performance_history( path = "/uptime-history/{node_id}", context_path = "/v1/nym-nodes", responses( - (status = 200, body = PerformanceHistoryResponse) + (status = 200, content( + (PerformanceHistoryResponse = "application/json"), + (PerformanceHistoryResponse = "application/yaml"), + (PerformanceHistoryResponse = "application/bincode") + )) ), params(PaginationRequest, NodeIdParam) )] @@ -385,9 +430,10 @@ async fn get_node_uptime_history( Path(NodeIdParam { node_id }): Path<NodeIdParam>, State(state): State<AppState>, Query(pagination): Query<PaginationRequest>, -) -> AxumResult<Json<UptimeHistoryResponse>> { +) -> AxumResult<FormattedResponse<UptimeHistoryResponse>> { // TODO: implement it let _ = pagination; + let output = pagination.output.unwrap_or_default(); let history = state .storage() @@ -398,7 +444,7 @@ async fn get_node_uptime_history( .collect::<Vec<_>>(); let total = history.len(); - Ok(Json(UptimeHistoryResponse { + Ok(output.to_response(UptimeHistoryResponse { node_id, history: PaginatedResponse { pagination: Pagination { diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs deleted file mode 100644 index f858065d2b..0000000000 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -//! All routes/nodes are split into three tiers: -//! -//! `/skimmed` -//! - used by clients -//! - returns the very basic information for routing purposes -//! -//! `/semi-skimmed` -//! - used by other nodes/VPN -//! - returns more additional information such noise keys -//! -//! `/full-fat` -//! - used by explorers, et al. -//! - returns almost everything there is about the nodes -//! -//! There's also additional split based on the role: -//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) -//! - `/mixnodes/<tier>` => only returns mixnode role data -//! - `/gateway/<tier>` => only returns (entry) gateway role data - -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::nym_nodes::handlers::unstable::full_fat::nodes_detailed; -use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; -use crate::nym_nodes::handlers::unstable::skimmed::{ - entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active, - exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active, - nodes_basic_all, -}; -use crate::support::http::helpers::PaginationRequest; -use crate::support::http::state::AppState; -use axum::extract::State; -use axum::routing::{get, post}; -use axum::{Json, Router}; -use nym_api_requests::nym_nodes::{ - NodeRoleQueryParam, NodesByAddressesRequestBody, NodesByAddressesResponse, -}; -use serde::Deserialize; -use std::collections::HashMap; -use tower_http::compression::CompressionLayer; - -pub(crate) mod full_fat; -mod helpers; -pub(crate) mod semi_skimmed; -pub(crate) mod skimmed; - -#[allow(deprecated)] -pub(crate) fn nym_node_routes_unstable() -> Router<AppState> { - Router::new() - .nest( - "/skimmed", - Router::new() - .route("/", get(nodes_basic_all)) - .route("/active", get(nodes_basic_active)) - .nest( - "/mixnodes", - Router::new() - .route("/active", get(mixnodes_basic_active)) - .route("/all", get(mixnodes_basic_all)), - ) - .nest( - "/entry-gateways", - Router::new() - .route("/active", get(entry_gateways_basic_active)) - .route("/all", get(entry_gateways_basic_all)), - ) - .nest( - "/exit-gateways", - Router::new() - .route("/active", get(exit_gateways_basic_active)) - .route("/all", get(exit_gateways_basic_all)), - ), - ) - .nest( - "/semi-skimmed", - Router::new().route("/", get(nodes_expanded)), - ) - .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) - .route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic)) - .route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic)) - .route("/by-addresses", post(nodes_by_addresses)) - .layer(CompressionLayer::new()) -} - -#[derive(Debug, Deserialize, utoipa::IntoParams)] -struct NodesParamsWithRole { - #[param(inline)] - role: Option<NodeRoleQueryParam>, - - #[allow(dead_code)] - semver_compatibility: Option<String>, - no_legacy: Option<bool>, - page: Option<u32>, - per_page: Option<u32>, - - // Identifier for the current epoch of the topology state. When sent by a client we can check if - // the client already knows about the latest topology state, allowing a `no-updates` response - // instead of wasting bandwidth serving an unchanged topology. - epoch_id: Option<u32>, -} - -#[derive(Debug, Deserialize, utoipa::IntoParams)] -#[into_params(parameter_in = Query)] -struct NodesParams { - #[allow(dead_code)] - semver_compatibility: Option<String>, - no_legacy: Option<bool>, - page: Option<u32>, - per_page: Option<u32>, - - // Identifier for the current epoch of the topology state. When sent by a client we can check if - // the client already knows about the latest topology state, allowing a `no-updates` response - // instead of wasting bandwidth serving an unchanged topology. - epoch_id: Option<u32>, -} - -impl From<NodesParamsWithRole> for NodesParams { - fn from(params: NodesParamsWithRole) -> Self { - NodesParams { - semver_compatibility: params.semver_compatibility, - no_legacy: params.no_legacy, - page: params.page, - per_page: params.per_page, - epoch_id: params.epoch_id, - } - } -} - -impl<'a> From<&'a NodesParams> for PaginationRequest { - fn from(params: &'a NodesParams) -> Self { - PaginationRequest { - page: params.page, - per_page: params.per_page, - } - } -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - post, - request_body = NodesByAddressesRequestBody, - path = "/by-addresses", - context_path = "/v1/unstable/nym-nodes", - responses( - (status = 200, body = NodesByAddressesResponse) - ) -)] -async fn nodes_by_addresses( - state: State<AppState>, - Json(body): Json<NodesByAddressesRequestBody>, -) -> AxumResult<Json<NodesByAddressesResponse>> { - // if the request is too big, simply reject it - if body.addresses.len() > 100 { - return Err(AxumErrorResponse::bad_request( - "requested too many addresses", - )); - } - - // TODO: perhaps introduce different cache because realistically nym-api will receive - // request for the same couple addresses from all nodes in quick succession - let describe_cache = state.describe_nodes_cache_data().await?; - - let mut existence = HashMap::new(); - for address in body.addresses { - existence.insert(address, describe_cache.node_with_address(address)); - } - - Ok(Json(NodesByAddressesResponse { existence })) -} diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs deleted file mode 100644 index 8eb5388dea..0000000000 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ /dev/null @@ -1,534 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation}; -use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; -use crate::support::caching::Cache; -use crate::support::http::state::AppState; -use axum::extract::{Query, State}; -use axum::Json; -use nym_api_requests::models::{ - NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, -}; -use nym_api_requests::nym_nodes::{ - CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, -}; -use nym_api_requests::pagination::PaginatedResponse; -use nym_mixnet_contract_common::NodeId; -use nym_topology::CachedEpochRewardedSet; -use std::collections::HashMap; -use std::future::Future; -use tokio::sync::RwLockReadGuard; -use tracing::trace; -use utoipa::ToSchema; - -pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<SkimmedNode>>>; - -/// Given all relevant caches, build part of response for JUST Nym Nodes -fn build_nym_nodes_response<'a, NI>( - rewarded_set: &CachedEpochRewardedSet, - nym_nodes_subset: NI, - annotations: &HashMap<NodeId, NodeAnnotation>, - active_only: bool, -) -> Vec<SkimmedNode> -where - NI: Iterator<Item = &'a NymNodeDescription> + 'a, -{ - let mut nodes = Vec::new(); - for nym_node in nym_nodes_subset { - let node_id = nym_node.node_id; - - let role: NodeRole = rewarded_set.role(node_id).into(); - - // if the role is inactive, see if our filter allows it - if active_only && role.is_inactive() { - continue; - } - - // honestly, not sure under what exact circumstances this value could be missing, - // but in that case just use 0 performance - let annotation = annotations.get(&node_id).copied().unwrap_or_default(); - - nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); - } - nodes -} - -/// Given all relevant caches, add appropriate legacy nodes to the part of the response -fn add_legacy<LN>( - nodes: &mut Vec<SkimmedNode>, - rewarded_set: &CachedEpochRewardedSet, - describe_cache: &DescribedNodes, - annotated_legacy_nodes: &HashMap<NodeId, LN>, - active_only: bool, -) where - LN: LegacyAnnotation, -{ - for (node_id, legacy) in annotated_legacy_nodes.iter() { - let role: NodeRole = rewarded_set.role(*node_id).into(); - - // if the role is inactive, see if our filter allows it - if active_only && role.is_inactive() { - continue; - } - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - nodes.push(described.to_skimmed_node(role, legacy.performance())) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } -} - -// hehe, what an abomination, but it's used in multiple different places and I hate copy-pasting code, -// especially if it has multiple loops, etc -async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( - state: &'a AppState, - Query(query_params): Query<NodesParams>, - nym_nodes_subset: NI, - annotated_legacy_nodes_getter: LG, - active_only: bool, -) -> PaginatedSkimmedNodes -where - // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) - NI: Iterator<Item = &'a NymNodeDescription> + 'a, - - // async function that returns cache of appropriate legacy nodes (mixnodes or gateways) - LG: Fn(&'a AppState) -> Fut, - Fut: - Future<Output = Result<RwLockReadGuard<'a, Cache<HashMap<NodeId, LN>>>, AxumErrorResponse>>, - - // the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated) - LN: LegacyAnnotation + 'a, -{ - // TODO: implement it - let _ = query_params.per_page; - let _ = query_params.page; - - // 1. get the rewarded set - let rewarded_set = state.rewarded_set().await?; - - // 2. grab all annotations so that we could attach scores to the [nym] nodes - let annotations = state.node_annotations().await?; - - // 3. implicitly grab the relevant described nodes - // (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler) - let describe_cache = state.describe_nodes_cache_data().await?; - - let maybe_interval = state - .nym_contract_cache() - .current_interval() - .await - .to_owned(); - - // 4.0 If the client indicates that they already know about the current topology send empty response - if let Some(client_known_epoch) = query_params.epoch_id { - if let Some(ref interval) = maybe_interval { - if client_known_epoch == interval.current_epoch_id() { - return Ok(Json(PaginatedCachedNodesResponse::no_updates())); - } - } - } - - // 4. start building the response - let mut nodes = - build_nym_nodes_response(&rewarded_set, nym_nodes_subset, &annotations, active_only); - - // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes - if let Some(true) = query_params.no_legacy { - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - ]); - - return Ok(Json( - PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval), - )); - } - - // 6. grab relevant legacy nodes - // (due to the existence of the legacy endpoints, we already have fully annotated data on them) - let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &annotated_legacy_nodes, - active_only, - ); - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - annotated_legacy_nodes.timestamp(), - ]); - - Ok(Json( - PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval), - )) -} - -/// Deprecated query that gets ALL gateways -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/gateways/skimmed", - context_path = "/v1/unstable/nym-nodes", - responses( - (status = 200, body = CachedNodesResponse<SkimmedNode>) - ) -)] -#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] -pub(super) async fn deprecated_gateways_basic( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> { - // 1. call '/v1/unstable/skimmed/entry-gateways/all' - let all_gateways = entry_gateways_basic_all(state, query_params).await?; - - // 3. return result - Ok(Json(CachedNodesResponse { - refreshed_at: all_gateways.refreshed_at, - // 2. remove pagination - nodes: all_gateways.0.nodes.data, - })) -} - -/// Deprecated query that gets ACTIVE-ONLY mixnodes -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/mixnodes/skimmed", - context_path = "/v1/unstable/nym-nodes", - responses( - (status = 200, body = CachedNodesResponse<SkimmedNode>) - ) -)] -#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")] -pub(super) async fn deprecated_mixnodes_basic( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> { - // 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active' - let active_mixnodes = mixnodes_basic_active(state, query_params).await?; - - // 3. return result - Ok(Json(CachedNodesResponse { - refreshed_at: active_mixnodes.refreshed_at, - // 2. remove pagination - nodes: active_mixnodes.0.nodes.data, - })) -} - -async fn nodes_basic( - state: State<AppState>, - Query(_query_params): Query<NodesParams>, - active_only: bool, -) -> PaginatedSkimmedNodes { - // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes - - // 1. grab all relevant described nym-nodes - let rewarded_set = state.rewarded_set().await?; - - let describe_cache = state.describe_nodes_cache_data().await?; - let all_nym_nodes = describe_cache.all_nym_nodes(); - let annotations = state.node_annotations().await?; - let legacy_mixnodes = state.legacy_mixnode_annotations().await?; - let legacy_gateways = state.legacy_gateways_annotations().await?; - - let mut nodes = - build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, active_only); - - // add legacy gateways to the response - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &legacy_gateways, - active_only, - ); - - // add legacy mixnodes to the response - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &legacy_mixnodes, - active_only, - ); - - // min of all caches - let refreshed_at = refreshed_at([ - rewarded_set.timestamp(), - annotations.timestamp(), - describe_cache.timestamp(), - legacy_mixnodes.timestamp(), - legacy_gateways.timestamp(), - ]); - - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) -} - -#[allow(dead_code)] // not dead, used in OpenAPI docs -#[derive(ToSchema)] -#[schema(title = "PaginatedCachedNodesResponse")] -pub struct PaginatedCachedNodesResponseSchema { - pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, - #[schema(value_type = SkimmedNode)] - pub nodes: PaginatedResponse<SkimmedNode>, -} - -/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) -/// that are currently bonded. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParamsWithRole), - path = "", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn nodes_basic_all( - state: State<AppState>, - Query(query_params): Query<NodesParamsWithRole>, -) -> PaginatedSkimmedNodes { - if let Some(role) = query_params.role { - return match role { - NodeRoleQueryParam::ActiveMixnode => { - mixnodes_basic_all(state, Query(query_params.into())).await - } - NodeRoleQueryParam::EntryGateway => { - entry_gateways_basic_all(state, Query(query_params.into())).await - } - NodeRoleQueryParam::ExitGateway => { - exit_gateways_basic_all(state, Query(query_params.into())).await - } - }; - } - - nodes_basic(state, Query(query_params.into()), false).await -} - -/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) -/// that are currently bonded and are in the **active set** -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/active", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn nodes_basic_active( - state: State<AppState>, - Query(query_params): Query<NodesParamsWithRole>, -) -> PaginatedSkimmedNodes { - if let Some(role) = query_params.role { - return match role { - NodeRoleQueryParam::ActiveMixnode => { - mixnodes_basic_active(state, Query(query_params.into())).await - } - NodeRoleQueryParam::EntryGateway => { - entry_gateways_basic_active(state, Query(query_params.into())).await - } - NodeRoleQueryParam::ExitGateway => { - exit_gateways_basic_active(state, Query(query_params.into())).await - } - }; - } - - nodes_basic(state, Query(query_params.into()), true).await -} - -async fn mixnodes_basic( - state: State<AppState>, - query_params: Query<NodesParams>, - active_only: bool, -) -> PaginatedSkimmedNodes { - // 1. grab all relevant described nym-nodes - let describe_cache = state.describe_nodes_cache_data().await?; - let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); - - build_skimmed_nodes_response( - &state.0, - query_params, - mixing_nym_nodes, - |state| state.legacy_mixnode_annotations(), - active_only, - ) - .await -} - -/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) -/// that are currently bonded and support mixing role. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/mixnodes/all", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn mixnodes_basic_all( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> PaginatedSkimmedNodes { - mixnodes_basic(state, query_params, false).await -} - -/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) -/// that are currently bonded and are in the active set with one of the mixing roles. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/mixnodes/active", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn mixnodes_basic_active( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> PaginatedSkimmedNodes { - mixnodes_basic(state, query_params, true).await -} - -async fn entry_gateways_basic( - state: State<AppState>, - query_params: Query<NodesParams>, - active_only: bool, -) -> PaginatedSkimmedNodes { - // 1. grab all relevant described nym-nodes - let describe_cache = state.describe_nodes_cache_data().await?; - let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes(); - - build_skimmed_nodes_response( - &state.0, - query_params, - mixing_nym_nodes, - |state| state.legacy_gateways_annotations(), - active_only, - ) - .await -} - -/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) -/// that are currently bonded and are in the active set with the entry role. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/entry-gateways/active", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn entry_gateways_basic_active( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> PaginatedSkimmedNodes { - entry_gateways_basic(state, query_params, true).await -} - -/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) -/// that are currently bonded and support entry gateway role. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/entry-gateways/all", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn entry_gateways_basic_all( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> PaginatedSkimmedNodes { - entry_gateways_basic(state, query_params, false).await -} - -async fn exit_gateways_basic( - state: State<AppState>, - query_params: Query<NodesParams>, - active_only: bool, -) -> PaginatedSkimmedNodes { - // 1. grab all relevant described nym-nodes - let describe_cache = state.describe_nodes_cache_data().await?; - let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes(); - - build_skimmed_nodes_response( - &state.0, - query_params, - mixing_nym_nodes, - |state| state.legacy_gateways_annotations(), - active_only, - ) - .await -} - -/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) -/// that are currently bonded and are in the active set with the exit role. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/exit-gateways/active", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn exit_gateways_basic_active( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> PaginatedSkimmedNodes { - exit_gateways_basic(state, query_params, true).await -} - -/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) -/// that are currently bonded and support exit gateway role. -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/exit-gateways/all", - context_path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = PaginatedCachedNodesResponseSchema) - ) -)] -pub(super) async fn exit_gateways_basic_all( - state: State<AppState>, - query_params: Query<NodesParams>, -) -> PaginatedSkimmedNodes { - exit_gateways_basic(state, query_params, false).await -} diff --git a/nym-api/src/signers_cache/cache/data.rs b/nym-api/src/signers_cache/cache/data.rs new file mode 100644 index 0000000000..5111033b0b --- /dev/null +++ b/nym-api/src/signers_cache/cache/data.rs @@ -0,0 +1,2 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only diff --git a/nym-api/src/signers_cache/cache/mod.rs b/nym-api/src/signers_cache/cache/mod.rs new file mode 100644 index 0000000000..a44ab4da1b --- /dev/null +++ b/nym-api/src/signers_cache/cache/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ecash_signer_check::SignersTestResult; + +pub(crate) mod data; +pub(crate) mod refresher; + +pub(crate) struct SignersCacheData { + pub(crate) signers_results: SignersTestResult, +} diff --git a/nym-api/src/signers_cache/cache/refresher.rs b/nym-api/src/signers_cache/cache/refresher.rs new file mode 100644 index 0000000000..aa0a341a6f --- /dev/null +++ b/nym-api/src/signers_cache/cache/refresher.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::signers_cache::cache::SignersCacheData; +use crate::support::caching::refresher::CacheItemProvider; +use crate::support::nyxd::Client; +use async_trait::async_trait; +use nym_ecash_signer_check::{check_signers_with_client, SignerCheckError}; + +pub(crate) struct SignersCacheDataProvider { + nyxd_client: Client, +} + +#[async_trait] +impl CacheItemProvider for SignersCacheDataProvider { + type Item = SignersCacheData; + type Error = SignerCheckError; + + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> { + self.refresh().await.map(Some) + } +} + +impl SignersCacheDataProvider { + pub(crate) fn new(nyxd_client: Client) -> Self { + SignersCacheDataProvider { nyxd_client } + } + + async fn refresh(&self) -> Result<SignersCacheData, SignerCheckError> { + let signers_results = check_signers_with_client(&self.nyxd_client).await?; + Ok(SignersCacheData { signers_results }) + } +} diff --git a/nym-api/src/signers_cache/handlers.rs b/nym-api/src/signers_cache/handlers.rs new file mode 100644 index 0000000000..23046caad1 --- /dev/null +++ b/nym-api/src/signers_cache/handlers.rs @@ -0,0 +1,95 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::ApiResult; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::routing::get; +use axum::Router; +use nym_api_requests::models::{ + DetailedSignersStatusResponse, DetailedSignersStatusResponseBody, SignersStatusOverview, + SignersStatusResponse, SignersStatusResponseBody, +}; +use nym_api_requests::signable::SignableMessageBody; +use nym_http_api_common::{FormattedResponse, OutputParams}; + +pub(crate) fn signers_routes() -> Router<AppState> { + Router::new() + .route("/status", get(signers_status)) + .route("/status-detailed", get(signers_status_detailed)) +} + +#[utoipa::path( + tag = "network", + get, + context_path = "/v1/network/signers", + path = "/status", + responses( + (status = 200, content( + (SignersStatusResponse = "application/json"), + (SignersStatusResponse = "application/yaml"), + (SignersStatusResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +pub(crate) async fn signers_status( + Query(params): Query<OutputParams>, + State(state): State<AppState>, +) -> ApiResult<FormattedResponse<SignersStatusResponse>> { + let output = params.get_output(); + + let cached = state.ecash_signers_cache.get().await?; + let as_at = cached.timestamp(); + Ok(output.to_response( + SignersStatusResponseBody { + as_at, + overview: SignersStatusOverview::new( + &cached.signers_results.results, + cached.signers_results.threshold, + ), + results: cached + .signers_results + .results + .iter() + .map(Into::into) + .collect(), + } + .sign(state.private_signing_key()), + )) +} + +#[utoipa::path( + tag = "network", + get, + context_path = "/v1/network/signers", + path = "/status-detailed", + responses( + (status = 200, content( + (DetailedSignersStatusResponse = "application/json"), + (DetailedSignersStatusResponse = "application/yaml"), + (DetailedSignersStatusResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +pub(crate) async fn signers_status_detailed( + Query(params): Query<OutputParams>, + State(state): State<AppState>, +) -> ApiResult<FormattedResponse<DetailedSignersStatusResponse>> { + let output = params.get_output(); + + let cached = state.ecash_signers_cache.get().await?; + let as_at = cached.timestamp(); + Ok(output.to_response( + DetailedSignersStatusResponseBody { + as_at, + overview: SignersStatusOverview::new( + &cached.signers_results.results, + cached.signers_results.threshold, + ), + details: cached.signers_results.results.clone(), + } + .sign(state.private_signing_key()), + )) +} diff --git a/nym-api/src/signers_cache/mod.rs b/nym-api/src/signers_cache/mod.rs new file mode 100644 index 0000000000..c902a4d52c --- /dev/null +++ b/nym-api/src/signers_cache/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::signers_cache::cache::refresher::SignersCacheDataProvider; +use crate::signers_cache::cache::SignersCacheData; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::CacheRefresher; +use crate::support::{config, nyxd}; +use nym_task::ShutdownManager; + +pub(crate) mod cache; +pub(crate) mod handlers; + +pub(crate) fn start_refresher( + config: &config::SignersCache, + nyxd_client: nyxd::Client, + shutdown_manager: &ShutdownManager, +) -> SharedCache<SignersCacheData> { + let refresher = CacheRefresher::new( + SignersCacheDataProvider::new(nyxd_client), + config.debug.refresh_interval, + ) + .named("signers-cache-refresher"); + let shared_cache = refresher.get_shared_cache(); + refresher.start_with_delay( + shutdown_manager.clone_token("signers-cache-refresher"), + config.debug.refresher_start_delay, + ); + shared_cache +} diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index 1a280eb5df..aa1d5101b2 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -3,16 +3,16 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::status::ApiStatusState; +use crate::support::config::CHAIN_STALL_THRESHOLD; use crate::support::http::state::AppState; -use axum::extract::State; -use axum::Json; +use axum::extract::{Query, State}; use axum::Router; use nym_api_requests::models::{ ApiHealthResponse, ApiStatus, ChainStatus, SignerInformationResponse, }; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; -use std::time::Duration; +use nym_http_api_common::{FormattedResponse, OutputParams}; use time::OffsetDateTime; pub(crate) fn api_status_routes() -> Router<AppState> { @@ -30,11 +30,19 @@ pub(crate) fn api_status_routes() -> Router<AppState> { get, path = "/v1/api-status/health", responses( - (status = 200, body = ApiHealthResponse) - ) + (status = 200, content( + (ApiHealthResponse = "application/json"), + (ApiHealthResponse = "application/yaml"), + (ApiHealthResponse = "application/bincode") + )) + ), + params(OutputParams) )] -async fn health(State(state): State<AppState>) -> Json<ApiHealthResponse> { - const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60); +async fn health( + Query(output): Query<OutputParams>, + State(state): State<AppState>, +) -> FormattedResponse<ApiHealthResponse> { + let output = output.output.unwrap_or_default(); let uptime = state.api_status.startup_time.elapsed(); let chain_status = match state @@ -44,15 +52,7 @@ async fn health(State(state): State<AppState>) -> Json<ApiHealthResponse> { { Ok(res) => { let now = OffsetDateTime::now_utc(); - let block_time: OffsetDateTime = res.latest_block.block.header.time.into(); - let diff = now - block_time; - if diff > CHAIN_STALL_THRESHOLD { - ChainStatus::Stalled { - approximate_amount: diff.unsigned_abs(), - } - } else { - ChainStatus::Synced - } + res.stall_status(now, CHAIN_STALL_THRESHOLD) } Err(_) => ChainStatus::Unknown, }; @@ -61,7 +61,7 @@ async fn health(State(state): State<AppState>) -> Json<ApiHealthResponse> { chain_status, uptime: uptime.as_secs(), }; - Json(health) + output.to_response(health) } #[utoipa::path( @@ -69,13 +69,21 @@ async fn health(State(state): State<AppState>) -> Json<ApiHealthResponse> { get, path = "/v1/api-status/build-information", responses( - (status = 200, body = BinaryBuildInformationOwned) - ) + (status = 200, content( + (BinaryBuildInformationOwned = "application/json"), + (BinaryBuildInformationOwned = "application/yaml"), + (BinaryBuildInformationOwned = "application/bincode") + )) + ), + params(OutputParams) )] async fn build_information( + Query(output): Query<OutputParams>, State(state): State<ApiStatusState>, -) -> Json<BinaryBuildInformationOwned> { - Json(state.build_information.to_owned()) +) -> FormattedResponse<BinaryBuildInformationOwned> { + let output = output.output.unwrap_or_default(); + + output.to_response(state.build_information.to_owned()) } #[utoipa::path( @@ -83,17 +91,25 @@ async fn build_information( get, path = "/v1/api-status/signer-information", responses( - (status = 200, body = SignerInformationResponse) - ) + (status = 200, content( + (SignerInformationResponse = "application/json"), + (SignerInformationResponse = "application/yaml"), + (SignerInformationResponse = "application/bincode") + )) + ), + params(OutputParams) )] async fn signer_information( + Query(output): Query<OutputParams>, State(state): State<ApiStatusState>, -) -> AxumResult<Json<SignerInformationResponse>> { +) -> AxumResult<FormattedResponse<SignerInformationResponse>> { let signer_state = state.signer_information.as_ref().ok_or_else(|| { AxumErrorResponse::internal_msg("this api does not expose zk-nym signing functionalities") })?; - Ok(Json(SignerInformationResponse { + let output = output.output.unwrap_or_default(); + + Ok(output.to_response(SignerInformationResponse { cosmos_address: signer_state.cosmos_address.clone(), identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index da746399f5..4923b70a9f 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -7,6 +7,7 @@ use std::time::Duration; use thiserror::Error; use time::OffsetDateTime; use tokio::sync::{RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard}; +use tracing::debug; #[derive(Debug, Error)] #[error("the cache item has not been initialised")] @@ -31,13 +32,60 @@ impl<T> SharedCache<T> { SharedCache::default() } - pub(crate) async fn update(&self, value: impl Into<T>) { - let mut guard = self.0.write().await; + pub(crate) fn new_with_value(value: T) -> Self { + SharedCache(Arc::new(RwLock::new(CachedItem { + inner: Some(Cache::new(value)), + }))) + } + + pub(crate) async fn try_update_value<S>( + &self, + update: S, + update_fn: impl Fn(&mut T, S), + typ: &str, + ) -> Result<(), S> + where + S: Into<T>, + { + let update_value = update; + let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await + { + Ok(guard) => guard, + Err(_) => { + debug!("failed to obtain write permit for {typ} cache"); + return Err(update_value); + } + }; + + if let Some(ref mut existing) = guard.inner { + existing.update(update_value, update_fn); + } else { + guard.inner = Some(Cache::new(update_value.into())) + }; + Ok(()) + } + + pub(crate) async fn try_overwrite_old_value( + &self, + value: impl Into<T>, + typ: &str, + ) -> Result<(), T> { + let value = value.into(); + let mut guard = match tokio::time::timeout(Duration::from_millis(200), self.0.write()).await + { + Ok(guard) => guard, + Err(_) => { + debug!("failed to obtain write permit for {typ} cache"); + return Err(value); + } + }; + if let Some(ref mut existing) = guard.inner { existing.unchecked_update(value) } else { - guard.inner = Some(Cache::new(value.into())) - } + guard.inner = Some(Cache::new(value)) + }; + Ok(()) } pub(crate) async fn get(&self) -> Result<RwLockReadGuard<'_, Cache<T>>, UninitialisedCache> { @@ -96,6 +144,19 @@ impl<T> From<Cache<T>> for CachedItem<T> { } } +// specialised variant of `Cache` for holding maps of values that allow updates to individual entries + +/* + pub(crate) fn partial_update<F>(&mut self, partial_value: impl Into<S>, update_fn: F) + where + F: FnOnce(&mut T, S), + { + update_fn(&mut self.value, partial_value.into()); + self.as_at = OffsetDateTime::now_utc() + } + +*/ + // don't use this directly! // opt for SharedCache<T> instead pub struct Cache<T> { @@ -104,6 +165,7 @@ pub struct Cache<T> { } impl<T> Cache<Option<T>> { + #[allow(dead_code)] pub(crate) fn transpose(self) -> Option<Cache<T>> { self.value.map(|value| Cache { value, @@ -133,6 +195,16 @@ impl<T> Cache<T> { } } + pub(crate) fn as_mapped<F, U>(this: &Self, f: F) -> Cache<U> + where + F: Fn(&T) -> U, + { + Cache { + value: f(&this.value), + as_at: this.as_at, + } + } + // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn clone_cache(&self) -> Self where @@ -144,6 +216,11 @@ impl<T> Cache<T> { } } + pub(crate) fn update<S>(&mut self, update: S, update_fn: impl Fn(&mut T, S)) { + update_fn(&mut self.value, update); + self.as_at = OffsetDateTime::now_utc(); + } + // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn unchecked_update(&mut self, value: impl Into<T>) { self.value = value.into(); diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index bee1e49f10..27d8480c03 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -4,61 +4,74 @@ use crate::support::caching::cache::SharedCache; use crate::support::caching::CacheNotification; use async_trait::async_trait; -use nym_task::TaskClient; +use nym_task::ShutdownToken; +use std::sync::Arc; use std::time::Duration; -use tokio::sync::watch; +use tokio::sync::{watch, Notify}; use tokio::time::interval; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; -pub struct CacheRefresher<T, E> { +pub(crate) type CacheUpdateWatcher = watch::Receiver<CacheNotification>; + +#[derive(Clone)] +pub struct RefreshRequester(Arc<Notify>); + +impl RefreshRequester { + pub(crate) fn request_cache_refresh(&self) { + self.0.notify_waiters() + } +} + +impl Default for RefreshRequester { + fn default() -> Self { + RefreshRequester(Arc::new(Notify::new())) + } +} + +/// Explanation on generics: +/// the internal SharedCache<T> can be updated in two ways +/// by default CacheItemProvider will just provide a T and the internal values will be swapped +/// however, an alternative is to make it provide another value of type S with an explicit update closure +/// this way the cache will be updated with a custom method mutating the existing value +/// the reason for this is to allow partial updates of maps, where we might not want to retrieve +/// the entire value, and we might want to just insert a new entry +pub struct CacheRefresher<T, E, S = T> { name: String, refreshing_interval: Duration, refresh_notification_sender: watch::Sender<CacheNotification>, + // it's not really THAT complex... it's just a boxed function + #[allow(clippy::type_complexity)] + update_fn: Option<Box<dyn Fn(&mut T, S) + Send + Sync>>, + // TODO: the Send + Sync bounds are only required for the `start` method. could we maybe make it less restrictive? - provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, shared_cache: SharedCache<T>, - // triggers: Vec<Box<dyn RefreshTriggerTrait>>, + refresh_requester: RefreshRequester, } #[async_trait] -pub trait CacheItemProvider { +pub(crate) trait CacheItemProvider { type Item; type Error: std::error::Error; async fn wait_until_ready(&self) {} - async fn try_refresh(&self) -> Result<Self::Item, Self::Error>; + async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error>; } -// pub struct TriggerFailure; -// -// #[async_trait] -// pub trait RefreshTriggerTrait { -// async fn triggerred(&mut self) -> Result<(), TriggerFailure>; -// } -// -// // TODO: how to get rid of `T: Send + Sync`? it really doesn't need to be Send + Sync -// // since it's wrapped in Shared<T> internally anyway -// #[async_trait] -// impl<T> RefreshTriggerTrait for watch::Receiver<T> -// where -// T: Send + Sync, -// { -// async fn triggerred(&mut self) -> Result<(), TriggerFailure> { -// self.changed().await.map_err(|err| { -// error!("failed to process refresh trigger: {err}"); -// TriggerFailure -// }) -// } -// } - -impl<T, E> CacheRefresher<T, E> +// Generics explanation: +// T: the actual type held in the cache +// E: Error type associated with refresh failure +// S: data type retrieved during update operation. it must be convertible into T +// (so that initial state could be established or when no `custom_fn` is set) +impl<T, E, S> CacheRefresher<T, E, S> where E: std::error::Error, + S: Into<T>, { - pub(crate) fn new( - item_provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + pub(crate) fn new_boxed( + item_provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, refreshing_interval: Duration, ) -> Self { let (refresh_notification_sender, _) = watch::channel(CacheNotification::Start); @@ -67,13 +80,22 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache: SharedCache::new(), + refresh_requester: Default::default(), } } + pub(crate) fn new<P>(item_provider: P, refreshing_interval: Duration) -> Self + where + P: CacheItemProvider<Error = E, Item = S> + Send + Sync + 'static, + { + Self::new_boxed(Box::new(item_provider), refreshing_interval) + } + pub(crate) fn new_with_initial_value( - item_provider: Box<dyn CacheItemProvider<Error = E, Item = T> + Send + Sync>, + item_provider: Box<dyn CacheItemProvider<Error = E, Item = S> + Send + Sync>, refreshing_interval: Duration, shared_cache: SharedCache<T>, ) -> Self { @@ -83,89 +105,195 @@ where name: "GenericCacheRefresher".to_string(), refreshing_interval, refresh_notification_sender, + update_fn: None, provider: item_provider, shared_cache, + refresh_requester: Default::default(), } } + /// Rather than performing default behaviour of overwriting all existing values in the cache, + /// provide a custom update function that will define the update behaviour. + #[must_use] + pub(crate) fn with_update_fn( + mut self, + update_fn: impl Fn(&mut T, S) + Send + Sync + 'static, + ) -> Self { + self.update_fn = Some(Box::new(update_fn)); + self + } + #[must_use] pub(crate) fn named(mut self, name: impl Into<String>) -> Self { self.name = name.into(); self } - pub(crate) fn update_watcher(&self) -> watch::Receiver<CacheNotification> { + pub(crate) fn update_watcher(&self) -> CacheUpdateWatcher { self.refresh_notification_sender.subscribe() } + pub(crate) fn refresh_requester(&self) -> RefreshRequester { + self.refresh_requester.clone() + } + #[allow(dead_code)] pub(crate) fn get_shared_cache(&self) -> SharedCache<T> { self.shared_cache.clone() } - // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly - // or via `FnMut(&mut T)` closure - async fn do_refresh_cache(&self) { - match self.provider.try_refresh().await { - Ok(updated_items) => { - self.shared_cache.update(updated_items).await; - if !self.refresh_notification_sender.is_closed() - && self - .refresh_notification_sender - .send(CacheNotification::Updated) - .is_err() - { - warn!("failed to send cache update notification"); + async fn update_cache(&self, mut update: S, update_fn: impl Fn(&mut T, S)) { + let mut failures = 0; + + loop { + match self + .shared_cache + .try_update_value(update, &update_fn, &self.name) + .await + { + Ok(_) => break, + Err(returned) => { + failures += 1; + update = returned } + }; + if failures % 10 == 0 { + warn!( + "failed to obtain write permit for {} cache {failures} times in a row!", + self.name + ); } - Err(err) => { - error!("{}: failed to refresh the cache: {err}", self.name) - } + + tokio::time::sleep(Duration::from_secs_f32(0.5)).await } } - pub async fn refresh(&self, task_client: &mut TaskClient) { + async fn overwrite_cache(&self, mut updated_items: T) { + let mut failures = 0; + + loop { + match self + .shared_cache + .try_overwrite_old_value(updated_items, &self.name) + .await + { + Ok(_) => break, + Err(returned) => { + failures += 1; + updated_items = returned + } + }; + if failures % 10 == 0 { + warn!( + "failed to obtain write permit for {} cache {failures} times in a row!", + self.name + ); + } + + tokio::time::sleep(Duration::from_secs_f32(0.5)).await + } + } + + async fn do_refresh_cache(&mut self) { + let updated_items = match self.provider.try_refresh().await { + Err(err) => { + error!("{}: failed to refresh the cache: {err}", self.name); + return; + } + Ok(Some(items)) => items, + Ok(None) => { + debug!("no updates for {} cache this iteration", self.name); + return; + } + }; + + if let Some(update_fn) = self.update_fn.as_ref() { + self.update_cache(updated_items, update_fn).await; + } else { + self.overwrite_cache(updated_items.into()).await; + } + + if !self.refresh_notification_sender.is_closed() + && self + .refresh_notification_sender + .send(CacheNotification::Updated) + .is_err() + { + warn!("failed to send cache update notification"); + } + } + + pub async fn refresh(&mut self, shutdown_token: &ShutdownToken) { info!("{}: refreshing cache state", self.name); tokio::select! { biased; - _ = task_client.recv() => { + _ = shutdown_token.cancelled() => { trace!("{}: Received shutdown while refreshing cache", self.name) } _ = self.do_refresh_cache() => (), } } - pub async fn run(&self, mut task_client: TaskClient) { + pub async fn run(&mut self, shutdown_token: ShutdownToken) { self.provider.wait_until_ready().await; let mut refresh_interval = interval(self.refreshing_interval); - while !task_client.is_shutdown() { + while !shutdown_token.is_cancelled() { tokio::select! { biased; - _ = task_client.recv() => { + _ = shutdown_token.cancelled() => { trace!("{}: Received shutdown", self.name) } - _ = refresh_interval.tick() => self.refresh(&mut task_client).await, + _ = refresh_interval.tick() => self.refresh(&shutdown_token).await, + // note: `Notify` is not cancellation safe, HOWEVER, there's only one listener, + // so it doesn't matter if we lose our queue position + _ = self.refresh_requester.0.notified() => { + self.refresh(&shutdown_token).await; + // since we just performed the full request, we can reset our existing interval + refresh_interval.reset(); + } } } } - pub fn start(self, task_client: TaskClient) + pub fn start(mut self, shutdown_token: ShutdownToken) where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, { - tokio::spawn(async move { self.run(task_client).await }); + tokio::spawn(async move { self.run(shutdown_token).await }); } - pub fn start_with_watcher(self, task_client: TaskClient) -> watch::Receiver<CacheNotification> + pub fn start_with_delay(mut self, shutdown_token: ShutdownToken, delay: Duration) where T: Send + Sync + 'static, E: Send + Sync + 'static, + S: Send + Sync + 'static, + { + tokio::spawn(async move { + let sleep = tokio::time::sleep(delay); + tokio::select! { + biased; + _ = shutdown_token.cancelled() => { + trace!("{}: Received shutdown", self.name); + return + } + _ = sleep => {}, + } + self.run(shutdown_token).await + }); + } + + pub fn start_with_watcher(self, shutdown_token: ShutdownToken) -> CacheUpdateWatcher + where + T: Send + Sync + 'static, + E: Send + Sync + 'static, + S: Send + Sync + 'static, { let receiver = self.update_watcher(); - self.start(task_client); + self.start(shutdown_token); receiver } } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 46300cec50..0ae75fe23e 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::client::Client; use crate::ecash::comm::QueryCommunicationChannel; use crate::ecash::dkg::controller::keys::{ @@ -10,36 +9,42 @@ use crate::ecash::dkg::controller::keys::{ use crate::ecash::dkg::controller::DkgController; use crate::ecash::state::EcashState; use crate::epoch_operations::EpochAdvancer; +use crate::key_rotation::KeyRotationController; +use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::network::models::NetworkDetails; -use crate::node_describe_cache::DescribedNodes; +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_performance::provider::contract_provider::ContractPerformanceProvider; +use crate::node_performance::provider::legacy_storage_provider::LegacyStoragePerformanceProvider; +use crate::node_performance::provider::NodePerformanceProvider; use crate::node_status_api::handlers::unstable; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; use crate::status::{ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL}; -use crate::support::http::state::{ - AppState, ChainStatusCache, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S, -}; -use crate::support::http::RouterBuilder; +use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; +use crate::support::http::state::force_refresh::ForcedRefresh; +use crate::support::http::state::AppState; +use crate::support::http::{RouterBuilder, TASK_MANAGER_TIMEOUT_S}; use crate::support::nyxd; use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::migrate_to_directory_services_v2_1; use crate::support::storage::NymApiStorage; +use crate::unstable_routes::v1::account::cache::AddressInfoCache; use crate::{ - circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, - node_status_api, nym_contract_cache, + ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache, + node_performance, node_status_api, signers_cache, }; use anyhow::{bail, Context}; use nym_config::defaults::NymNetworkDetails; use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; +use nym_task::ShutdownManager; use nym_validator_client::nyxd::Coin; use std::net::SocketAddr; use std::sync::Arc; -use tokio_util::sync::CancellationToken; -use tracing::{error, info}; +use std::time::Duration; +use tracing::info; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -101,12 +106,22 @@ pub(crate) struct Args { #[clap(long)] pub(crate) bind_address: Option<SocketAddr>, + /// account/address cache TTL: should be lower than epoch length (1 hour) + /// because, at worst, data will be stale for <epoch_length> + <cache_ttl> seconds + #[clap(long, env = "ADDRESS_CACHE_REFRESH_INTERVAL_S")] + pub(crate) address_cache_ttl_seconds: Option<u64>, + + /// number of addresses that are cached on account/address endpoint + #[clap(long, env = "ADDRESS_CACHE_CAPACITY")] + pub(crate) address_cache_capacity: Option<u64>, + #[clap(hide = true, long, default_value_t = false)] pub(crate) allow_illegal_ips: bool, } -async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHandles> { - let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); +async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager> { + let shutdown_manager = ShutdownManager::new("nym-api") + .with_shutdown_duration(Duration::from_secs(TASK_MANAGER_TIMEOUT_S)); let nyxd_client = nyxd::Client::new(config)?; let connected_nyxd = config.get_nyxd_url(); @@ -135,13 +150,20 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - let nym_contract_cache_state = NymContractCache::new(); + let mixnet_contract_cache_state = MixnetContractCache::new(); let node_status_cache_state = NodeStatusCache::new(); let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); let described_nodes_cache = SharedCache::<DescribedNodes>::new(); let node_info_cache = unstable::NodeInfoCache::default(); + let mixnet_contract_cache_refresher = mixnet_contract_cache::build_refresher( + &config.mixnet_contract_cache, + &mixnet_contract_cache_state.clone(), + nyxd_client.clone(), + ); + let mixnet_contract_cache_refresh_requester = + mixnet_contract_cache_refresher.refresh_requester(); + let ecash_contract = nyxd_client .get_ecash_contract_address() .await @@ -158,7 +180,7 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan ecash_keypair_wrapper.clone(), comm_channel, storage.clone(), - task_manager.subscribe_named("ecash-state-data-cleaner"), + &shutdown_manager, ); // if ecash signer is enabled, there are additional constraints on the nym-api, @@ -189,20 +211,34 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan None }; + // check if signers cache is enabled, and if so, start the refresher + let ecash_signers_cache = if config.signers_cache.enabled { + signers_cache::start_refresher( + &config.signers_cache, + nyxd_client.clone(), + &shutdown_manager, + ) + } else { + SharedCache::new() + }; + ecash_state.spawn_background_cleaner(); let router = router.with_state(AppState { nyxd_client: nyxd_client.clone(), chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL), - forced_refresh: ForcedRefresh::new( - config.topology_cacher.debug.node_describe_allow_illegal_ips, + ecash_signers_cache, + address_info_cache: AddressInfoCache::new( + config.address_cache.time_to_live, + config.address_cache.capacity, ), - nym_contract_cache: nym_contract_cache_state.clone(), + forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips), + mixnet_contract_cache: mixnet_contract_cache_state.clone(), node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), storage: storage.clone(), described_nodes_cache: described_nodes_cache.clone(), - network_details, + network_details: network_details.clone(), node_info_cache, + contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live), api_status: ApiStatusState::new(signer_information), ecash_state: Arc::new(ecash_state), }); @@ -211,36 +247,60 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan // we should be doing the below, but can't due to our current startup structure // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); // let cache = refresher.get_shared_cache(); - let describe_cache_watcher = node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), + let describe_cache_refresher = node_describe_cache::provider::new_provider_with_initial_value( + &config.describe_cache, + mixnet_contract_cache_state.clone(), described_nodes_cache.clone(), ) - .named("node-self-described-data-refresher") - .start_with_watcher(task_manager.subscribe_named("node-self-described-data-refresher")); + .named("node-self-described-data-refresher"); + + let describe_cache_refresh_requester = describe_cache_refresher.refresh_requester(); + + let describe_cache_watcher = describe_cache_refresher + .start_with_watcher(shutdown_manager.clone_token("node-self-described-data-refresher")); + + let performance_provider = if config.performance_provider.use_performance_contract_data { + if network_details + .network + .contracts + .performance_contract_address + .is_none() + { + bail!("can't use performance contract data without setting the address of the contract") + } + + let performance_contract_cache = node_performance::contract_cache::start_cache_refresher( + &config.performance_provider, + nyxd_client.clone(), + mixnet_contract_cache_state.clone(), + &shutdown_manager, + ) + .await?; + let provider = ContractPerformanceProvider::new( + &config.performance_provider, + performance_contract_cache, + ); + Box::new(provider) as Box<dyn NodePerformanceProvider + Send + Sync> + } else { + Box::new(LegacyStoragePerformanceProvider::new( + storage.clone(), + mixnet_contract_cache_state.clone(), + )) + }; // start all the caches first - let contract_cache_watcher = nym_contract_cache::start_refresher( - &config.node_status_api, - &nym_contract_cache_state, - nyxd_client.clone(), - &task_manager, - ); + let contract_cache_watcher = mixnet_contract_cache_refresher + .start_with_watcher(shutdown_manager.clone_token("contracts-data-refresher")); + node_status_api::start_cache_refresh( &config.node_status_api, - &nym_contract_cache_state, + &mixnet_contract_cache_state, &described_nodes_cache, &node_status_cache_state, - storage.clone(), - contract_cache_watcher, + performance_provider, + contract_cache_watcher.clone(), describe_cache_watcher, - &task_manager, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, + &shutdown_manager, ); // start dkg task @@ -254,56 +314,67 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan dkg_bte_keypair, identity_public_key, rand::rngs::OsRng, - &task_manager, + &shutdown_manager, )?; } + let has_performance_data = + config.network_monitor.enabled || config.performance_provider.use_performance_contract_data; + // and then only start the uptime updater (and the monitor itself, duh) // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::<SphinxMessageReceiver>( config, - &nym_contract_cache_state, + &mixnet_contract_cache_state, described_nodes_cache.clone(), node_status_cache_state.clone(), &storage, nyxd_client.clone(), - &task_manager, + &shutdown_manager, ) .await; - HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - EpochAdvancer::start( - nyxd_client, - &nym_contract_cache_state, - &node_status_cache_state, - described_nodes_cache.clone(), - &storage, - &task_manager, - ); - } + HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown_manager); } + // start 'rewarding' if its enabled and there exists source for performance data + if config.rewarding.enabled && has_performance_data { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &mixnet_contract_cache_state, + mixnet_contract_cache_refresh_requester, + &node_status_cache_state, + described_nodes_cache.clone(), + &storage, + &shutdown_manager, + ); + } + + // finally start a background task watching the contract changes and requesting + // self-described cache refresh upon being close to key rotation rollover + KeyRotationController::new( + describe_cache_refresh_requester, + contract_cache_watcher, + mixnet_contract_cache_state, + ) + .start(shutdown_manager.clone_token("KeyRotationController")); + let bind_address = config.base.bind_address.to_owned(); let server = router.build_server(&bind_address).await?; - let cancellation_token = CancellationToken::new(); - let shutdown_button = cancellation_token.clone(); - let axum_shutdown_receiver = cancellation_token.cancelled_owned(); - let server_handle = tokio::spawn(async move { + let http_shutdown = shutdown_manager.clone_token("axum-http"); + tokio::spawn(async move { { info!("Started Axum HTTP V2 server on {bind_address}"); - server.run(axum_shutdown_receiver).await + server.run(http_shutdown).await } }); - let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); + shutdown_manager.close(); - Ok(shutdown) + Ok(shutdown_manager) } pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { @@ -314,32 +385,8 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - let mut axum_shutdown = start_nym_api_tasks_axum(&config).await?; - - // it doesn't matter which server catches the interrupt: it needs only be caught once - if let Err(err) = axum_shutdown.task_manager_mut().catch_interrupt().await { - error!("Error stopping axum tasks: {err}"); - } - - info!("Stopping nym API"); - - axum_shutdown.task_manager_mut().signal_shutdown().ok(); - axum_shutdown.task_manager_mut().wait_for_shutdown().await; - - let running_server = axum_shutdown.shutdown_axum(); - - match running_server.await { - Ok(Ok(_)) => { - info!("Axum HTTP server shut down without errors"); - } - Ok(Err(err)) => { - error!("Axum HTTP server terminated with: {err}"); - anyhow::bail!(err) - } - Err(err) => { - error!("Server task panicked: {err}"); - } - }; + let shutdown_manager = start_nym_api_tasks(&config).await?; + shutdown_manager.run_until_shutdown().await; Ok(()) } diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index f1d54d054b..612de6d815 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -50,15 +50,29 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000; const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; -const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30); -const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120); -const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600); +const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305); +const DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL: Duration = Duration::from_secs(150); +const DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS: u32 = 12; +const DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS: usize = 25; + +pub(crate) const DEFAULT_ADDRESS_CACHE_TTL: Duration = Duration::from_secs(60 * 15); +pub(crate) const DEFAULT_ADDRESS_CACHE_CAPACITY: u64 = 1000; pub(crate) const DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL: Duration = Duration::from_secs(4500); pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50; // TODO: make it configurable -pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(60); +pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(30); +pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60); + +// contract info is changed very infrequently (essentially once per release cycle) +// so this default is more than enough +pub(crate) const DEFAULT_CONTRACT_DETAILS_CACHE_TTL: Duration = Duration::from_secs(60 * 60); + +pub(crate) const DEFAULT_NODE_SIGNERS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(600); +pub(crate) const DEFAULT_NODE_SIGNERS_CACHE_REFRESHER_START_DELAY: Duration = + Duration::from_secs(30); const DEFAULT_MONITOR_THRESHOLD: u8 = 60; const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50; @@ -98,19 +112,33 @@ pub struct Config { pub base: Base, + #[serde(default)] + pub performance_provider: PerformanceProvider, + // TODO: perhaps introduce separate 'path finder' field for all the paths and directories like we have with other configs pub network_monitor: NetworkMonitor, + #[serde(default)] + pub mixnet_contract_cache: MixnetContractCache, + pub node_status_api: NodeStatusAPI, - pub topology_cacher: TopologyCacher, + #[serde(alias = "topology_cacher")] + pub describe_cache: DescribeCache, - pub circulating_supply_cacher: CirculatingSupplyCacher, + #[serde(default)] + pub contracts_info_cache: ContractsInfoCache, pub rewarding: Rewarding, + #[serde(default)] + pub signers_cache: SignersCache, + #[serde(alias = "coconut_signer")] pub ecash_signer: EcashSigner, + + #[serde(skip)] + pub address_cache: AddressCacheConfig, } impl NymConfigTemplate for Config { @@ -124,12 +152,16 @@ impl Config { Config { save_path: None, base: Base::new_default(id.as_ref()), + performance_provider: Default::default(), network_monitor: NetworkMonitor::new_default(id.as_ref()), + mixnet_contract_cache: Default::default(), node_status_api: NodeStatusAPI::new_default(id.as_ref()), - topology_cacher: Default::default(), - circulating_supply_cacher: Default::default(), + describe_cache: Default::default(), + contracts_info_cache: Default::default(), rewarding: Default::default(), + signers_cache: Default::default(), ecash_signer: EcashSigner::new_default(id.as_ref()), + address_cache: Default::default(), } } @@ -177,7 +209,13 @@ impl Config { self.base.bind_address = http_bind_address } if args.allow_illegal_ips { - self.topology_cacher.debug.node_describe_allow_illegal_ips = true + self.describe_cache.debug.allow_illegal_ips = true + } + if let Some(address_cache_ttl) = args.address_cache_ttl { + self.address_cache.time_to_live = address_cache_ttl; + } + if let Some(address_cache_capacity) = args.address_cache_capacity { + self.address_cache.capacity = address_cache_capacity; } self @@ -292,6 +330,151 @@ impl Base { } } +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct ContractsInfoCache { + pub time_to_live: Duration, +} + +impl Default for ContractsInfoCache { + fn default() -> Self { + ContractsInfoCache { + time_to_live: DEFAULT_CONTRACT_DETAILS_CACHE_TTL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct MixnetContractCache { + #[serde(default)] + pub debug: MixnetContractCacheDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for MixnetContractCache { + fn default() -> Self { + MixnetContractCache { + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default)] +pub struct MixnetContractCacheDebug { + #[serde(with = "humantime_serde")] + pub caching_interval: Duration, +} + +impl Default for MixnetContractCacheDebug { + fn default() -> Self { + MixnetContractCacheDebug { + caching_interval: DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProvider { + /// Specifies whether this nym-api should attempt to retrieve node performance + /// information from the performance contract. + pub use_performance_contract_data: bool, + + pub debug: PerformanceProviderDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProvider { + fn default() -> Self { + PerformanceProvider { + // to be changed later + use_performance_contract_data: false, + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PerformanceProviderDebug { + /// Specifies interval of polling the performance contract. Note it is only applicable + /// if the contract data is being used. + /// Further note that if there have been no updates to the cache, the performance overhead is negligible + /// (i.e. there will be only a single query performed to check if anything has changed) + #[serde(with = "humantime_serde")] + pub contract_polling_interval: Duration, + + /// Specify the maximum number of epochs we can fallback to if given epoch's performance data + /// is not available in the contract + pub max_performance_fallback_epochs: u32, + + /// Specify the maximum number of epoch entries to be kept in the cache in case we needed non-current data + // (currently we need an equivalent of full day worth of data for legacy endpoints) + pub max_epoch_entries_to_retain: usize, +} + +#[allow(clippy::derivable_impls)] +impl Default for PerformanceProviderDebug { + fn default() -> Self { + PerformanceProviderDebug { + contract_polling_interval: DEFAULT_PERFORMANCE_CONTRACT_POLLING_INTERVAL, + max_performance_fallback_epochs: DEFAULT_PERFORMANCE_CONTRACT_FALLBACK_EPOCHS, + max_epoch_entries_to_retain: DEFAULT_PERFORMANCE_CONTRACT_RETAINED_EPOCHS, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct SignersCache { + pub enabled: bool, + + pub debug: SignersCacheDebug, +} + +impl Default for SignersCache { + fn default() -> Self { + SignersCache { + enabled: true, + debug: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct SignersCacheDebug { + // TODO: make it into a decaying function so that if multiple signers are down, + // the refresh interval would decrease + #[serde(with = "humantime_serde")] + pub refresh_interval: Duration, + + // give it some time so that the actual api on THIS singer could start + // and it wouldn't self-report itself as being down + #[serde(with = "humantime_serde")] + pub refresher_start_delay: Duration, +} + +impl Default for SignersCacheDebug { + fn default() -> Self { + SignersCacheDebug { + refresh_interval: DEFAULT_NODE_SIGNERS_CACHE_REFRESH_INTERVAL, + refresher_start_delay: DEFAULT_NODE_SIGNERS_CACHE_REFRESHER_START_DELAY, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct AddressCacheConfig { + pub time_to_live: Duration, + pub capacity: u64, +} + +impl Default for AddressCacheConfig { + fn default() -> Self { + Self { + time_to_live: DEFAULT_ADDRESS_CACHE_TTL, + capacity: DEFAULT_ADDRESS_CACHE_CAPACITY, + } + } +} + // this got separated into 2 structs so that we could have a sane `default` implementation for the latter #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct NetworkMonitor { @@ -419,76 +602,41 @@ pub struct NodeStatusAPIDebug { impl Default for NodeStatusAPIDebug { fn default() -> Self { NodeStatusAPIDebug { - caching_interval: DEFAULT_NODE_STATUS_CACHE_INTERVAL, + caching_interval: DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL, } } } #[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacher { +pub struct DescribeCache { // pub enabled: bool, // pub paths: TopologyCacherPathfinder, #[serde(default)] - pub debug: TopologyCacherDebug, + pub debug: DescribeCacheDebug, } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct TopologyCacherDebug { +pub struct DescribeCacheDebug { #[serde(with = "humantime_serde")] + #[serde(alias = "node_describe_caching_interval")] pub caching_interval: Duration, - #[serde(with = "humantime_serde")] - pub node_describe_caching_interval: Duration, + #[serde(alias = "node_describe_batch_size")] + pub batch_size: usize, - pub node_describe_batch_size: usize, - - pub node_describe_allow_illegal_ips: bool, + #[serde(alias = "node_describe_allow_illegal_ips")] + pub allow_illegal_ips: bool, } -impl Default for TopologyCacherDebug { +impl Default for DescribeCacheDebug { fn default() -> Self { - TopologyCacherDebug { - caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, - node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, - node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, - node_describe_allow_illegal_ips: false, - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacher { - pub enabled: bool, - - // pub paths: CirculatingSupplyCacherPathfinder, - #[serde(default)] - pub debug: CirculatingSupplyCacherDebug, -} - -impl Default for CirculatingSupplyCacher { - fn default() -> Self { - CirculatingSupplyCacher { - enabled: true, - debug: CirculatingSupplyCacherDebug::default(), - } - } -} - -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -#[serde(default)] -pub struct CirculatingSupplyCacherDebug { - #[serde(with = "humantime_serde")] - pub caching_interval: Duration, -} - -impl Default for CirculatingSupplyCacherDebug { - fn default() -> Self { - CirculatingSupplyCacherDebug { - caching_interval: DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL, + DescribeCacheDebug { + caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + allow_illegal_ips: false, } } } diff --git a/nym-api/src/support/config/override.rs b/nym-api/src/support/config/override.rs index a2f6d9c76c..4f8b96db31 100644 --- a/nym-api/src/support/config/override.rs +++ b/nym-api/src/support/config/override.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::cli::{init, run}; -use std::net::SocketAddr; +use std::{net::SocketAddr, time::Duration}; // Configuration that can be overridden. pub(crate) struct OverrideConfig { @@ -32,6 +32,9 @@ pub(crate) struct OverrideConfig { /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` pub(crate) bind_address: Option<SocketAddr>, + pub(crate) address_cache_ttl: Option<Duration>, + pub(crate) address_cache_capacity: Option<u64>, + pub(crate) allow_illegal_ips: bool, } @@ -46,6 +49,9 @@ impl From<init::Args> for OverrideConfig { announce_address: args.announce_address, monitor_credentials_mode: Some(args.monitor_credentials_mode), bind_address: args.bind_address, + // irrelevant for --init command because we set the value in --run + address_cache_ttl: None, + address_cache_capacity: None, allow_illegal_ips: args.allow_illegal_ips, } } @@ -62,6 +68,8 @@ impl From<run::Args> for OverrideConfig { announce_address: args.announce_address, monitor_credentials_mode: args.monitor_credentials_mode, bind_address: args.bind_address, + address_cache_ttl: args.address_cache_ttl_seconds.map(Duration::from_secs), + address_cache_capacity: args.address_cache_capacity, allow_illegal_ips: args.allow_illegal_ips, } } diff --git a/nym-api/src/support/http/helpers.rs b/nym-api/src/support/http/helpers.rs index 92f6d5585a..ed11746900 100644 --- a/nym-api/src/support/http/helpers.rs +++ b/nym-api/src/support/http/helpers.rs @@ -1,14 +1,15 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use nym_http_api_common::Output; use nym_mixnet_contract_common::NodeId; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; -#[derive(Serialize, Deserialize, Debug, JsonSchema, ToSchema, IntoParams)] +#[derive(Serialize, Deserialize, Debug, ToSchema, IntoParams)] #[into_params(parameter_in = Query)] pub struct PaginationRequest { + pub output: Option<Output>, pub page: Option<u32>, pub per_page: Option<u32>, } diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 9446891443..db73c64a3c 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -5,116 +5,7 @@ pub(crate) mod helpers; pub(crate) mod openapi; pub(crate) mod router; pub(crate) mod state; -mod unstable_routes; pub(crate) use router::RouterBuilder; -// pub(crate) async fn setup_rocket( -// config: &Config, -// network_details: NetworkDetails, -// nyxd_client: nyxd::Client, -// identity_keypair: identity::KeyPair, -// coconut_keypair: ecash::keys::KeyPair, -// storage: NymApiStorage, -// ) -> anyhow::Result<Rocket<Ignite>> { -// let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); -// let mut rocket = rocket::build(); -// -// let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); -// -// mount_endpoints_and_merged_docs! { -// rocket, -// "/v1".to_owned(), -// openapi_settings, -// "/" => (vec![], openapi::custom_openapi_spec()), -// "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), -// "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), -// "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), -// "/network" => network_routes(&openapi_settings), -// "/api-status" => api_status_routes(&openapi_settings), -// "/ecash" => ecash::routes_open_api(&openapi_settings, config.ecash_signer.enabled), -// "" => nym_node_routes_deprecated(&openapi_settings), -// -// // => when we move those routes, we'll need to add a redirection for backwards compatibility -// "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) -// } -// -// let rocket = rocket -// .manage(network_details) -// .manage(SharedCache::<DescribedNodes>::new()) -// .mount("/swagger", make_swagger_ui(&openapi::get_docs())) -// .attach(setup_rocket_cors()?) -// .attach(NymContractCache::stage()) -// .attach(NodeStatusCache::stage()) -// .attach(CirculatingSupplyCache::stage(mix_denom.clone())) -// .manage(unstable::NodeInfoCache::default()) -// .manage(storage.clone()); -// -// let mut status_state = ApiStatusState::new(); -// -// let rocket = if config.ecash_signer.enabled { -// // make sure we have some tokens to cover multisig fees -// let balance = nyxd_client.balance(&mix_denom).await?; -// if balance.amount < ecash::MINIMUM_BALANCE { -// let address = nyxd_client.address().await; -// let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); -// bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") -// } -// -// let cosmos_address = nyxd_client.address().await.to_string(); -// let announce_address = config -// .ecash_signer -// .announce_address -// .clone() -// .map(|u| u.to_string()) -// .unwrap_or_default(); -// status_state.add_zk_nym_signer(SignerState { -// cosmos_address, -// identity: identity_keypair.public_key().to_base58_string(), -// announce_address, -// ecash_keypair: coconut_keypair.clone(), -// }); -// -// let ecash_contract = nyxd_client -// .get_ecash_contract_address() -// .await -// .context("e-cash contract address is required to setup the zk-nym signer")?; -// -// let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); -// -// let ecash_state = EcashState::new( -// ecash_contract, -// nyxd_client.clone(), -// identity_keypair, -// coconut_keypair, -// comm_channel, -// storage.clone(), -// ) -// .await?; -// -// rocket.manage(ecash_state) -// } else { -// rocket -// }; -// -// Ok(rocket.manage(status_state).ignite().await?) -// } -// -// fn setup_rocket_cors() -> Result<Cors> { -// let allowed_origins = AllowedOrigins::all(); -// -// // You can also deserialize this -// let cors = rocket_cors::CorsOptions { -// allowed_origins, -// allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] -// .into_iter() -// .map(From::from) -// .collect(), -// allowed_headers: AllowedHeaders::all(), -// allow_credentials: true, -// ..Default::default() -// } -// .to_cors()?; -// -// Ok(cors) -// } +pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10; diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 4c291fbc58..1badfff81f 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -3,23 +3,24 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::ecash::api_routes::handlers::ecash_routes; +use crate::mixnet_contract_cache::handlers::nym_contract_cache_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::status_routes; -use crate::nym_contract_cache::handlers::nym_contract_cache_routes; use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; use crate::status; use crate::support::http::openapi::ApiDoc; use crate::support::http::state::AppState; -use crate::support::http::unstable_routes::unstable_routes; +use crate::unstable_routes::v1::unstable_routes_v1; +use crate::unstable_routes::v2::unstable_routes_v2; use anyhow::anyhow; use axum::response::Redirect; use axum::routing::get; use axum::Router; use core::net::SocketAddr; -use nym_http_api_common::middleware::logging::logger; +use nym_http_api_common::middleware::logging::log_request_info; +use nym_task::ShutdownToken; use tokio::net::TcpListener; -use tokio_util::sync::WaitForCancellationFutureOwned; use tower_http::cors::CorsLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -64,8 +65,9 @@ impl RouterBuilder { .nest("/api-status", status::handlers::api_status_routes()) .nest("/nym-nodes", nym_node_routes()) .nest("/ecash", ecash_routes()) - .nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes - ); + .nest("/unstable", unstable_routes_v1()), // CORS layer needs to be "outside" of routes + ) + .nest("/v2", Router::new().nest("/unstable", unstable_routes_v2())); Self { unfinished_router: default_routes, @@ -91,7 +93,7 @@ impl RouterBuilder { fn finalize_routes(self) -> Router<AppState> { self.unfinished_router .layer(setup_cors()) - .layer(axum::middleware::from_fn(logger)) + .layer(axum::middleware::from_fn(log_request_info)) } } @@ -129,14 +131,13 @@ pub(crate) struct ApiHttpServer { } impl ApiHttpServer { - pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> { - // into_make_service_with_connect_info allows us to see client ip address + pub async fn run(self, shutdown_token: ShutdownToken) -> Result<(), std::io::Error> { axum::serve( self.listener, self.router .into_make_service_with_connect_info::<SocketAddr>(), ) - .with_graceful_shutdown(receiver) + .with_graceful_shutdown(async move { shutdown_token.cancelled().await }) .await } } diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs deleted file mode 100644 index 224db98e53..0000000000 --- a/nym-api/src/support/http/state.rs +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::state::EcashState; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::handlers::unstable; -use crate::node_status_api::models::AxumErrorResponse; -use crate::node_status_api::NodeStatusCache; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::status::ApiStatusState; -use crate::support::caching::cache::SharedCache; -use crate::support::caching::Cache; -use crate::support::nyxd::Client; -use crate::support::storage; -use axum::extract::FromRef; -use nym_api_requests::models::{ - DetailedChainStatus, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, -}; -use nym_mixnet_contract_common::NodeId; -use nym_task::TaskManager; -use nym_topology::CachedEpochRewardedSet; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; -use time::OffsetDateTime; -use tokio::sync::{RwLock, RwLockReadGuard}; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10; - -/// Shutdown goes 2 directions: -/// 1. signal background tasks to gracefully finish -/// 2. signal server itself -/// -/// These are done through separate shutdown handles. Of course, shut down server -/// AFTER you have shut down BG tasks (or past their grace period). -pub(crate) struct ShutdownHandles { - task_manager: TaskManager, - axum_shutdown_button: ShutdownAxum, - /// Tokio JoinHandle for axum server's task - axum_join_handle: AxumJoinHandle, -} - -impl ShutdownHandles { - /// Cancellation token is given to Axum server constructor. When the token - /// receives a shutdown signal, Axum server will shut down gracefully. - pub(crate) fn new( - task_manager: TaskManager, - axum_server_handle: AxumJoinHandle, - shutdown_button: CancellationToken, - ) -> Self { - Self { - task_manager, - axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), - axum_join_handle: axum_server_handle, - } - } - - pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { - &mut self.task_manager - } - - /// Signal server to shut down, then return join handle to its - /// `tokio` task - /// - /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html - #[must_use] - pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { - self.axum_shutdown_button.0.cancel(); - self.axum_join_handle - } -} - -struct ShutdownAxum(CancellationToken); - -type AxumJoinHandle = JoinHandle<std::io::Result<()>>; - -#[derive(Clone)] -pub(crate) struct AppState { - // ideally this would have been made generic to make tests easier, - // however, it'd be a way bigger change (I tried) - pub(crate) nyxd_client: Client, - pub(crate) chain_status_cache: ChainStatusCache, - - pub(crate) forced_refresh: ForcedRefresh, - pub(crate) nym_contract_cache: NymContractCache, - pub(crate) node_status_cache: NodeStatusCache, - pub(crate) circulating_supply_cache: CirculatingSupplyCache, - pub(crate) storage: storage::NymApiStorage, - pub(crate) described_nodes_cache: SharedCache<DescribedNodes>, - pub(crate) network_details: NetworkDetails, - pub(crate) node_info_cache: unstable::NodeInfoCache, - pub(crate) api_status: ApiStatusState, - // todo: refactor it into inner: Arc<EcashStateInner> - pub(crate) ecash_state: Arc<EcashState>, -} - -impl FromRef<AppState> for ApiStatusState { - fn from_ref(app_state: &AppState) -> Self { - app_state.api_status.clone() - } -} - -impl FromRef<AppState> for Arc<EcashState> { - fn from_ref(app_state: &AppState) -> Self { - app_state.ecash_state.clone() - } -} - -#[derive(Clone)] -pub(crate) struct ForcedRefresh { - pub(crate) allow_all_ip_addresses: bool, - pub(crate) refreshes: Arc<RwLock<HashMap<NodeId, OffsetDateTime>>>, -} - -impl ForcedRefresh { - pub(crate) fn new(allow_all_ip_addresses: bool) -> ForcedRefresh { - ForcedRefresh { - allow_all_ip_addresses, - refreshes: Arc::new(Default::default()), - } - } - - pub(crate) async fn last_refreshed(&self, node_id: NodeId) -> Option<OffsetDateTime> { - self.refreshes.read().await.get(&node_id).copied() - } - - pub(crate) async fn set_last_refreshed(&self, node_id: NodeId) { - self.refreshes - .write() - .await - .insert(node_id, OffsetDateTime::now_utc()); - } -} - -#[derive(Clone)] -pub(crate) struct ChainStatusCache { - cache_ttl: Duration, - inner: Arc<RwLock<Option<ChainStatusCacheInner>>>, -} - -impl ChainStatusCache { - pub(crate) fn new(cache_ttl: Duration) -> Self { - ChainStatusCache { - cache_ttl, - inner: Arc::new(Default::default()), - } - } -} - -struct ChainStatusCacheInner { - last_refreshed_at: OffsetDateTime, - cache_value: DetailedChainStatus, -} - -impl ChainStatusCacheInner { - fn is_valid(&self, ttl: Duration) -> bool { - if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { - return true; - } - false - } -} - -impl ChainStatusCache { - pub(crate) async fn get_or_refresh( - &self, - client: &Client, - ) -> Result<DetailedChainStatus, AxumErrorResponse> { - if let Some(cached) = self.check_cache().await { - return Ok(cached); - } - - self.refresh(client).await - } - - async fn check_cache(&self) -> Option<DetailedChainStatus> { - let guard = self.inner.read().await; - let inner = guard.as_ref()?; - if inner.is_valid(self.cache_ttl) { - return Some(inner.cache_value.clone()); - } - None - } - - async fn refresh(&self, client: &Client) -> Result<DetailedChainStatus, AxumErrorResponse> { - // 1. attempt to get write lock permit - let mut guard = self.inner.write().await; - - // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit - if let Some(cached) = guard.as_ref() { - if cached.is_valid(self.cache_ttl) { - return Ok(cached.cache_value.clone()); - } - } - - // 3. attempt to query the chain for the chain data - let abci = client.abci_info().await?; - let block = client - .block_info(abci.last_block_height.value() as u32) - .await?; - - let status = DetailedChainStatus { - abci: abci.into(), - latest_block: block.into(), - }; - - *guard = Some(ChainStatusCacheInner { - last_refreshed_at: OffsetDateTime::now_utc(), - cache_value: status.clone(), - }); - - Ok(status) - } -} - -impl AppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache - } - - pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { - &self.node_status_cache - } - - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - - pub(crate) fn network_details(&self) -> &NetworkDetails { - &self.network_details - } - - pub(crate) fn described_nodes_cache(&self) -> &SharedCache<DescribedNodes> { - &self.described_nodes_cache - } - - pub(crate) fn storage(&self) -> &storage::NymApiStorage { - &self.storage - } - - pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { - &self.node_info_cache - } -} - -// handler helpers to easily get data or return error response -impl AppState { - pub(crate) async fn describe_nodes_cache_data( - &self, - ) -> Result<RwLockReadGuard<Cache<DescribedNodes>>, AxumErrorResponse> { - Ok(self.described_nodes_cache().get().await?) - } - - pub(crate) async fn rewarded_set( - &self, - ) -> Result<RwLockReadGuard<Cache<CachedEpochRewardedSet>>, AxumErrorResponse> { - self.nym_contract_cache() - .rewarded_set() - .await - .ok_or_else(AxumErrorResponse::internal) - } - - pub(crate) async fn node_annotations( - &self, - ) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>, AxumErrorResponse> { - self.node_status_cache() - .node_annotations() - .await - .ok_or_else(AxumErrorResponse::internal) - } - - pub(crate) async fn legacy_mixnode_annotations( - &self, - ) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, MixNodeBondAnnotated>>>, AxumErrorResponse> - { - self.node_status_cache() - .annotated_legacy_mixnodes() - .await - .ok_or_else(AxumErrorResponse::internal) - } - - pub(crate) async fn legacy_gateways_annotations( - &self, - ) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, GatewayBondAnnotated>>>, AxumErrorResponse> - { - self.node_status_cache() - .annotated_legacy_gateways() - .await - .ok_or_else(AxumErrorResponse::internal) - } -} diff --git a/nym-api/src/support/http/state/chain_status.rs b/nym-api/src/support/http/state/chain_status.rs new file mode 100644 index 0000000000..5dfd0130ac --- /dev/null +++ b/nym-api/src/support/http/state/chain_status.rs @@ -0,0 +1,91 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumErrorResponse; +use crate::support::nyxd::Client; +use nym_api_requests::models::DetailedChainStatus; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +#[derive(Clone)] +pub(crate) struct ChainStatusCache { + cache_ttl: Duration, + inner: Arc<RwLock<Option<ChainStatusCacheInner>>>, +} + +impl ChainStatusCache { + pub(crate) fn new(cache_ttl: Duration) -> Self { + ChainStatusCache { + cache_ttl, + inner: Arc::new(Default::default()), + } + } +} + +struct ChainStatusCacheInner { + last_refreshed_at: OffsetDateTime, + cache_value: DetailedChainStatus, +} + +impl ChainStatusCacheInner { + fn is_valid(&self, ttl: Duration) -> bool { + if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { + return true; + } + false + } +} + +impl ChainStatusCache { + pub(crate) async fn get_or_refresh( + &self, + client: &Client, + ) -> Result<DetailedChainStatus, AxumErrorResponse> { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option<DetailedChainStatus> { + let guard = self.inner.read().await; + let inner = guard.as_ref()?; + if inner.is_valid(self.cache_ttl) { + return Some(inner.cache_value.clone()); + } + None + } + + async fn refresh(&self, client: &Client) -> Result<DetailedChainStatus, AxumErrorResponse> { + // 1. attempt to get write lock permit + let mut guard = self.inner.write().await; + + // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit + if let Some(cached) = guard.as_ref() { + if cached.is_valid(self.cache_ttl) { + return Ok(cached.cache_value.clone()); + } + } + + // 3. attempt to query the chain for the chain data + let abci = client.abci_info().await?; + let block = client + .block_info(abci.last_block_height.value() as u32) + .await?; + + let status = DetailedChainStatus { + abci: abci.into(), + latest_block: block.into(), + }; + + *guard = Some(ChainStatusCacheInner { + last_refreshed_at: OffsetDateTime::now_utc(), + cache_value: status.clone(), + }); + + Ok(status) + } +} diff --git a/nym-api/src/support/http/state/contract_details.rs b/nym-api/src/support/http/state/contract_details.rs new file mode 100644 index 0000000000..5380a92809 --- /dev/null +++ b/nym-api/src/support/http/state/contract_details.rs @@ -0,0 +1,182 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumErrorResponse; +use crate::support::nyxd::Client; +use nym_contracts_common::ContractBuildInformation; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, NymContractsProvider, VestingQueryClient, +}; +use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::AccountId; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +type ContractAddress = String; + +pub type CachedContractsInfo = HashMap<ContractAddress, CachedContractInfo>; + +#[derive(Clone)] +pub struct CachedContractInfo { + pub(crate) address: Option<AccountId>, + pub(crate) base: Option<cw2::ContractVersion>, + pub(crate) detailed: Option<ContractBuildInformation>, +} + +impl CachedContractInfo { + pub fn new( + address: Option<&AccountId>, + base: Option<cw2::ContractVersion>, + detailed: Option<ContractBuildInformation>, + ) -> Self { + Self { + address: address.cloned(), + base, + detailed, + } + } +} + +#[derive(Clone)] +pub(crate) struct ContractDetailsCache { + cache_ttl: Duration, + inner: Arc<RwLock<ContractDetailsCacheInner>>, +} + +impl ContractDetailsCache { + pub(crate) fn new(cache_ttl: Duration) -> Self { + ContractDetailsCache { + cache_ttl, + inner: Arc::new(RwLock::new(ContractDetailsCacheInner::new())), + } + } +} + +struct ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime, + cache_value: CachedContractsInfo, +} + +impl ContractDetailsCacheInner { + pub(crate) fn new() -> Self { + ContractDetailsCacheInner { + last_refreshed_at: OffsetDateTime::UNIX_EPOCH, + cache_value: Default::default(), + } + } + + fn is_valid(&self, ttl: Duration) -> bool { + if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { + return true; + } + false + } + + async fn retrieve_nym_contracts_info( + &self, + nyxd_client: &Client, + ) -> Result<CachedContractsInfo, NyxdError> { + use crate::query_guard; + + let mut updated = HashMap::new(); + + let client_guard = nyxd_client.read().await; + + let mixnet = query_guard!(client_guard, mixnet_contract_address()); + let vesting = query_guard!(client_guard, vesting_contract_address()); + let coconut_dkg = query_guard!(client_guard, dkg_contract_address()); + let group = query_guard!(client_guard, group_contract_address()); + let multisig = query_guard!(client_guard, multisig_contract_address()); + let ecash = query_guard!(client_guard, ecash_contract_address()); + let performance = query_guard!(client_guard, performance_contract_address()); + + for (address, name) in [ + (mixnet, "nym-mixnet-contract"), + (vesting, "nym-vesting-contract"), + (coconut_dkg, "nym-coconut-dkg-contract"), + (group, "nym-cw4-group-contract"), + (multisig, "nym-cw3-multisig-contract"), + (ecash, "nym-ecash-contract"), + (performance, "nym-performance-contract"), + ] { + let (cw2, build_info) = if let Some(address) = address { + let cw2 = query_guard!(client_guard, try_get_cw2_contract_version(address).await); + let mut build_info = query_guard!( + client_guard, + try_get_contract_build_information(address).await + ); + + // for backwards compatibility until we migrate the contracts + if build_info.is_none() { + match name { + "nym-mixnet-contract" => { + build_info = Some(query_guard!( + client_guard, + get_mixnet_contract_version().await + )?) + } + "nym-vesting-contract" => { + build_info = Some(query_guard!( + client_guard, + get_vesting_contract_version().await + )?) + } + _ => (), + } + } + + (cw2, build_info) + } else { + (None, None) + }; + + updated.insert( + name.to_string(), + CachedContractInfo::new(address, cw2, build_info), + ); + } + + Ok(updated) + } +} + +impl ContractDetailsCache { + pub(crate) async fn get_or_refresh( + &self, + client: &Client, + ) -> Result<CachedContractsInfo, AxumErrorResponse> { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option<CachedContractsInfo> { + let guard = self.inner.read().await; + if guard.is_valid(self.cache_ttl) { + return Some(guard.cache_value.clone()); + } + None + } + + async fn refresh(&self, client: &Client) -> Result<CachedContractsInfo, AxumErrorResponse> { + // 1. attempt to get write lock permit + let mut guard = self.inner.write().await; + + // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit + if guard.is_valid(self.cache_ttl) { + return Ok(guard.cache_value.clone()); + } + + // 3. attempt to query the chain for the contracts data + let updated_values = guard.retrieve_nym_contracts_info(client).await?; + guard.last_refreshed_at = OffsetDateTime::now_utc(); + guard.cache_value = updated_values.clone(); + + Ok(updated_values) + } +} diff --git a/nym-api/src/support/http/state/force_refresh.rs b/nym-api/src/support/http/state/force_refresh.rs new file mode 100644 index 0000000000..667de00371 --- /dev/null +++ b/nym-api/src/support/http/state/force_refresh.rs @@ -0,0 +1,34 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use nym_mixnet_contract_common::NodeId; +use std::collections::HashMap; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +#[derive(Clone)] +pub(crate) struct ForcedRefresh { + pub(crate) allow_all_ip_addresses: bool, + pub(crate) refreshes: Arc<RwLock<HashMap<NodeId, OffsetDateTime>>>, +} + +impl ForcedRefresh { + pub(crate) fn new(allow_all_ip_addresses: bool) -> ForcedRefresh { + ForcedRefresh { + allow_all_ip_addresses, + refreshes: Arc::new(Default::default()), + } + } + + pub(crate) async fn last_refreshed(&self, node_id: NodeId) -> Option<OffsetDateTime> { + self.refreshes.read().await.get(&node_id).copied() + } + + pub(crate) async fn set_last_refreshed(&self, node_id: NodeId) { + self.refreshes + .write() + .await + .insert(node_id, OffsetDateTime::now_utc()); + } +} diff --git a/nym-api/src/support/http/state/mod.rs b/nym-api/src/support/http/state/mod.rs new file mode 100644 index 0000000000..bad1b7f4ed --- /dev/null +++ b/nym-api/src/support/http/state/mod.rs @@ -0,0 +1,229 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::ecash::state::EcashState; +use crate::mixnet_contract_cache::cache::MixnetContractCache; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::models::AxumErrorResponse; +use crate::node_status_api::NodeStatusCache; +use crate::signers_cache::cache::SignersCacheData; +use crate::status::ApiStatusState; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::Cache; +use crate::support::http::state::chain_status::ChainStatusCache; +use crate::support::http::state::contract_details::ContractDetailsCache; +use crate::support::http::state::force_refresh::ForcedRefresh; +use crate::support::nyxd::Client; +use crate::support::storage; +use crate::unstable_routes::v1::account::cache::AddressInfoCache; +use crate::unstable_routes::v1::account::models::NyxAccountDetails; +use axum::extract::FromRef; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::NodeId; +use nym_topology::CachedEpochRewardedSet; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLockReadGuard; + +pub(crate) mod chain_status; +pub(crate) mod contract_details; +pub(crate) mod force_refresh; + +#[derive(Clone)] +pub(crate) struct AppState { + // ideally this would have been made generic to make tests easier, + // however, it'd be a way bigger change (I tried) + /// Instance of a client used for interacting with the nyx chain. + pub(crate) nyxd_client: Client, + + /// Holds information about the latest chain block it has queried. + /// Note, it is not updated on every request. It follows the embedded ttl. + pub(crate) chain_status_cache: ChainStatusCache, + + /// Holds cached state of the statuses of all [ecash] signers on the network - + /// their perceived chain statuses and signing capabilities. + pub(crate) ecash_signers_cache: SharedCache<SignersCacheData>, + + /// Holds mapping between a nyx address and tokens/delegations it holds + pub(crate) address_info_cache: AddressInfoCache, + + /// Holds information on when nym-nodes requested an explicit request of their self-described data. + /// It is used to prevent DoS by nodes constantly requesting the refresh. + pub(crate) forced_refresh: ForcedRefresh, + + /// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval. + pub(crate) mixnet_contract_cache: MixnetContractCache, + + /// Holds processed information on network nodes, i.e. performance, config scores, etc. + // TODO: also perhaps redundant? + pub(crate) node_status_cache: NodeStatusCache, + + /// Holds reference to the persistent storage of this nym-api. + pub(crate) storage: storage::NymApiStorage, + + /// Holds information on the self-reported information of nodes, e.g. auxiliary keys they use, + /// ports they announce, etc. + pub(crate) described_nodes_cache: SharedCache<DescribedNodes>, + + /// Information about the current network this nym-api is connected to, e.g. contract addresses, + /// endpoints, denominations. + pub(crate) network_details: NetworkDetails, + + /// A simple in-memory cache of node information mapping their database id to their node-ids + /// and public keys. Useful (I guess?) for returning information about test routes. + // TODO: do we need it? + pub(crate) node_info_cache: unstable::NodeInfoCache, + + /// Cache containing data (build info, versions, etc.) on all nym smart contracts on the network + pub(crate) contract_info_cache: ContractDetailsCache, + + /// Information about this nym-api, i.e. its public key, startup time, etc. + pub(crate) api_status: ApiStatusState, + + // todo: refactor it into inner: Arc<EcashStateInner> + /// Cache holding data required by the ecash credentials - static signatures, merkle trees, etc. + pub(crate) ecash_state: Arc<EcashState>, +} + +impl FromRef<AppState> for ApiStatusState { + fn from_ref(app_state: &AppState) -> Self { + app_state.api_status.clone() + } +} + +impl FromRef<AppState> for Arc<EcashState> { + fn from_ref(app_state: &AppState) -> Self { + app_state.ecash_state.clone() + } +} + +impl FromRef<AppState> for MixnetContractCache { + fn from_ref(app_state: &AppState) -> Self { + app_state.mixnet_contract_cache.clone() + } +} + +impl FromRef<AppState> for SharedCache<SignersCacheData> { + fn from_ref(app_state: &AppState) -> Self { + app_state.ecash_signers_cache.clone() + } +} + +impl AppState { + pub(crate) fn private_signing_key(&self) -> &ed25519::PrivateKey { + // even though we have to go through ecash state, the key is always available + // (moving it would involve some refactoring that's not worth it now) + self.ecash_state.local.identity_keypair.private_key() + } + + pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache { + &self.mixnet_contract_cache + } + + pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { + &self.node_status_cache + } + + pub(crate) fn network_details(&self) -> &NetworkDetails { + &self.network_details + } + + pub(crate) fn described_nodes_cache(&self) -> &SharedCache<DescribedNodes> { + &self.described_nodes_cache + } + + pub(crate) fn storage(&self) -> &storage::NymApiStorage { + &self.storage + } + + pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { + &self.node_info_cache + } +} + +// handler helpers to easily get data or return error response +impl AppState { + pub(crate) async fn describe_nodes_cache_data( + &self, + ) -> Result<RwLockReadGuard<'_, Cache<DescribedNodes>>, AxumErrorResponse> { + Ok(self.described_nodes_cache().get().await?) + } + + pub(crate) async fn rewarded_set( + &self, + ) -> Result<Cache<CachedEpochRewardedSet>, AxumErrorResponse> { + Ok(self.nym_contract_cache().cached_rewarded_set().await?) + } + + pub(crate) async fn node_annotations( + &self, + ) -> Result<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>, AxumErrorResponse> + { + self.node_status_cache() + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn legacy_mixnode_annotations( + &self, + ) -> Result<RwLockReadGuard<'_, Cache<HashMap<NodeId, MixNodeBondAnnotated>>>, AxumErrorResponse> + { + self.node_status_cache() + .annotated_legacy_mixnodes() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn legacy_gateways_annotations( + &self, + ) -> Result<RwLockReadGuard<'_, Cache<HashMap<NodeId, GatewayBondAnnotated>>>, AxumErrorResponse> + { + self.node_status_cache() + .annotated_legacy_gateways() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn get_address_info( + self, + account_id: nym_validator_client::nyxd::AccountId, + ) -> Result<NyxAccountDetails, AxumErrorResponse> { + let address = account_id.to_string(); + match self.address_info_cache.get(&address).await { + Some(guard) => { + tracing::trace!("Fetching from cache..."); + let read_lock = guard.read().await; + Ok(read_lock.clone()) + } + None => { + tracing::trace!("No cache for {}, refreshing data...", &address); + + let address_info = self + .address_info_cache + .collect_balances( + self.nyxd_client.clone(), + self.mixnet_contract_cache.clone(), + self.network_details() + .network + .chain_details + .mix_denom + .base + .to_owned(), + &address, + account_id, + ) + .await?; + + self.address_info_cache + .upsert_address_info(&address, address_info.clone()) + .await; + + Ok(address_info) + } + } + } +} diff --git a/nym-api/src/support/http/unstable_routes.rs b/nym-api/src/support/http/unstable_routes.rs deleted file mode 100644 index 7bc1a9644c..0000000000 --- a/nym-api/src/support/http/unstable_routes.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::nym_nodes::handlers::unstable::nym_node_routes_unstable; -use crate::support::http::state::AppState; -use axum::Router; - -// as those get stabilised, they should get deprecated and use a redirection instead -pub(crate) fn unstable_routes() -> Router<AppState> { - Router::new().nest("/nym-nodes", nym_node_routes_unstable()) -} diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs index abc95b1b6d..430a73aad6 100644 --- a/nym-api/src/support/legacy_helpers.rs +++ b/nym-api/src/support/legacy_helpers.rs @@ -4,12 +4,12 @@ use nym_api_requests::legacy::{LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::NymNodeData; use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_crypto::aes::cipher::crypto_common::rand_core::OsRng; use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; use nym_mixnet_contract_common::{ Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, }; use rand::prelude::SliceRandom; +use rand::rngs::OsRng; use std::net::{IpAddr, ToSocketAddrs}; use std::str::FromStr; @@ -61,7 +61,12 @@ pub(crate) fn to_legacy_mixnode( .node .custom_http_port .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), - sphinx_key: description.host_information.keys.x25519.to_base58_string(), + sphinx_key: description + .host_information + .keys + .current_x25519_sphinx_key + .public_key + .to_base58_string(), identity_key: nym_node.bond_information.node.identity_key.clone(), version: description.build_information.build_version.clone(), }, @@ -95,7 +100,12 @@ pub(crate) fn to_legacy_gateway( .location .map(|c| c.to_string()) .unwrap_or_default(), - sphinx_key: description.host_information.keys.x25519.to_base58_string(), + sphinx_key: description + .host_information + .keys + .current_x25519_sphinx_key + .public_key + .to_base58_string(), identity_key: nym_node.bond_information.node.identity_key.clone(), version: description.build_information.build_version.clone(), }, diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 889ffc0acd..6de33c4738 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -17,10 +17,10 @@ use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, PartialContractDealingData, State}; use nym_coconut_dkg_common::{ dealer::{DealerDetails, DealerDetailsResponse}, - types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}, + types::{EncodedBTEPublicKeyWithProof, Epoch}, verification_key::{ContractVKShare, VerificationKeyShare}, }; -use nym_config::defaults::{ChainDetails, NymNetworkDetails}; +use nym_config::defaults::NymNetworkDetails; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; @@ -29,31 +29,36 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - ConfigScoreParams, CurrentIntervalResponse, EpochRewardedSet, EpochStatus, ExecuteMsg, - GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet, - RoleAssignment, + ConfigScoreParams, CurrentIntervalResponse, Delegation, EpochRewardedSet, EpochStatus, + ExecuteMsg, GatewayBond, HistoricalNymNodeVersionEntry, IdentityKey, KeyRotationState, + NymNodeDetails, RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; -use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; +use nym_validator_client::nyxd::contract_traits::performance_query_client::{ + LastSubmission, NodePerformance, +}; +use nym_validator_client::nyxd::contract_traits::{ + PagedDkgQueryClient, PagedPerformanceQueryClient, PerformanceQueryClient, +}; use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::Coin; use nym_validator_client::nyxd::{ contract_traits::{ DkgQueryClient, DkgSigningClient, EcashQueryClient, GroupQueryClient, MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient, NymContractsProvider, - PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, + PagedMixnetQueryClient, PagedMultisigQueryClient, }, cosmwasm_client::types::ExecuteResult, BlockResponse, CosmWasmClient, Fee, TendermintRpcClient, }; use nym_validator_client::nyxd::{ hash::{Hash, SHA256_HASH_SIZE}, - AccountId, Coin, TendermintTime, + AccountId, TendermintTime, }; use nym_validator_client::{ nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient, QueryHttpRpcNyxdClient, }; -use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; use tendermint::abci::response::Info; @@ -162,10 +167,6 @@ impl Client { } } - pub(crate) async fn chain_details(&self) -> ChainDetails { - nyxd_query!(self, current_chain_details().clone()) - } - pub(crate) async fn get_ecash_contract_address(&self) -> Result<AccountId, EcashError> { nyxd_query!( self, @@ -238,6 +239,10 @@ impl Client { nyxd_query!(self, get_all_preassigned_gateway_ids().await) } + pub(crate) async fn get_key_rotation_state(&self) -> Result<KeyRotationState, NyxdError> { + nyxd_query!(self, get_key_rotation_state().await) + } + pub(crate) async fn get_config_score_params(&self) -> Result<ConfigScoreParams, NyxdError> { nyxd_query!(self, get_mixnet_contract_state_params().await) .map(|state| state.config_score_params) @@ -253,6 +258,12 @@ impl Client { nyxd_query!(self, get_current_interval_details().await) } + pub(crate) async fn get_mixnet_contract_state( + &self, + ) -> Result<nym_mixnet_contract_common::ContractState, NyxdError> { + nyxd_query!(self, get_mixnet_contract_state().await) + } + pub(crate) async fn get_current_epoch_status(&self) -> Result<EpochStatus, NyxdError> { nyxd_query!(self, get_current_epoch_status().await) } @@ -267,31 +278,6 @@ impl Client { nyxd_query!(self, get_rewarded_set().await) } - pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result<u32, NyxdError> { - let guard = self.inner.read().await; - - // the expect is fine as we always construct the client with the vesting contract explicitly set - let vesting_contract = query_guard!( - guard, - vesting_contract_address().expect("vesting contract address is not available") - ); - // TODO: I don't like the usage of the hardcoded value here - let res = query_guard!( - guard, - query_contract_raw(vesting_contract, b"key".to_vec()).await? - ); - if res.is_empty() { - return Ok(0); - } - - serde_json::from_slice(&res).map_err(NyxdError::from) - } - - pub(crate) async fn get_all_vesting_coins( - &self, - ) -> Result<Vec<AccountVestingCoins>, NyxdError> { - nyxd_query!(self, get_all_accounts_vesting_coins().await) - } pub(crate) async fn get_pending_events_count(&self) -> Result<u32, NyxdError> { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -403,6 +389,34 @@ impl Client { nyxd_signing!(self, reconcile_epoch_events(limit, None).await?); Ok(()) } + + pub(crate) async fn get_all_delegator_delegations( + &self, + delegation_owner: &AccountId, + ) -> Result<Vec<Delegation>, NyxdError> { + nyxd_query!(self, get_all_delegator_delegations(delegation_owner).await) + } + + pub(crate) async fn get_address_balance( + &self, + address: &AccountId, + denom: impl Into<String>, + ) -> Result<Option<Coin>, NyxdError> { + nyxd_query!(self, get_balance(&address, denom.into()).await) + } + + pub(crate) async fn get_last_performance_contract_submission( + &self, + ) -> Result<LastSubmission, NyxdError> { + nyxd_query!(self, get_last_submission().await) + } + + pub(crate) async fn get_full_epoch_performance( + &self, + epoch_id: nym_mixnet_contract_common::EpochId, + ) -> Result<Vec<NodePerformance>, NyxdError> { + nyxd_query!(self, get_all_epoch_performance(epoch_id).await) + } } #[async_trait] @@ -488,7 +502,7 @@ impl crate::ecash::client::Client for Client { async fn get_epoch_threshold( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> crate::ecash::error::Result<Option<Threshold>> { Ok(nyxd_query!(self, get_epoch_threshold(epoch_id).await?)) } @@ -502,7 +516,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_dealer_details( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result<RegisteredDealerDetails> { let dealer = dealer @@ -517,7 +531,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealer_dealings_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> crate::ecash::error::Result<DealerDealingsStatusResponse> { Ok(nyxd_query!( @@ -528,7 +542,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_status( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result<DealingStatusResponse> { @@ -544,7 +558,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_metadata( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, dealing_index: DealingIndex, ) -> crate::ecash::error::Result<Option<DealingMetadata>> { @@ -558,7 +572,7 @@ impl crate::ecash::client::Client for Client { async fn get_dealing_chunk( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: &str, dealing_index: DealingIndex, chunk_index: ChunkIndex, @@ -573,7 +587,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_share( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, dealer: String, ) -> Result<Option<ContractVKShare>, EcashError> { Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) @@ -581,7 +595,7 @@ impl crate::ecash::client::Client for Client { async fn get_verification_key_shares( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result<Vec<ContractVKShare>, EcashError> { Ok(nyxd_query!( self, @@ -591,7 +605,7 @@ impl crate::ecash::client::Client for Client { async fn get_registered_ecash_clients( &self, - epoch_id: EpochId, + epoch_id: nym_coconut_dkg_common::types::EpochId, ) -> Result<Vec<EcashApiClient>, EcashError> { Ok(self .get_verification_key_shares(epoch_id) diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index e30b316c0d..710481d41e 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -11,7 +11,6 @@ use crate::support::storage::models::{ use crate::support::storage::DbIdCache; use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; use nym_types::monitoring::NodeResult; -use sqlx::FromRow; use time::{Date, OffsetDateTime}; use tracing::info; @@ -20,113 +19,8 @@ pub(crate) struct StorageManager { pub(crate) connection_pool: sqlx::SqlitePool, } -pub struct AvgMixnodeReliability { - mix_id: NodeId, - value: Option<f32>, -} - -impl AvgMixnodeReliability { - pub fn mix_id(&self) -> NodeId { - self.mix_id - } - - pub fn value(&self) -> f32 { - self.value.unwrap_or_default() - } -} - -#[derive(FromRow)] -pub struct AvgGatewayReliability { - node_id: NodeId, - value: Option<f32>, -} - -impl AvgGatewayReliability { - pub fn node_id(&self) -> NodeId { - self.node_id - } - - pub fn value(&self) -> f32 { - self.value.unwrap_or_default() - } -} - // all SQL goes here impl StorageManager { - pub(super) async fn get_all_avg_mix_reliability_in_last_24hr( - &self, - end_ts_secs: i64, - ) -> Result<Vec<AvgMixnodeReliability>, sqlx::Error> { - let start_ts_secs = end_ts_secs - 86400; - self.get_all_avg_mix_reliability_in_time_interval(start_ts_secs, end_ts_secs) - .await - } - - pub(super) async fn get_all_avg_gateway_reliability_in_last_24hr( - &self, - end_ts_secs: i64, - ) -> Result<Vec<AvgGatewayReliability>, sqlx::Error> { - let start_ts_secs = end_ts_secs - 86400; - self.get_all_avg_gateway_reliability_in_interval(start_ts_secs, end_ts_secs) - .await - } - - pub(super) async fn get_all_avg_mix_reliability_in_time_interval( - &self, - start_ts_secs: i64, - end_ts_secs: i64, - ) -> Result<Vec<AvgMixnodeReliability>, sqlx::Error> { - let result = sqlx::query_as!( - AvgMixnodeReliability, - r#" - SELECT - d.mix_id as "mix_id: NodeId", - AVG(s.reliability) as "value: f32" - FROM - mixnode_details d - JOIN - mixnode_status s on d.id = s.mixnode_details_id - WHERE - timestamp >= ? AND - timestamp <= ? - GROUP BY 1 - "#, - start_ts_secs, - end_ts_secs - ) - .fetch_all(&self.connection_pool) - .await?; - Ok(result) - } - - pub(super) async fn get_all_avg_gateway_reliability_in_interval( - &self, - start_ts_secs: i64, - end_ts_secs: i64, - ) -> Result<Vec<AvgGatewayReliability>, sqlx::Error> { - let result = sqlx::query_as!( - AvgGatewayReliability, - r#" - SELECT - d.node_id as "node_id: NodeId", - CASE WHEN count(*) > 3 THEN AVG(reliability) ELSE 100 END as "value: f32" - FROM - gateway_details d - JOIN - gateway_status s on d.id = s.gateway_details_id - WHERE - timestamp >= ? AND - timestamp <= ? - GROUP BY 1 - "#, - start_ts_secs, - end_ts_secs - ) - .fetch_all(&self.connection_pool) - .await?; - Ok(result) - } - /// Tries to obtain row id of given mixnode given its identity. /// /// # Arguments @@ -187,21 +81,6 @@ impl StorageManager { Ok(node_id) } - pub(super) async fn get_gateway_identity_key( - &self, - node_id: NodeId, - ) -> Result<Option<IdentityKey>, sqlx::Error> { - let identity_key = sqlx::query!( - "SELECT identity FROM gateway_details WHERE node_id = ?", - node_id - ) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.identity); - - Ok(identity_key) - } - /// Tries to obtain identity value of given mixnode given its mix_id /// /// # Arguments @@ -222,62 +101,6 @@ impl StorageManager { Ok(identity_key) } - /// Gets all reliability statuses for mixnode with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_mixnode_statuses_since( - &self, - mix_id: NodeId, - timestamp: i64, - ) -> Result<Vec<NodeStatus>, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM mixnode_status - JOIN mixnode_details - ON mixnode_status.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?; - "#, - mix_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - - /// Gets all reliability statuses for gateway with particular identity that were inserted - /// into the database after the specified unix timestamp. - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(super) async fn get_gateway_statuses_since( - &self, - node_id: NodeId, - timestamp: i64, - ) -> Result<Vec<NodeStatus>, sqlx::Error> { - sqlx::query_as!( - NodeStatus, - r#" - SELECT timestamp, reliability as "reliability: u8" - FROM gateway_status - JOIN gateway_details - ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; - "#, - node_id, - timestamp, - ) - .fetch_all(&self.connection_pool) - .await - } - /// Gets the historical daily uptime associated with the particular mixnode /// /// # Arguments @@ -747,7 +570,7 @@ impl StorageManager { &self, db_mixnode_id: i64, since: i64, - ) -> Result<i32, sqlx::Error> { + ) -> Result<i64, sqlx::Error> { let count = sqlx::query!( r#" SELECT COUNT(*) as count FROM @@ -786,7 +609,7 @@ impl StorageManager { &self, gateway_id: i64, since: i64, - ) -> Result<i32, sqlx::Error> { + ) -> Result<i64, sqlx::Error> { let count = sqlx::query!( r#" SELECT COUNT(*) as count FROM @@ -824,7 +647,7 @@ impl StorageManager { ) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == Some(1)) + .map(|result| result.exists == 1) } /// Creates new entry for mixnode historical uptime @@ -966,7 +789,7 @@ impl StorageManager { &self, since: i64, until: i64, - ) -> Result<i32, sqlx::Error> { + ) -> Result<i64, sqlx::Error> { let count = sqlx::query!( "SELECT COUNT(*) as count FROM monitor_run WHERE timestamp > ? AND timestamp < ?", since, @@ -1227,7 +1050,7 @@ impl StorageManager { .await } - pub(super) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result<i32, sqlx::Error> { + pub(super) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result<i64, sqlx::Error> { sqlx::query!( r#" SELECT COUNT(*) as count @@ -1279,7 +1102,7 @@ impl StorageManager { .await } - pub(super) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result<i32, sqlx::Error> { + pub(super) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result<i64, sqlx::Error> { sqlx::query!( r#" SELECT COUNT(*) as count @@ -1341,7 +1164,7 @@ pub(crate) mod v3_migration { sqlx::query!("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'",) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == Some(1)) + .map(|result| result.exists == 1) } pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), sqlx::Error> { diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index cc10d55e76..8bb14a697d 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -1,7 +1,6 @@ // Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; use crate::network_monitor::monitor::summary_producer::TestReport; use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ @@ -10,7 +9,7 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; -use crate::storage::models::{NodeStatus, TestingRoute}; +use crate::storage::models::TestingRoute; use crate::support::storage::models::{ GatewayDetails, HistoricalUptime, MixnodeDetails, MonitorRunReport, MonitorRunScore, TestedGatewayStatus, TestedMixnodeStatus, @@ -134,148 +133,6 @@ impl NymApiStorage { Ok(None) } - pub(crate) async fn get_all_avg_gateway_reliability_in_last_24hr( - &self, - end_ts_secs: i64, - ) -> Result<Vec<AvgGatewayReliability>, NymApiStorageError> { - let result = self - .manager - .get_all_avg_gateway_reliability_in_last_24hr(end_ts_secs) - .await?; - - Ok(result) - } - - pub(crate) async fn get_all_avg_mix_reliability_in_last_24hr( - &self, - end_ts_secs: i64, - ) -> Result<Vec<AvgMixnodeReliability>, NymApiStorageError> { - let result = self - .manager - .get_all_avg_mix_reliability_in_last_24hr(end_ts_secs) - .await?; - - Ok(result) - } - - /// Gets all statuses for particular mixnode that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode to query. - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_mixnode_statuses( - &self, - mix_id: NodeId, - since: i64, - ) -> Result<Vec<NodeStatus>, NymApiStorageError> { - let statuses = self - .manager - .get_mixnode_statuses_since(mix_id, since) - .await?; - - Ok(statuses) - } - - /// Gets all statuses for particular gateway that were inserted - /// since the provided timestamp. - /// - /// # Arguments - /// - /// * `since`: unix timestamp indicating the lower bound interval of the selection. - async fn get_gateway_statuses( - &self, - node_id: NodeId, - since: i64, - ) -> Result<Vec<NodeStatus>, NymApiStorageError> { - let statuses = self - .manager - .get_gateway_statuses_since(node_id, since) - .await?; - - Ok(statuses) - } - - /// Tries to construct a status report for mixnode with the specified mix_id. - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn construct_mixnode_report( - &self, - mix_id: NodeId, - ) -> Result<MixnodeStatusReport, NymApiStorageError> { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_mixnode_statuses(mix_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::MixnodeReportNotFound { mix_id }); - } - - // determine the number of runs the mixnode should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(mixnode_identity) = self.manager.get_mixnode_identity_key(mix_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {mix_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(MixnodeStatusReport::construct_from_last_day_reports( - now, - mix_id, - mixnode_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - - pub(crate) async fn construct_gateway_report( - &self, - node_id: NodeId, - ) -> Result<GatewayStatusReport, NymApiStorageError> { - let now = OffsetDateTime::now_utc(); - let day_ago = (now - ONE_DAY).unix_timestamp(); - let hour_ago = (now - ONE_HOUR).unix_timestamp(); - - let statuses = self.get_gateway_statuses(node_id, day_ago).await?; - - // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report - if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { node_id }); - } - - // determine the number of runs the gateway should have been online for - let last_hour_runs_count = self - .get_monitor_runs_count(hour_ago, now.unix_timestamp()) - .await?; - let last_day_runs_count = self - .get_monitor_runs_count(day_ago, now.unix_timestamp()) - .await?; - - let Some(gateway_identity) = self.manager.get_gateway_identity_key(node_id).await? else { - return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {node_id} doesn't have an identity even though we have status information on it!") }); - }; - - Ok(GatewayStatusReport::construct_from_last_day_reports( - now, - node_id, - gateway_identity, - statuses, - last_hour_runs_count, - last_day_runs_count, - )) - } - pub(crate) async fn get_mixnode_uptime_history( &self, mix_id: NodeId, @@ -679,7 +536,7 @@ impl NymApiStorage { &self, mix_id: NodeId, since: Option<i64>, - ) -> Result<i32, NymApiStorageError> { + ) -> Result<i64, NymApiStorageError> { let db_id = self.manager.get_mixnode_database_id(mix_id).await?; if let Some(node_id) = db_id { @@ -707,7 +564,7 @@ impl NymApiStorage { &self, identity: &str, since: Option<i64>, - ) -> Result<i32, NymApiStorageError> { + ) -> Result<i64, NymApiStorageError> { let node_id = self .manager .get_gateway_database_id_by_identity(identity) diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/unstable_routes/helpers.rs similarity index 69% rename from nym-api/src/nym_nodes/handlers/unstable/helpers.rs rename to nym-api/src/unstable_routes/helpers.rs index 3940d99237..216ffacf4b 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/unstable_routes/helpers.rs @@ -1,10 +1,10 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only use nym_api_requests::models::{ GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, }; -use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; +use nym_api_requests::nym_nodes::{NodeRole, SemiSkimmedNode, SkimmedNode}; use nym_mixnet_contract_common::reward_params::Performance; use time::OffsetDateTime; @@ -14,6 +14,11 @@ pub(crate) trait LegacyAnnotation { fn identity(&self) -> &str; fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond>; + + fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result<SemiSkimmedNode, MalformedNodeBond>; } impl LegacyAnnotation for MixNodeBondAnnotated { @@ -28,6 +33,13 @@ impl LegacyAnnotation for MixNodeBondAnnotated { fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> { self.try_to_skimmed_node(role) } + + fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result<SemiSkimmedNode, MalformedNodeBond> { + self.try_to_semi_skimmed_node(role) + } } impl LegacyAnnotation for GatewayBondAnnotated { @@ -42,6 +54,13 @@ impl LegacyAnnotation for GatewayBondAnnotated { fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> { self.try_to_skimmed_node(role) } + + fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result<SemiSkimmedNode, MalformedNodeBond> { + self.try_to_semi_skimmed_node(role) + } } pub(crate) fn refreshed_at( diff --git a/nym-api/src/unstable_routes/mod.rs b/nym-api/src/unstable_routes/mod.rs new file mode 100644 index 0000000000..d95805ba63 --- /dev/null +++ b/nym-api/src/unstable_routes/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod helpers; +pub(crate) mod v1; +pub(crate) mod v2; diff --git a/nym-api/src/unstable_routes/v1/account/cache.rs b/nym-api/src/unstable_routes/v1/account/cache.rs new file mode 100644 index 0000000000..23623b2bf3 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/account/cache.rs @@ -0,0 +1,120 @@ +use crate::unstable_routes::v1::account::data_collector::AddressDataCollector; +use crate::unstable_routes::v1::account::models::{NyxAccountDelegationDetails, NyxAccountDetails}; +use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, node_status_api::models::AxumResult, +}; +use moka::{future::Cache, Entry}; +use nym_validator_client::nyxd::AccountId; +use std::{sync::Arc, time::Duration}; +use tokio::sync::RwLock; + +#[derive(Clone)] +pub(crate) struct AddressInfoCache { + inner: Cache<String, Arc<RwLock<NyxAccountDetails>>>, +} + +impl AddressInfoCache { + pub(crate) fn new(cache_ttl: Duration, capacity: u64) -> Self { + AddressInfoCache { + inner: Cache::builder() + .time_to_live(cache_ttl) + .max_capacity(capacity) + .build(), + } + } + + pub(crate) async fn get(&self, key: &str) -> Option<Arc<RwLock<NyxAccountDetails>>> { + self.inner.get(key).await + } + + pub(crate) async fn upsert_address_info( + &self, + address: &str, + address_info: NyxAccountDetails, + ) -> Entry<String, Arc<RwLock<NyxAccountDetails>>> { + self.inner + .entry_by_ref(address) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = address_info; + v.clone() + } else { + Arc::new(RwLock::new(address_info)) + } + }) + .await + } + + pub(crate) async fn collect_balances( + &self, + nyxd_client: crate::nyxd::Client, + nym_contract_cache: MixnetContractCache, + base_denom: String, + address: &str, + account_id: AccountId, + ) -> AxumResult<NyxAccountDetails> { + let mut collector = AddressDataCollector::new( + nyxd_client, + nym_contract_cache, + base_denom, + account_id.clone(), + ); + + // ==> get balances of chain tokens <== + let balance = collector.get_address_balance().await?; + + // it's very difficult to lower existing balance to exactly 0 + // so assume this is an unused address and return early + if balance.amount == 0 { + let address_info = NyxAccountDetails { + address: address.to_string(), + balance: balance.clone().into(), + total_value: balance.clone().into(), + delegations: Vec::new(), + accumulated_rewards: Vec::new(), + total_delegations: balance.clone().into(), + claimable_rewards: balance.clone().into(), + operator_rewards: None, + }; + + return Ok(address_info); + } + + // ==> get list of delegations (history) <== + let delegation_data = collector.get_delegations().await?; + + // ==> get the current reward for each active delegation <== + // calculate rewards from nodes this delegator delegated to + let accumulated_rewards = collector.calculate_rewards(&delegation_data).await?; + + // ==> convert totals <== + let claimable_rewards = collector.claimable_rewards(); + let total_value = collector.total_value(); + let total_delegations = collector.total_delegations(); + let operator_rewards = collector.operator_rewards(); + + let address_info = NyxAccountDetails { + address: account_id.to_string(), + balance: balance.into(), + delegations: delegation_data + .into_iter() + .map(|d| NyxAccountDelegationDetails { + delegated: d.details().amount.clone(), + height: d.details().height, + node_id: d.details().node_id, + proxy: d.details().proxy.clone(), + node_bonded: d.is_node_bonded(), + }) + .collect(), + accumulated_rewards, + total_delegations, + claimable_rewards, + total_value, + operator_rewards, + }; + + Ok(address_info) + } +} diff --git a/nym-api/src/unstable_routes/v1/account/data_collector.rs b/nym-api/src/unstable_routes/v1/account/data_collector.rs new file mode 100644 index 0000000000..4d29f6dc29 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/account/data_collector.rs @@ -0,0 +1,233 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::unstable_routes::v1::account::models::NyxAccountDelegationRewardDetails; +use crate::{ + mixnet_contract_cache::cache::MixnetContractCache, + node_status_api::models::{AxumErrorResponse, AxumResult}, +}; +use cosmwasm_std::{Coin, Decimal}; +use nym_mixnet_contract_common::NodeRewarding; +use nym_validator_client::nyxd::AccountId; +use std::collections::{HashMap, HashSet}; +use tracing::error; + +pub(crate) struct AddressDataCollector { + nyxd_client: crate::nyxd::Client, + nym_contract_cache: MixnetContractCache, + account_id: AccountId, + total_value: u128, + operator_rewards: u128, + claimable_rewards: u128, + total_delegations: u128, + base_denom: String, +} + +impl AddressDataCollector { + pub(crate) fn new( + nyxd_client: crate::nyxd::Client, + nym_contract_cache: MixnetContractCache, + base_denom: String, + account_id: AccountId, + ) -> Self { + Self { + nyxd_client, + nym_contract_cache, + base_denom, + account_id, + total_value: 0, + operator_rewards: 0, + claimable_rewards: 0, + total_delegations: 0, + } + } + + pub(crate) async fn get_address_balance( + &mut self, + ) -> AxumResult<nym_validator_client::nyxd::Coin> { + let balance = self + .nyxd_client + .get_address_balance(&self.account_id, &self.base_denom) + .await? + .unwrap_or_else(|| nym_validator_client::nyxd::Coin::new(0u128, &self.base_denom)); + self.total_value += balance.amount; + + Ok(balance) + } + + pub(crate) async fn get_delegations(&mut self) -> AxumResult<Vec<AddressDelegationInfo>> { + let og_delegations = self + .nyxd_client + .get_all_delegator_delegations(&self.account_id) + .await? + .into_iter() + .map(|delegation| (delegation.node_id, delegation)) + .collect::<HashMap<_, _>>(); + + let mut node_delegation_info = Vec::new(); + + let delegated_to_nodes_bonded = self + .nym_contract_cache + .all_cached_nym_nodes() + .await + .ok_or_else(AxumErrorResponse::service_unavailable)? + .iter() + .filter_map(|node_details| { + // is this an operator of this node? + if self.account_id.to_string() == node_details.bond_information.owner.as_str() { + let pending_operator_reward = + node_details.pending_operator_reward().amount.u128(); + + // add operator rewards + self.operator_rewards += pending_operator_reward; + + // add to totals + self.total_value += pending_operator_reward; + } + if let Some(delegation) = og_delegations.get(&node_details.node_id()) { + node_delegation_info.push(AddressDelegationInfo { + details: delegation.clone(), + node_reward_info: NodeBondStatus::Bonded { + rewarding_info: node_details.rewarding_details.to_owned(), + unbonding: node_details.is_unbonding(), + }, + }); + + Some(node_details.node_id()) + } else { + None + } + }) + .collect::<HashSet<_>>(); + + for (node_id, delegation) in og_delegations { + if !delegated_to_nodes_bonded.contains(&node_id) { + node_delegation_info.push(AddressDelegationInfo { + details: delegation.clone(), + node_reward_info: NodeBondStatus::UnBonded, + }); + } + } + + Ok(node_delegation_info) + } + + pub(crate) async fn calculate_rewards( + &mut self, + delegation_data: &Vec<AddressDelegationInfo>, + ) -> AxumResult<Vec<NyxAccountDelegationRewardDetails>> { + let mut accumulated_rewards = Vec::new(); + for delegation in delegation_data { + let node_id = delegation.details.node_id; + + match &delegation.node_reward_info { + NodeBondStatus::Bonded { + rewarding_info, + unbonding, + } => { + match rewarding_info.determine_delegation_reward(&delegation.details) { + Ok(delegation_reward) => { + let reward = NyxAccountDelegationRewardDetails { + node_id, + rewards: decimal_to_coin(delegation_reward, &self.base_denom), + amount_staked: delegation.details.amount.clone(), + node_still_fully_bonded: !unbonding, + }; + // 4. sum the rewards and delegations + self.total_delegations += delegation.details.amount.amount.u128(); + self.total_value += delegation.details.amount.amount.u128(); + self.total_value += reward.rewards.amount.u128(); + self.claimable_rewards += reward.rewards.amount.u128(); + + accumulated_rewards.push(reward); + } + Err(err) => { + error!( + "Couldn't determine delegations for {} on node {}: {}", + &self.account_id, node_id, err + ) + } + } + } + NodeBondStatus::UnBonded => { + // directory cache doesn't store node details required to + // calculate rewarding for unbonded nodes + } + } + } + + Ok(accumulated_rewards) + } + + pub(crate) fn claimable_rewards(&self) -> Coin { + Coin::new(self.claimable_rewards, self.base_denom.to_string()) + } + + pub(crate) fn total_value(&self) -> Coin { + Coin::new(self.total_value, self.base_denom.to_string()) + } + + pub(crate) fn total_delegations(&self) -> Coin { + Coin::new(self.total_delegations, self.base_denom.to_string()) + } + + pub(crate) fn operator_rewards(&self) -> Option<Coin> { + if self.operator_rewards > 0 { + Some(Coin::new( + self.operator_rewards, + self.base_denom.to_string(), + )) + } else { + None + } + } +} + +pub(crate) struct AddressDelegationInfo { + details: nym_mixnet_contract_common::Delegation, + node_reward_info: NodeBondStatus, +} + +impl AddressDelegationInfo { + pub(crate) fn details(&self) -> &nym_mixnet_contract_common::Delegation { + &self.details + } + + pub(crate) fn is_node_bonded(&self) -> bool { + matches!(self.node_reward_info, NodeBondStatus::Bonded { .. }) + } +} + +pub(crate) enum NodeBondStatus { + Bonded { + rewarding_info: NodeRewarding, + unbonding: bool, + }, + UnBonded, +} + +fn decimal_to_coin(decimal: Decimal, denom: impl Into<String>) -> Coin { + Coin::new(decimal.to_uint_floor(), denom) +} + +#[cfg(test)] +mod test { + + use super::*; + + #[tokio::test] + async fn decimal_to_coin_test() { + let test_values = [ + (1234, 0, 1234), + (1234, 2, 12), + (1_234_000_000_000_000u128, 6, 1_234_000_000u128), + ]; + + for (amount, decimal_places, coin_amount) in test_values { + let decimal = + Decimal::from_atomics(cosmwasm_std::Uint128::new(amount), decimal_places).unwrap(); + let coin_from_decimal = decimal_to_coin(decimal, "unym"); + assert_eq!(coin_from_decimal, Coin::new(coin_amount, "unym")); + } + } +} diff --git a/nym-api/src/unstable_routes/v1/account/mod.rs b/nym-api/src/unstable_routes/v1/account/mod.rs new file mode 100644 index 0000000000..8b7b83d11c --- /dev/null +++ b/nym-api/src/unstable_routes/v1/account/mod.rs @@ -0,0 +1,55 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{ + node_status_api::models::{AxumErrorResponse, AxumResult}, + support::http::state::AppState, +}; +use axum::{ + extract::{Path, State}, + routing::get, + Json, Router, +}; +use models::NyxAccountDetails; +use nym_validator_client::nyxd::AccountId; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tracing::{error, instrument}; +use utoipa::ToSchema; + +pub(crate) mod cache; +pub(crate) mod data_collector; +pub(crate) mod models; + +pub(crate) fn routes() -> Router<AppState> { + Router::new().route("/:address", get(address)) +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)] +pub struct AddressQueryParam { + #[serde(default)] + pub address: String, +} + +#[utoipa::path( + tag = "Unstable", + get, + path = "/{address}", + context_path = "/v1/unstable/account", + responses( + (status = 200, body = NyxAccountDetails) + ), + params(AddressQueryParam) +)] +#[instrument(level = "info", skip_all, fields(address=address))] +async fn address( + Path(AddressQueryParam { address }): Path<AddressQueryParam>, + State(state): State<AppState>, +) -> AxumResult<Json<NyxAccountDetails>> { + let account_id = AccountId::from_str(&address).map_err(|err| { + error!("{err}"); + AxumErrorResponse::not_found(&address) + })?; + + state.get_address_info(account_id).await.map(Json) +} diff --git a/nym-api/src/unstable_routes/v1/account/models.rs b/nym-api/src/unstable_routes/v1/account/models.rs new file mode 100644 index 0000000000..1d802f8aa0 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/account/models.rs @@ -0,0 +1,55 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use cosmwasm_std::{Addr, Coin}; +use nym_topology::NodeId; +use serde::{Deserialize, Serialize}; +use utoipa::schema; + +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)] +#[schema(title = "Coin")] +pub struct CoinSchema { + pub denom: String, + pub amount: u128, +} + +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)] +pub struct NyxAccountDelegationDetails { + pub node_id: NodeId, + #[schema(value_type = CoinSchema)] + pub delegated: Coin, + pub height: u64, + #[schema(value_type = Option<String>)] + pub proxy: Option<Addr>, + pub node_bonded: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)] +pub struct NyxAccountDelegationRewardDetails { + pub node_id: NodeId, + #[schema(value_type = CoinSchema)] + pub rewards: Coin, + #[schema(value_type = String)] + pub amount_staked: Coin, + pub node_still_fully_bonded: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)] +pub struct NyxAccountDetails { + pub address: String, + #[schema(value_type = CoinSchema)] + pub balance: Coin, + #[schema(value_type = CoinSchema)] + pub total_value: Coin, + pub delegations: Vec<NyxAccountDelegationDetails>, + /// Shows rewards from delegations to **currently** bonded nodes. + /// Rewards from nodes that user delegated to, but were later unbonded, + /// are claimable, but not shown here. + pub accumulated_rewards: Vec<NyxAccountDelegationRewardDetails>, + #[schema(value_type = String)] + pub total_delegations: Coin, + #[schema(value_type = CoinSchema)] + pub claimable_rewards: Coin, + #[schema(value_type = Option<CoinSchema>)] + pub operator_rewards: Option<Coin>, +} diff --git a/nym-api/src/unstable_routes/v1/mod.rs b/nym-api/src/unstable_routes/v1/mod.rs new file mode 100644 index 0000000000..321ae6039e --- /dev/null +++ b/nym-api/src/unstable_routes/v1/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use axum::Router; + +pub(crate) mod account; +pub(crate) mod nym_nodes; + +// as those get stabilised, they should get deprecated and use a redirection instead +pub(crate) fn unstable_routes_v1() -> Router<AppState> { + Router::new() + .nest("/nym-nodes", nym_nodes::routes()) + .nest("/account", account::routes()) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs b/nym-api/src/unstable_routes/v1/nym_nodes/full_fat/mod.rs similarity index 62% rename from nym-api/src/nym_nodes/handlers/unstable/full_fat.rs rename to nym-api/src/unstable_routes/v1/nym_nodes/full_fat/mod.rs index debc284208..df76984fcf 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs +++ b/nym-api/src/unstable_routes/v1/nym_nodes/full_fat/mod.rs @@ -1,12 +1,12 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; +use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole; use axum::extract::{Query, State}; -use axum::Json; use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode}; +use nym_http_api_common::FormattedResponse; #[utoipa::path( tag = "Unstable Nym Nodes", @@ -15,13 +15,13 @@ use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode}; path = "", context_path = "/v1/unstable/nym-nodes/full-fat", responses( - // (status = 200, body = CachedNodesResponse<FullFatNode>) + // (status = 200, body = CachedNodesResponse<FullFatNode>) (status = 501) ) )] -pub(super) async fn nodes_detailed( +pub(crate) async fn nodes_detailed( _state: State<AppState>, _query_params: Query<NodesParamsWithRole>, -) -> AxumResult<Json<CachedNodesResponse<FullFatNode>>> { +) -> AxumResult<FormattedResponse<CachedNodesResponse<FullFatNode>>> { Err(AxumErrorResponse::not_implemented()) } diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/handlers.rs b/nym-api/src/unstable_routes/v1/nym_nodes/handlers.rs new file mode 100644 index 0000000000..f2582cec82 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/nym_nodes/handlers.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::nym_nodes::{NodesByAddressesRequestBody, NodesByAddressesResponse}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use std::collections::HashMap; + +#[utoipa::path( + tag = "Unstable Nym Nodes", + post, + request_body = NodesByAddressesRequestBody, + path = "/by-addresses", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, content( + (NodesByAddressesResponse = "application/json"), + (NodesByAddressesResponse = "application/yaml"), + (NodesByAddressesResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +pub(crate) async fn nodes_by_addresses( + Query(output): Query<OutputParams>, + state: State<AppState>, + Json(body): Json<NodesByAddressesRequestBody>, +) -> AxumResult<FormattedResponse<NodesByAddressesResponse>> { + // if the request is too big, simply reject it + if body.addresses.len() > 100 { + return Err(AxumErrorResponse::bad_request( + "requested too many addresses", + )); + } + + let output = output.output.unwrap_or_default(); + + // TODO: perhaps introduce different cache because realistically nym-api will receive + // request for the same couple addresses from all nodes in quick succession + let describe_cache = state.describe_nodes_cache_data().await?; + + let mut existence = HashMap::new(); + for address in body.addresses { + existence.insert(address, describe_cache.node_with_address(address)); + } + + Ok(output.to_response(NodesByAddressesResponse { existence })) +} diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/helpers.rs b/nym-api/src/unstable_routes/v1/nym_nodes/helpers.rs new file mode 100644 index 0000000000..d3c021a7a9 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/nym_nodes/helpers.rs @@ -0,0 +1,65 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::http::helpers::PaginationRequest; +use nym_api_requests::nym_nodes::NodeRoleQueryParam; +use nym_http_api_common::Output; +use serde::Deserialize; + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +pub(crate) struct NodesParamsWithRole { + #[param(inline)] + pub(crate) role: Option<NodeRoleQueryParam>, + + #[allow(dead_code)] + pub(crate) semver_compatibility: Option<String>, + pub(crate) no_legacy: Option<bool>, + pub(crate) page: Option<u32>, + pub(crate) per_page: Option<u32>, + + // Identifier for the current epoch of the topology state. When sent by a client we can check if + // the client already knows about the latest topology state, allowing a `no-updates` response + // instead of wasting bandwidth serving an unchanged topology. + pub(crate) epoch_id: Option<u32>, + + pub(crate) output: Option<Output>, +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct NodesParams { + #[allow(dead_code)] + pub(crate) semver_compatibility: Option<String>, + pub(crate) no_legacy: Option<bool>, + pub(crate) page: Option<u32>, + pub(crate) per_page: Option<u32>, + + // Identifier for the current epoch of the topology state. When sent by a client we can check if + // the client already knows about the latest topology state, allowing a `no-updates` response + // instead of wasting bandwidth serving an unchanged topology. + pub(crate) epoch_id: Option<u32>, + pub(crate) output: Option<Output>, +} + +impl From<NodesParamsWithRole> for NodesParams { + fn from(params: NodesParamsWithRole) -> Self { + NodesParams { + semver_compatibility: params.semver_compatibility, + no_legacy: params.no_legacy, + page: params.page, + per_page: params.per_page, + epoch_id: params.epoch_id, + output: params.output, + } + } +} + +impl<'a> From<&'a NodesParams> for PaginationRequest { + fn from(params: &'a NodesParams) -> Self { + PaginationRequest { + output: params.output, + page: params.page, + per_page: params.per_page, + } + } +} diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/mod.rs b/nym-api/src/unstable_routes/v1/nym_nodes/mod.rs new file mode 100644 index 0000000000..73499d859b --- /dev/null +++ b/nym-api/src/unstable_routes/v1/nym_nodes/mod.rs @@ -0,0 +1,80 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +//! All routes/nodes are split into three tiers: +//! +//! `/skimmed` +//! - used by clients +//! - returns the very basic information for routing purposes +//! +//! `/semi-skimmed` +//! - used by other nodes/VPN +//! - returns more additional information such as noise keys +//! +//! `/full-fat` +//! - used by explorers, et al. +//! - returns almost everything there is about the nodes +//! +//! There's also additional split based on the role: +//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) +//! - `/mixnodes/<tier>` => only returns mixnode role data +//! - `/gateway/<tier>` => only returns (entry) gateway role data + +use crate::support::http::state::AppState; +use crate::unstable_routes::v1::nym_nodes::full_fat::nodes_detailed; +use crate::unstable_routes::v1::nym_nodes::handlers::nodes_by_addresses; +use crate::unstable_routes::v1::nym_nodes::semi_skimmed::nodes_expanded; +use axum::routing::{get, post}; +use axum::Router; +use tower_http::compression::CompressionLayer; + +#[allow(deprecated)] +use crate::unstable_routes::v1::nym_nodes::skimmed::{ + entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active, + exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active, + nodes_basic_all, +}; + +pub(crate) mod full_fat; +pub(crate) mod handlers; +pub(crate) mod helpers; +pub(crate) mod semi_skimmed; +pub(crate) mod skimmed; + +#[allow(deprecated)] +pub(crate) fn routes() -> Router<AppState> { + Router::new() + .nest( + "/skimmed", + Router::new() + .route("/", get(nodes_basic_all)) + .route("/active", get(nodes_basic_active)) + .nest( + "/mixnodes", + Router::new() + .route("/active", get(mixnodes_basic_active)) + .route("/all", get(mixnodes_basic_all)), + ) + .nest( + "/entry-gateways", + Router::new() + .route("/active", get(entry_gateways_basic_active)) + .route("/all", get(entry_gateways_basic_all)), + ) + .nest( + "/exit-gateways", + Router::new() + .route("/active", get(exit_gateways_basic_active)) + .route("/all", get(exit_gateways_basic_all)), + ), + ) + .nest( + "/semi-skimmed", + Router::new().route("/", get(nodes_expanded)), + ) + .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) + .route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic)) + .route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic)) + .route("/by-addresses", post(nodes_by_addresses)) + .layer(CompressionLayer::new()) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs similarity index 62% rename from nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs rename to nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs index e8b9ffb2bc..e83c220321 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs +++ b/nym-api/src/unstable_routes/v1/nym_nodes/semi_skimmed/mod.rs @@ -1,12 +1,12 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; +use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole; use axum::extract::{Query, State}; -use axum::Json; use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; +use nym_http_api_common::FormattedResponse; #[utoipa::path( tag = "Unstable Nym Nodes", @@ -15,13 +15,13 @@ use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; path = "", context_path = "/v1/unstable/nym-nodes/semi-skimmed", responses( - // (status = 200, body = CachedNodesResponse<SemiSkimmedNode>) + // (status = 200, body = CachedNodesResponse<SemiSkimmedNode>) (status = 501) ) )] -pub(super) async fn nodes_expanded( +pub(crate) async fn nodes_expanded( _state: State<AppState>, _query_params: Query<NodesParamsWithRole>, -) -> AxumResult<Json<CachedNodesResponse<SemiSkimmedNode>>> { +) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> { Err(AxumErrorResponse::not_implemented()) } diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/handlers.rs b/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/handlers.rs new file mode 100644 index 0000000000..ded98f2923 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/handlers.rs @@ -0,0 +1,309 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use crate::unstable_routes::v1::nym_nodes::helpers::{NodesParams, NodesParamsWithRole}; +use crate::unstable_routes::v1::nym_nodes::skimmed::helpers::{ + entry_gateways_basic, exit_gateways_basic, mixnodes_basic, nodes_basic, +}; +use crate::unstable_routes::v1::nym_nodes::skimmed::{ + PaginatedCachedNodesResponseSchema, PaginatedSkimmedNodes, +}; +use axum::extract::{Query, State}; +use nym_api_requests::nym_nodes::{CachedNodesResponse, NodeRoleQueryParam, SkimmedNode}; +use nym_http_api_common::FormattedResponse; + +/// Deprecated query that gets ALL gateways +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/gateways/skimmed", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, content( + (CachedNodesResponse<SkimmedNode> = "application/json"), + (CachedNodesResponse<SkimmedNode> = "application/yaml"), + (CachedNodesResponse<SkimmedNode> = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] +#[allow(deprecated)] +pub(crate) async fn deprecated_gateways_basic( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> { + let output = query_params.output.unwrap_or_default(); + + // 1. call '/v1/unstable/skimmed/entry-gateways/all' + let all_gateways = entry_gateways_basic_all(state, query_params) + .await? + .into_inner(); + + // 3. return result + Ok(output.to_response(CachedNodesResponse { + refreshed_at: all_gateways.refreshed_at, + // 2. remove pagination + nodes: all_gateways.nodes.data, + })) +} + +/// Deprecated query that gets ACTIVE-ONLY mixnodes +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/skimmed", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, content( + (CachedNodesResponse<SkimmedNode> = "application/json"), + (CachedNodesResponse<SkimmedNode> = "application/yaml"), + (CachedNodesResponse<SkimmedNode> = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")] +#[allow(deprecated)] +pub(crate) async fn deprecated_mixnodes_basic( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> AxumResult<FormattedResponse<CachedNodesResponse<SkimmedNode>>> { + let output = query_params.output.unwrap_or_default(); + + // 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active' + let active_mixnodes = mixnodes_basic_active(state, query_params) + .await? + .into_inner(); + + // 3. return result + Ok(output.to_response(CachedNodesResponse { + refreshed_at: active_mixnodes.refreshed_at, + // 2. remove pagination + nodes: active_mixnodes.nodes.data, + })) +} + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v2/unstable/nym-nodes/skimmed' instead")] +#[allow(deprecated)] +pub(crate) async fn nodes_basic_all( + state: State<AppState>, + Query(query_params): Query<NodesParamsWithRole>, +) -> PaginatedSkimmedNodes { + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_all(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_all(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_all(state, Query(query_params.into())).await + } + }; + } + + nodes_basic(state, Query(query_params.into()), false).await +} + +/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the **active set** +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated] +#[allow(deprecated)] +pub(crate) async fn nodes_basic_active( + state: State<AppState>, + Query(query_params): Query<NodesParamsWithRole>, +) -> PaginatedSkimmedNodes { + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_active(state, Query(query_params.into())).await + } + }; + } + + nodes_basic(state, Query(query_params.into()), true).await +} + +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and support mixing role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v2/unstable/nym-nodes/skimmed/mixnodes/all' instead")] +pub(crate) async fn mixnodes_basic_all( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + mixnodes_basic(state, query_params, false).await +} + +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with one of the mixing roles. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v2/unstable/nym-nodes/skimmed/mixnodes/active' instead")] +pub(crate) async fn mixnodes_basic_active( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + mixnodes_basic(state, query_params, true).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the entry role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/entry-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated] +pub(crate) async fn entry_gateways_basic_active( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + entry_gateways_basic(state, query_params, true).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support entry gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/entry-gateways/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v2/unstable/nym-nodes/skimmed/entry-gateways' instead")] +pub(crate) async fn entry_gateways_basic_all( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + entry_gateways_basic(state, query_params, false).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the exit role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/exit-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated] +pub(crate) async fn exit_gateways_basic_active( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + exit_gateways_basic(state, query_params, true).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support exit gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/exit-gateways/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +#[deprecated(note = "use '/v2/unstable/nym-nodes/skimmed/exit-gateways' instead")] +pub(crate) async fn exit_gateways_basic_all( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + exit_gateways_basic(state, query_params, false).await +} diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/helpers.rs b/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/helpers.rs new file mode 100644 index 0000000000..78bb32e1eb --- /dev/null +++ b/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/helpers.rs @@ -0,0 +1,66 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use crate::unstable_routes::v1::nym_nodes::helpers::NodesParams; +use crate::unstable_routes::v1::nym_nodes::skimmed::PaginatedSkimmedNodes; +use crate::unstable_routes::v2; +use axum::extract::{Query, State}; + +pub(crate) async fn nodes_basic( + state: State<AppState>, + Query(query_params): Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + Ok( + v2::nym_nodes::skimmed::helpers::nodes_basic( + state, + Query(query_params.into()), + active_only, + ) + .await? + .map(Into::into), + ) +} + +pub(crate) async fn mixnodes_basic( + state: State<AppState>, + Query(query_params): Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + Ok(v2::nym_nodes::skimmed::helpers::mixnodes_basic( + state, + Query(query_params.into()), + active_only, + ) + .await? + .map(Into::into)) +} + +pub(crate) async fn entry_gateways_basic( + state: State<AppState>, + Query(query_params): Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + Ok(v2::nym_nodes::skimmed::helpers::entry_gateways_basic( + state, + Query(query_params.into()), + active_only, + ) + .await? + .map(Into::into)) +} + +pub(crate) async fn exit_gateways_basic( + state: State<AppState>, + query_params: Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + Ok(v2::nym_nodes::skimmed::helpers::exit_gateways_basic( + state, + Query(query_params.0.into()), + active_only, + ) + .await? + .map(Into::into)) +} diff --git a/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/mod.rs b/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/mod.rs new file mode 100644 index 0000000000..703ae85320 --- /dev/null +++ b/nym-api/src/unstable_routes/v1/nym_nodes/skimmed/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumResult; +use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper; +use nym_api_requests::nym_nodes::{PaginatedCachedNodesResponseV1, SkimmedNode}; +use nym_api_requests::pagination::PaginatedResponse; +use nym_http_api_common::FormattedResponse; +use utoipa::ToSchema; + +pub(crate) mod handlers; +pub(crate) mod helpers; + +pub type PaginatedSkimmedNodes = + AxumResult<FormattedResponse<PaginatedCachedNodesResponseV1<SkimmedNode>>>; + +pub(crate) use handlers::*; + +#[allow(dead_code)] // not dead, used in OpenAPI docs +#[derive(ToSchema)] +#[schema(title = "PaginatedCachedNodesResponse")] +pub struct PaginatedCachedNodesResponseSchema { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + #[schema(value_type = SkimmedNode)] + pub nodes: PaginatedResponse<SkimmedNode>, +} diff --git a/nym-api/src/unstable_routes/v2/mod.rs b/nym-api/src/unstable_routes/v2/mod.rs new file mode 100644 index 0000000000..bda90bac78 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use axum::Router; + +pub(crate) mod nym_nodes; + +pub(crate) fn unstable_routes_v2() -> Router<AppState> { + Router::new().nest("/nym-nodes", nym_nodes::routes()) +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/helpers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/helpers.rs new file mode 100644 index 0000000000..9a24232a43 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/nym_nodes/helpers.rs @@ -0,0 +1,93 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::helpers::PaginationRequest; +use crate::unstable_routes::v1; +use nym_api_requests::nym_nodes::NodeRoleQueryParam; +use nym_http_api_common::Output; +use serde::Deserialize; + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +pub(crate) struct NodesParamsWithRole { + #[param(inline)] + pub(crate) role: Option<NodeRoleQueryParam>, + + #[allow(dead_code)] + pub(crate) semver_compatibility: Option<String>, + pub(crate) no_legacy: Option<bool>, + pub(crate) page: Option<u32>, + pub(crate) per_page: Option<u32>, + + // Identifier for the current epoch of the topology state. When sent by a client we can check if + // the client already knows about the latest topology state, allowing a `no-updates` response + // instead of wasting bandwidth serving an unchanged topology. + pub(crate) epoch_id: Option<u32>, + + pub(crate) output: Option<Output>, +} + +impl From<v1::nym_nodes::helpers::NodesParamsWithRole> for NodesParamsWithRole { + fn from(value: v1::nym_nodes::helpers::NodesParamsWithRole) -> Self { + NodesParamsWithRole { + role: value.role, + semver_compatibility: value.semver_compatibility, + no_legacy: value.no_legacy, + page: value.page, + per_page: value.per_page, + epoch_id: value.epoch_id, + output: value.output, + } + } +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct NodesParams { + #[allow(dead_code)] + pub(crate) semver_compatibility: Option<String>, + pub(crate) no_legacy: Option<bool>, + pub(crate) page: Option<u32>, + pub(crate) per_page: Option<u32>, + + // Identifier for the current epoch of the topology state. When sent by a client we can check if + // the client already knows about the latest topology state, allowing a `no-updates` response + // instead of wasting bandwidth serving an unchanged topology. + pub(crate) epoch_id: Option<u32>, + pub(crate) output: Option<Output>, +} + +impl From<v1::nym_nodes::helpers::NodesParams> for NodesParams { + fn from(value: v1::nym_nodes::helpers::NodesParams) -> Self { + NodesParams { + semver_compatibility: value.semver_compatibility, + no_legacy: value.no_legacy, + page: value.page, + per_page: value.per_page, + epoch_id: value.epoch_id, + output: value.output, + } + } +} + +impl From<NodesParamsWithRole> for NodesParams { + fn from(params: NodesParamsWithRole) -> Self { + NodesParams { + semver_compatibility: params.semver_compatibility, + no_legacy: params.no_legacy, + page: params.page, + per_page: params.per_page, + epoch_id: params.epoch_id, + output: params.output, + } + } +} + +impl<'a> From<&'a NodesParams> for PaginationRequest { + fn from(params: &'a NodesParams) -> Self { + PaginationRequest { + output: params.output, + page: params.page, + per_page: params.per_page, + } + } +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/mod.rs new file mode 100644 index 0000000000..e3cc67ef84 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/nym_nodes/mod.rs @@ -0,0 +1,39 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::http::state::AppState; +use crate::unstable_routes::v2::nym_nodes::semi_skimmed::nodes_expanded; +use crate::unstable_routes::v2::nym_nodes::skimmed::{ + entry_gateways_basic_all, exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, + nodes_basic_all, +}; +use axum::routing::get; +use axum::Router; +use tower_http::compression::CompressionLayer; + +pub(crate) mod helpers; +pub(crate) mod semi_skimmed; +pub(crate) mod skimmed; + +#[allow(deprecated)] +pub(crate) fn routes() -> Router<AppState> { + Router::new() + .nest( + "/skimmed", + Router::new() + .route("/", get(nodes_basic_all)) + .nest( + "/mixnodes", + Router::new() + .route("/active", get(mixnodes_basic_active)) + .route("/all", get(mixnodes_basic_all)), + ) + .route("/entry-gateways", get(entry_gateways_basic_all)) + .route("/exit-gateways", get(exit_gateways_basic_all)), + ) + .nest( + "/semi-skimmed", + Router::new().route("/", get(nodes_expanded)), + ) + .layer(CompressionLayer::new()) +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs new file mode 100644 index 0000000000..69cc2dea45 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs @@ -0,0 +1,177 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use crate::unstable_routes::helpers::{refreshed_at, LegacyAnnotation}; +use crate::unstable_routes::v2::nym_nodes::helpers::NodesParamsWithRole; +use axum::extract::{Query, State}; +use nym_api_requests::models::{ + NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponseV2, SemiSkimmedNode}; +use nym_api_requests::pagination::PaginatedResponse; +use nym_http_api_common::FormattedResponse; +use nym_mixnet_contract_common::NodeId; +use nym_topology::CachedEpochRewardedSet; +use std::collections::HashMap; +use tracing::trace; +use utoipa::ToSchema; + +pub type PaginatedSemiSkimmedNodes = + AxumResult<FormattedResponse<PaginatedCachedNodesResponseV2<SemiSkimmedNode>>>; + +//SW TODO : this is copied from skimmed nodes, surely we can do better than that +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedEpochRewardedSet, + nym_nodes_subset: NI, + annotations: &HashMap<NodeId, NodeAnnotation>, + current_key_rotation: u32, + active_only: bool, +) -> Vec<SemiSkimmedNode> +where + NI: Iterator<Item = &'a NymNodeDescription> + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_semi_skimmed_node( + current_key_rotation, + role, + annotation.last_24h_performance, + )); + } + nodes +} + +//SW TODO : this is copied from skimmed nodes, surely we can do better than that +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy<LN>( + nodes: &mut Vec<SemiSkimmedNode>, + rewarded_set: &CachedEpochRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap<NodeId, LN>, + current_key_rotation: u32, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_semi_skimmed_node( + current_key_rotation, + role, + legacy.performance(), + )) + } else { + match legacy.try_to_semi_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +#[allow(dead_code)] // not dead, used in OpenAPI docs +#[derive(ToSchema)] +#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")] +pub struct PaginatedCachedNodesExpandedResponseSchema { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + #[schema(value_type = SemiSkimmedNode)] + pub nodes: PaginatedResponse<SemiSkimmedNode>, +} + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "", + context_path = "/v2/unstable/nym-nodes/semi-skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesExpandedResponseSchema = "application/json"), + (PaginatedCachedNodesExpandedResponseSchema = "application/yaml"), + (PaginatedCachedNodesExpandedResponseSchema = "application/bincode") + )) + ) +)] +pub(super) async fn nodes_expanded( + state: State<AppState>, + query_params: Query<NodesParamsWithRole>, +) -> PaginatedSemiSkimmedNodes { + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let contract_cache = state.nym_contract_cache(); + let current_key_rotation = contract_cache.current_key_rotation_id().await?; + let interval = contract_cache.current_interval().await?; + + let mut nodes = build_nym_nodes_response( + &rewarded_set, + all_nym_nodes, + &annotations, + current_key_rotation, + false, + ); + + // add legacy gateways to the response + add_legacy( + &mut nodes, + &rewarded_set, + &describe_cache, + &legacy_gateways, + current_key_rotation, + ); + + // add legacy mixnodes to the response + add_legacy( + &mut nodes, + &rewarded_set, + &describe_cache, + &legacy_mixnodes, + current_key_rotation, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + let output = query_params.output.unwrap_or_default(); + Ok(output.to_response(PaginatedCachedNodesResponseV2::new_full( + interval.current_epoch_absolute_id(), + current_key_rotation, + refreshed_at, + nodes, + ))) +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/handlers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/handlers.rs new file mode 100644 index 0000000000..5eb47bae02 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/handlers.rs @@ -0,0 +1,142 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use crate::unstable_routes::v2::nym_nodes::helpers::{NodesParams, NodesParamsWithRole}; +use crate::unstable_routes::v2::nym_nodes::skimmed::helpers::{ + entry_gateways_basic, exit_gateways_basic, mixnodes_basic, nodes_basic, +}; +use crate::unstable_routes::v2::nym_nodes::skimmed::{ + PaginatedCachedNodesResponseSchema, PaginatedSkimmedNodes, +}; +use axum::extract::{Query, State}; +use nym_api_requests::nym_nodes::NodeRoleQueryParam; + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "", + context_path = "/v2/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +pub(crate) async fn nodes_basic_all( + state: State<AppState>, + Query(query_params): Query<NodesParamsWithRole>, +) -> PaginatedSkimmedNodes { + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_all(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_all(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_all(state, Query(query_params.into())).await + } + }; + } + + nodes_basic(state, Query(query_params.into()), false).await +} + +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and support mixing role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/all", + context_path = "/v2/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +pub(crate) async fn mixnodes_basic_all( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + mixnodes_basic(state, query_params, false).await +} + +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with one of the mixing roles. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/active", + context_path = "/v2/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +pub(crate) async fn mixnodes_basic_active( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + mixnodes_basic(state, query_params, true).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support entry gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/entry-gateways", + context_path = "/v2/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +pub(crate) async fn entry_gateways_basic_all( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + entry_gateways_basic(state, query_params, false).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support exit gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/exit-gateways", + context_path = "/v2/unstable/nym-nodes/skimmed", + responses( + (status = 200, content( + (PaginatedCachedNodesResponseSchema = "application/json"), + (PaginatedCachedNodesResponseSchema = "application/yaml"), + (PaginatedCachedNodesResponseSchema = "application/bincode") + )) + ), +)] +pub(crate) async fn exit_gateways_basic_all( + state: State<AppState>, + query_params: Query<NodesParams>, +) -> PaginatedSkimmedNodes { + exit_gateways_basic(state, query_params, false).await +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs new file mode 100644 index 0000000000..1f1fb39203 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs @@ -0,0 +1,365 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_describe_cache::cache::DescribedNodes; +use crate::node_status_api::models::AxumErrorResponse; +use crate::support::caching::Cache; +use crate::support::http::state::AppState; +use crate::unstable_routes::helpers::{refreshed_at, LegacyAnnotation}; +use crate::unstable_routes::v2::nym_nodes::helpers::NodesParams; +use crate::unstable_routes::v2::nym_nodes::skimmed::PaginatedSkimmedNodes; +use axum::extract::{Query, State}; +use nym_api_requests::models::{ + NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponseV2, SkimmedNode}; +use nym_http_api_common::Output; +use nym_mixnet_contract_common::{Interval, NodeId}; +use nym_topology::CachedEpochRewardedSet; +use std::collections::HashMap; +use std::future::Future; +use std::time::Duration; +use tokio::sync::RwLockReadGuard; +use tracing::trace; + +/// Given all relevant caches, build part of response for JUST Nym Nodes +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedEpochRewardedSet, + nym_nodes_subset: NI, + annotations: &HashMap<NodeId, NodeAnnotation>, + current_key_rotation: u32, + active_only: bool, +) -> Vec<SkimmedNode> +where + NI: Iterator<Item = &'a NymNodeDescription> + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node( + current_key_rotation, + role, + annotation.last_24h_performance, + )); + } + nodes +} + +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy<LN>( + nodes: &mut Vec<SkimmedNode>, + rewarded_set: &CachedEpochRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap<NodeId, LN>, + current_key_rotation: u32, + active_only: bool, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + // legacy nodes don't support key rotation + nodes.push(described.to_skimmed_node(current_key_rotation, role, legacy.performance())) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +fn maybe_add_expires_header( + output: Output, + interval: Interval, + current_key_rotation: u32, + refreshed_at: OffsetDateTimeJsonSchemaWrapper, + nodes: Vec<SkimmedNode>, + active_only: bool, +) -> PaginatedSkimmedNodes { + let base_response = output.to_response( + PaginatedCachedNodesResponseV2::new_full( + interval.current_epoch_absolute_id(), + current_key_rotation, + refreshed_at, + nodes, + ) + .fresh(interval), + ); + + if !active_only { + return Ok(base_response); + } + + // if caller requested only active nodes, the response is valid until the epoch changes + // (but add 2 minutes due to epoch transition not being instantaneous + let epoch_end = interval.current_epoch_end(); + let expiration = epoch_end + Duration::from_secs(120); + Ok(base_response.with_expires_header(expiration)) +} + +// hehe, what an abomination, but it's used in multiple different places and I hate copy-pasting code, +// especially if it has multiple loops, etc +pub(crate) async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( + state: &'a AppState, + Query(query_params): Query<NodesParams>, + nym_nodes_subset: NI, + annotated_legacy_nodes_getter: LG, + active_only: bool, + output: Output, +) -> PaginatedSkimmedNodes +where + // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) + NI: Iterator<Item = &'a NymNodeDescription> + 'a, + + // async function that returns cache of appropriate legacy nodes (mixnodes or gateways) + LG: Fn(&'a AppState) -> Fut, + Fut: + Future<Output = Result<RwLockReadGuard<'a, Cache<HashMap<NodeId, LN>>>, AxumErrorResponse>>, + + // the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated) + LN: LegacyAnnotation + 'a, +{ + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. implicitly grab the relevant described nodes + // (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler) + let describe_cache = state.describe_nodes_cache_data().await?; + + let contract_cache = state.nym_contract_cache(); + + let interval = contract_cache.current_interval().await?; + let current_key_rotation = contract_cache.current_key_rotation_id().await?; + + // 4.0 If the client indicates that they already know about the current topology send empty response + if let Some(client_known_epoch) = query_params.epoch_id { + if client_known_epoch == interval.current_epoch_id() { + return Ok( + output.to_response(PaginatedCachedNodesResponseV2::no_updates( + interval.current_epoch_absolute_id(), + current_key_rotation, + )), + ); + } + } + + // 4. start building the response + let mut nodes = build_nym_nodes_response( + &rewarded_set, + nym_nodes_subset, + &annotations, + current_key_rotation, + active_only, + ); + + // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes + if let Some(true) = query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return maybe_add_expires_header( + output, + interval, + current_key_rotation, + refreshed_at, + nodes, + active_only, + ); + } + + // 6. grab relevant legacy nodes + // (due to the existence of the legacy endpoints, we already have fully annotated data on them) + let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; + add_legacy( + &mut nodes, + &rewarded_set, + &describe_cache, + &annotated_legacy_nodes, + current_key_rotation, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_nodes.timestamp(), + ]); + + maybe_add_expires_header( + output, + interval, + current_key_rotation, + refreshed_at, + nodes, + active_only, + ) +} + +pub(crate) async fn nodes_basic( + state: State<AppState>, + Query(query_params): Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + + // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes + + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let interval = state.nym_contract_cache().current_interval().await?; + let current_key_rotation = state.nym_contract_cache().current_key_rotation_id().await?; + + let mut nodes = build_nym_nodes_response( + &rewarded_set, + all_nym_nodes, + &annotations, + current_key_rotation, + active_only, + ); + + // add legacy gateways to the response + add_legacy( + &mut nodes, + &rewarded_set, + &describe_cache, + &legacy_gateways, + current_key_rotation, + active_only, + ); + + // add legacy mixnodes to the response + add_legacy( + &mut nodes, + &rewarded_set, + &describe_cache, + &legacy_mixnodes, + current_key_rotation, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + Ok(output.to_response(PaginatedCachedNodesResponseV2::new_full( + interval.current_epoch_absolute_id(), + current_key_rotation, + refreshed_at, + nodes, + ))) +} + +pub(crate) async fn mixnodes_basic( + state: State<AppState>, + query_params: Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + + // 1. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_mixnode_annotations(), + active_only, + output, + ) + .await +} + +pub(crate) async fn entry_gateways_basic( + state: State<AppState>, + query_params: Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + + // 1. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes(); + + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + output, + ) + .await +} + +pub(crate) async fn exit_gateways_basic( + state: State<AppState>, + query_params: Query<NodesParams>, + active_only: bool, +) -> PaginatedSkimmedNodes { + let output = query_params.output.unwrap_or_default(); + + // 1. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes(); + + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + output, + ) + .await +} diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/mod.rs new file mode 100644 index 0000000000..bda0975342 --- /dev/null +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::AxumResult; +use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper; +use nym_api_requests::nym_nodes::{PaginatedCachedNodesResponseV2, SkimmedNode}; +use nym_api_requests::pagination::PaginatedResponse; +use nym_http_api_common::FormattedResponse; +use utoipa::ToSchema; + +pub(crate) mod handlers; +pub(crate) mod helpers; + +pub type PaginatedSkimmedNodes = + AxumResult<FormattedResponse<PaginatedCachedNodesResponseV2<SkimmedNode>>>; + +pub(crate) use handlers::*; + +#[allow(dead_code)] // not dead, used in OpenAPI docs +#[derive(ToSchema)] +#[schema(title = "PaginatedCachedNodesResponse")] +pub struct PaginatedCachedNodesResponseSchema { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + #[schema(value_type = SkimmedNode)] + pub nodes: PaginatedResponse<SkimmedNode>, +} diff --git a/nym-api/tests/public-api/api_status.rs b/nym-api/tests/public-api/api_status.rs index f6e6230ff2..e06f42ea5c 100644 --- a/nym-api/tests/public-api/api_status.rs +++ b/nym-api/tests/public-api/api_status.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_health() -> Result<(), String> { let url = format!("{}/v1/api-status/health", base_url()?); let res = make_request(&url).await?; @@ -11,6 +12,7 @@ async fn test_health() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_build_information() -> Result<(), String> { let url = format!("{}/v1/api-status/build-information", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/circulating_supply.rs b/nym-api/tests/public-api/circulating_supply.rs index 105c48dd07..0f112f0595 100644 --- a/nym-api/tests/public-api/circulating_supply.rs +++ b/nym-api/tests/public-api/circulating_supply.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_circulating_supply() -> Result<(), String> { let url = format!("{}/v1/circulating-supply", base_url()?); let res = make_request(&url).await?; @@ -14,6 +15,7 @@ async fn test_get_circulating_supply() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_circulating_supply_value() -> Result<(), String> { let url = format!( "{}/v1/circulating-supply/circulating-supply-value", @@ -32,6 +34,7 @@ async fn test_get_circulating_supply_value() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_total_supply_value() -> Result<(), String> { let url = format!("{}/v1/circulating-supply/total-supply-value", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/contract_cache.rs b/nym-api/tests/public-api/contract_cache.rs index 8510f30d95..0d93bda1f8 100644 --- a/nym-api/tests/public-api/contract_cache.rs +++ b/nym-api/tests/public-api/contract_cache.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_current_epoch() -> Result<(), String> { let url = format!("{}/v1/epoch/current", base_url()?); let res = make_request(&url).await?; @@ -19,6 +20,7 @@ async fn test_get_current_epoch() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_reward_params() -> Result<(), String> { let url = format!("{}/v1/epoch/reward_params", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/network.rs b/nym-api/tests/public-api/network.rs index 62f2440011..6b23f8df7d 100644 --- a/nym-api/tests/public-api/network.rs +++ b/nym-api/tests/public-api/network.rs @@ -1,13 +1,14 @@ use crate::utils::{base_url, test_client, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_chain_status() -> Result<(), String> { let url = format!("{}/v1/network/chain-status", base_url()?); let res = test_client() .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; let block_header = json @@ -29,13 +30,14 @@ async fn test_get_chain_status() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_network_details() -> Result<(), String> { let url = format!("{}/v1/network/details", base_url()?); let res = test_client() .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; assert!( @@ -54,13 +56,14 @@ async fn test_get_network_details() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_nym_contracts() -> Result<(), String> { let url = format!("{}/v1/network/nym-contracts", base_url()?); let res = test_client() .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; assert!( @@ -75,13 +78,14 @@ async fn test_get_nym_contracts() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_nym_contracts_detailed() -> Result<(), String> { let url = format!("{}/v1/network/nym-contracts-detailed", base_url()?); let res = test_client() .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; let mixnet_contract = json diff --git a/nym-api/tests/public-api/nym_nodes.rs b/nym-api/tests/public-api/nym_nodes.rs index 63ff057d45..35d2eaf0ea 100644 --- a/nym-api/tests/public-api/nym_nodes.rs +++ b/nym-api/tests/public-api/nym_nodes.rs @@ -1,7 +1,8 @@ use crate::utils::{base_url, get_any_node_id, make_request, test_client, validate_json_response}; -use chrono::Utc; +use time::OffsetDateTime; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_bonded_nodes() -> Result<(), String> { let url = format!("{}/v1/nym-nodes/bonded", base_url()?); let res = make_request(&url).await?; @@ -19,6 +20,7 @@ async fn test_get_bonded_nodes() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_described_nodes() -> Result<(), String> { let url = format!("{}/v1/nym-nodes/described", base_url()?); let res = make_request(&url).await?; @@ -37,13 +39,14 @@ async fn test_get_described_nodes() -> Result<(), String> { // TODO enable this once noise is properly integrated // #[tokio::test] +#[test_with::env(NYM_API)] // async fn test_get_noise() -> Result<(), String> { // let url = format!("{}/v1/nym-nodes/noise", base_url()?); // let res = test_client().get(&url).send().await.map_err(|err| panic!("Failed to send request to {}: {}", url, err))?; // let json = validate_json_response(res).await; // } - #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_rewarded_set() -> Result<(), String> { let url = format!("{}/v1/nym-nodes/rewarded-set", base_url()?); let res = make_request(&url).await?; @@ -64,6 +67,7 @@ async fn test_get_rewarded_set() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_annotation_for_node() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/annotation/{}", base_url()?, id); @@ -81,16 +85,17 @@ async fn test_get_annotation_for_node() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_historical_performance() -> Result<(), String> { let id = get_any_node_id().await?; - let date = Utc::now().date_naive().to_string(); + let date = OffsetDateTime::now_utc().date().to_string(); let url = format!("{}/v1/nym-nodes/historical-performance/{}", base_url()?, id); let res = test_client() .get(&url) .query(&[("date", date)]) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json = validate_json_response(res).await?; assert!( @@ -101,6 +106,7 @@ async fn test_get_historical_performance() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_performance_history() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/performance-history/{}", base_url()?, id); @@ -120,6 +126,7 @@ async fn test_get_performance_history() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_performance() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/performance/{}", base_url()?, id); @@ -138,6 +145,7 @@ async fn test_get_performance() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_uptime_history() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/uptime-history/{}", base_url()?, id); diff --git a/nym-api/tests/public-api/status.rs b/nym-api/tests/public-api/status.rs index d0631fb68f..36510f3360 100644 --- a/nym-api/tests/public-api/status.rs +++ b/nym-api/tests/public-api/status.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_config_score_details() -> Result<(), String> { let url = format!("{}/v1/status/config-score-details", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/unstable_nym_nodes.rs b/nym-api/tests/public-api/unstable_nym_nodes.rs index e83aea6297..542ddbb35e 100644 --- a/nym-api/tests/public-api/unstable_nym_nodes.rs +++ b/nym-api/tests/public-api/unstable_nym_nodes.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_nodes_active() -> Result<(), String> { let url = format!("{}/v1/unstable/nym-nodes/skimmed/active", base_url()?); let res = make_request(&url).await?; @@ -19,6 +20,7 @@ async fn test_get_skimmed_nodes_active() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_active_mixnodes() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/mixnodes/active", @@ -44,6 +46,7 @@ async fn test_get_skimmed_active_mixnodes() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_all_mixnodes() -> Result<(), String> { let url = format!("{}/v1/unstable/nym-nodes/skimmed/mixnodes/all", base_url()?); let res = make_request(&url).await?; @@ -66,6 +69,7 @@ async fn test_get_skimmed_all_mixnodes() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_active_exit_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/exit-gateways/active", @@ -91,6 +95,7 @@ async fn test_get_skimmed_active_exit_gateways() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_all_exit_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/exit-gateways/all", @@ -116,6 +121,7 @@ async fn test_get_skimmed_all_exit_gateways() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_active_entry_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/entry-gateways/active", @@ -141,6 +147,7 @@ async fn test_get_skimmed_active_entry_gateways() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_all_entry_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/entry-gateways/all", diff --git a/nym-api/tests/public-api/unstable_status.rs b/nym-api/tests/public-api/unstable_status.rs index fe36329646..6ebbb476ca 100644 --- a/nym-api/tests/public-api/unstable_status.rs +++ b/nym-api/tests/public-api/unstable_status.rs @@ -4,6 +4,7 @@ use crate::utils::{ }; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_gateway_unstable_test_results() -> Result<(), String> { let identity = get_gateway_identity_key().await?; let url = format!( @@ -35,6 +36,7 @@ async fn test_get_gateway_unstable_test_results() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_mixnode_unstable_test_results() -> Result<(), String> { let mix_id = get_mixnode_node_id().await?; let url = format!( @@ -66,6 +68,7 @@ async fn test_get_mixnode_unstable_test_results() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_latest_network_monitor_run_details() -> Result<(), String> { let url = format!( "{}/v1/status/network-monitor/unstable/run/latest/details", @@ -87,7 +90,7 @@ async fn test_get_latest_network_monitor_run_details() -> Result<(), String> { .get(&follow_up_url) .send() .await - .map_err(|err| format!("Failed to follow up with URL {}: {}", follow_up_url, err))?; + .map_err(|err| format!("Failed to follow up with URL {follow_up_url}: {err}"))?; assert!(follow_up_res.status().is_success()); Ok(()) } diff --git a/nym-api/tests/public-api/utils.rs b/nym-api/tests/public-api/utils.rs index 96c8c3b61e..fb697945ff 100644 --- a/nym-api/tests/public-api/utils.rs +++ b/nym-api/tests/public-api/utils.rs @@ -1,4 +1,4 @@ -use dotenv::dotenv; +use dotenvy::dotenv; use reqwest::{Client, Response}; use serde_json::Value; @@ -25,7 +25,7 @@ pub async fn make_request(url: &str) -> Result<Response, String> { .get(url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; if res.status().is_success() { Ok(res) @@ -43,7 +43,7 @@ pub async fn validate_json_response(res: Response) -> Result<Value, String> { res.json::<Value>() .await - .map_err(|err| format!("Invalid JSON response: {}", err)) + .map_err(|err| format!("Invalid JSON response: {err}")) } #[allow(dead_code)] @@ -54,11 +54,11 @@ pub async fn get_any_node_id() -> Result<String, String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json: Value = res .json() .await - .map_err(|err| format!("Failed to parse response as JSON: {}", err))?; + .map_err(|err| format!("Failed to parse response as JSON: {err}"))?; let id = json .get("data") @@ -80,11 +80,11 @@ pub async fn get_mixnode_node_id() -> Result<u64, String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json: Value = res .json() .await - .map_err(|err| format!("Failed to parse response as JSON: {}", err))?; + .map_err(|err| format!("Failed to parse response as JSON: {err}"))?; json.get("data") .and_then(|v| v.as_array()) @@ -110,11 +110,11 @@ pub async fn get_gateway_identity_key() -> Result<String, String> { .get(&url) .send() .await - .map_err(|err| format!("Failed to send request to {}: {}", url, err))?; + .map_err(|err| format!("Failed to send request to {url}: {err}"))?; let json: Value = res .json() .await - .map_err(|err| format!("Failed to parse response as JSON: {}", err))?; + .map_err(|err| format!("Failed to parse response as JSON: {err}"))?; let key = json .get("data") diff --git a/nym-authenticator-client/Cargo.toml b/nym-authenticator-client/Cargo.toml new file mode 100644 index 0000000000..35a72ae019 --- /dev/null +++ b/nym-authenticator-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-authenticator-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +bincode.workspace = true +futures.workspace = true +semver.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +tokio.workspace = true +tracing.workspace = true + +nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } +nym-wireguard-types = { path = "../common/wireguard-types" } diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs new file mode 100644 index 0000000000..7bb0cd4dc4 --- /dev/null +++ b/nym-authenticator-client/src/error.rs @@ -0,0 +1,42 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("mixnet client stopped returning responses")] + NoMixnetMessagesReceived, + + #[error("failed to get version from message")] + NoVersionInMessage, + + #[error( + "received response with version v{received}, the client is too new and can only understand v{expected}" + )] + ReceivedResponseWithOldVersion { expected: u8, received: u8 }, + + #[error( + "received response with version v{received}, the client is too old and can only understand v{expected}" + )] + ReceivedResponseWithNewVersion { expected: u8, received: u8 }, + + #[error("failed to send mixnet message")] + SendMixnetMessage(#[source] Box<nym_sdk::Error>), + + #[error("timeout waiting for connect response from exit gateway (authenticator)")] + TimeoutWaitingForConnectResponse, + + #[error("unable to get mixnet handle when sending authenticator message")] + UnableToGetMixnetHandle, + + #[error("unknown version number")] + UnknownVersion, + + #[error(transparent)] + Bincode(#[from] bincode::Error), + + #[error("gateway doesn't support this type of message")] + UnsupportedMessage, + + #[error(transparent)] + AuthenticatorRequests(#[from] nym_authenticator_requests::Error), +} + +// Result type based on our error type +pub type Result<T> = std::result::Result<T, Error>; diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs new file mode 100644 index 0000000000..03112e1651 --- /dev/null +++ b/nym-authenticator-client/src/lib.rs @@ -0,0 +1,1232 @@ +use std::{ + fmt, + net::{Ipv4Addr, Ipv6Addr}, + time::Duration, +}; + +use nym_authenticator_requests::{ + v2, v3, v4, + v5::{self, registration::IpPair}, +}; + +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519::PrivateKey; +use nym_sdk::mixnet::{ + IncludedSurbs, MixnetClientSender, MixnetMessageSender, Recipient, ReconstructedMessage, + TransmissionLane, +}; +use nym_service_provider_requests_common::ServiceProviderType; +use nym_wireguard_types::PeerPublicKey; +use tracing::{debug, error}; + +mod error; +mod mixnet_listener; + +pub use crate::{ + error::{Error, Result}, + mixnet_listener::{ + AuthClientMixnetListener, AuthClientMixnetListenerHandle, MixnetMessageBroadcastReceiver, + }, +}; + +pub trait Versionable { + fn version(&self) -> AuthenticatorVersion; +} + +impl Versionable for v2::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V2 + } +} + +impl Versionable for v3::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +impl Versionable for v2::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V2 + } +} + +impl Versionable for v3::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +impl Versionable for PeerPublicKey { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v3::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +pub trait InitMessage: Versionable + fmt::Debug { + fn pub_key(&self) -> PeerPublicKey; +} + +impl InitMessage for v2::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v3::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v4::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v5::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait FinalMessage: Versionable + fmt::Debug { + fn gateway_client_pub_key(&self) -> PeerPublicKey; + fn gateway_client_ipv4(&self) -> Option<Ipv4Addr>; + fn gateway_client_ipv6(&self) -> Option<Ipv6Addr>; + fn gateway_client_mac(&self) -> Vec<u8>; + fn credential(&self) -> Option<CredentialSpendingData>; +} + +impl FinalMessage for v2::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> { + match self.gateway_client.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> { + None + } + + fn gateway_client_mac(&self) -> Vec<u8> { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option<CredentialSpendingData> { + self.credential.clone() + } +} + +impl FinalMessage for v3::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> { + match self.gateway_client.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> { + None + } + + fn gateway_client_mac(&self) -> Vec<u8> { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option<CredentialSpendingData> { + self.credential.clone() + } +} + +impl FinalMessage for v4::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec<u8> { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option<CredentialSpendingData> { + self.credential.clone() + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec<u8> { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option<CredentialSpendingData> { + self.credential.clone() + } +} + +// Temporary solution for lacking a query message wrapper in monorepo +#[derive(Debug)] +pub struct QueryMessageImpl { + pub pub_key: PeerPublicKey, + pub version: AuthenticatorVersion, +} + +impl Versionable for QueryMessageImpl { + fn version(&self) -> AuthenticatorVersion { + self.version + } +} + +pub trait QueryMessage: Versionable + fmt::Debug { + fn pub_key(&self) -> PeerPublicKey; +} + +impl QueryMessage for QueryMessageImpl { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait TopUpMessage: Versionable + fmt::Debug { + fn pub_key(&self) -> PeerPublicKey; + fn credential(&self) -> CredentialSpendingData; +} + +impl TopUpMessage for v3::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +impl TopUpMessage for v4::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +impl TopUpMessage for v5::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +#[derive(Debug)] +pub enum ClientMessage { + Initial(Box<dyn InitMessage + Send + Sync + 'static>), + Final(Box<dyn FinalMessage + Send + Sync + 'static>), + Query(Box<dyn QueryMessage + Send + Sync + 'static>), + TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>), +} + +impl ClientMessage { + // check if message is wasteful e.g. contains a credential + pub fn is_wasteful(&self) -> bool { + match self { + Self::Final(msg) => msg.credential().is_some(), + Self::TopUp(_) => true, + Self::Initial(_) | Self::Query(_) => false, + } + } + + fn version(&self) -> AuthenticatorVersion { + match self { + ClientMessage::Initial(msg) => msg.version(), + ClientMessage::Final(msg) => msg.version(), + ClientMessage::Query(msg) => msg.version(), + ClientMessage::TopUp(msg) => msg.version(), + } + } + + pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec<u8>, u64)> { + match self.version() { + AuthenticatorVersion::V2 => { + use v2::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request( + query_message.pub_key(), + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + AuthenticatorVersion::V3 => { + use v3::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request( + query_message.pub_key(), + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + } + } + AuthenticatorVersion::V4 => { + use v4::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + } + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request( + query_message.pub_key(), + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + } + } + AuthenticatorVersion::V5 => { + use v5::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok((req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok((req.to_bytes()?, id)) + } + } + } + AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), + } + } + + pub fn use_surbs(&self) -> bool { + match self.version() { + AuthenticatorVersion::V2 | AuthenticatorVersion::V3 | AuthenticatorVersion::V4 => false, + AuthenticatorVersion::V5 => true, + AuthenticatorVersion::UNKNOWN => true, + } + } +} + +pub trait Id { + fn id(&self) -> u64; +} + +impl Id for v2::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v2::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v2::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +pub trait PendingRegistrationResponse: Id + fmt::Debug { + fn nonce(&self) -> u64; + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error>; + fn pub_key(&self) -> PeerPublicKey; + fn private_ips(&self) -> IpPair; +} + +impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ip.into() + } +} + +impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ip.into() + } +} + +impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ips.into() + } +} + +impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ips + } +} + +pub trait RegisteredResponse: Id + fmt::Debug { + fn private_ips(&self) -> IpPair; + fn pub_key(&self) -> PeerPublicKey; + fn wg_port(&self) -> u16; +} + +impl RegisteredResponse for v2::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ip.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v3::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ip.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} +impl RegisteredResponse for v4::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v5::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +pub trait RemainingBandwidthResponse: Id + fmt::Debug { + fn available_bandwidth(&self) -> Option<i64>; +} + +impl RemainingBandwidthResponse for v2::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option<i64> { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v3::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option<i64> { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v4::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option<i64> { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option<i64> { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +pub trait TopUpBandwidthResponse: Id + fmt::Debug { + fn available_bandwidth(&self) -> i64; +} + +impl TopUpBandwidthResponse for v3::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +impl TopUpBandwidthResponse for v4::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +#[derive(Debug)] +pub enum AuthenticatorResponse { + PendingRegistration(Box<dyn PendingRegistrationResponse + Send + Sync + 'static>), + Registered(Box<dyn RegisteredResponse + Send + Sync + 'static>), + RemainingBandwidth(Box<dyn RemainingBandwidthResponse + Send + Sync + 'static>), + TopUpBandwidth(Box<dyn TopUpBandwidthResponse + Send + Sync + 'static>), +} + +impl Id for AuthenticatorResponse { + fn id(&self) -> u64 { + match self { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + pending_registration_response.id() + } + AuthenticatorResponse::Registered(registered_response) => registered_response.id(), + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + remaining_bandwidth_response.id() + } + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.id() + } + } + } +} + +impl From<v2::response::AuthenticatorResponse> for AuthenticatorResponse { + fn from(value: v2::response::AuthenticatorResponse) -> Self { + match value.data { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + } + } +} + +impl From<v3::response::AuthenticatorResponse> for AuthenticatorResponse { + fn from(value: v3::response::AuthenticatorResponse) -> Self { + match value.data { + v3::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v3::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v3::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v3::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +impl From<v4::response::AuthenticatorResponse> for AuthenticatorResponse { + fn from(value: v4::response::AuthenticatorResponse) -> Self { + match value.data { + v4::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v4::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v4::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +impl From<v5::response::AuthenticatorResponse> for AuthenticatorResponse { + fn from(value: v5::response::AuthenticatorResponse) -> Self { + match value.data { + v5::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v5::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum AuthenticatorVersion { + V2, + V3, + V4, + V5, + UNKNOWN, +} + +impl AuthenticatorVersion { + pub const LATEST: AuthenticatorVersion = AuthenticatorVersion::V5; +} + +impl fmt::Display for AuthenticatorVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V2 => write!(f, "v2"), + Self::V3 => write!(f, "v3"), + Self::V4 => write!(f, "v4"), + Self::V5 => write!(f, "v5"), + Self::UNKNOWN => write!(f, "unknown"), + } + } +} + +impl From<u8> for AuthenticatorVersion { + fn from(value: u8) -> Self { + if value == 2 { + Self::V2 + } else if value == 3 { + Self::V3 + } else if value == 4 { + Self::V4 + } else if value == 5 { + Self::V5 + } else { + Self::UNKNOWN + } + } +} + +impl From<&str> for AuthenticatorVersion { + fn from(value: &str) -> Self { + let Ok(semver) = semver::Version::parse(value) else { + return Self::UNKNOWN; + }; + + semver.into() + } +} + +impl From<Option<&String>> for AuthenticatorVersion { + fn from(value: Option<&String>) -> Self { + match value { + None => Self::UNKNOWN, + Some(value) => value.as_str().into(), + } + } +} + +impl From<String> for AuthenticatorVersion { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From<Option<String>> for AuthenticatorVersion { + fn from(value: Option<String>) -> Self { + value.as_ref().into() + } +} + +impl From<semver::Version> for AuthenticatorVersion { + fn from(semver: semver::Version) -> Self { + if semver.major < 1 { + return Self::UNKNOWN; + } + if semver.minor < 1 { + return Self::UNKNOWN; + } + if semver.minor == 1 && semver.patch == 9 { + return Self::V2; + } + if semver.minor == 1 && semver.patch >= 10 { + return Self::V3; + } + if semver.minor < 6 { + return Self::V4; + } + if semver.minor == 6 && semver.patch == 0 { + return Self::V4; + } + if semver.minor == 6 && semver.patch >= 1 { + return Self::V5; + } + Self::LATEST + } +} + +#[derive(Clone)] +pub struct AuthenticatorClient { + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, +} + +impl AuthenticatorClient { + pub fn new( + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self { + auth_mix_client, + auth_recipient, + auth_version, + } + } + + pub fn auth_recipient(&self) -> Recipient { + self.auth_recipient + } + + pub fn auth_version(&self) -> AuthenticatorVersion { + self.auth_version + } + + pub async fn send(&mut self, message: &ClientMessage) -> Result<AuthenticatorResponse> { + self.auth_mix_client + .send(message, self.auth_recipient) + .await + } +} + +pub struct AuthenticatorMixnetClient { + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetClientSender, + our_nym_address: Recipient, +} + +impl Clone for AuthenticatorMixnetClient { + fn clone(&self) -> Self { + Self { + mixnet_listener: self.mixnet_listener.resubscribe(), + mixnet_sender: self.mixnet_sender.clone(), + our_nym_address: self.our_nym_address, + } + } +} + +impl AuthenticatorMixnetClient { + pub async fn new( + mixnet_sender: MixnetClientSender, + mixnet_listener: MixnetMessageBroadcastReceiver, + our_nym_address: Recipient, + ) -> Self { + Self { + mixnet_listener, + mixnet_sender, + our_nym_address, + } + } + + pub async fn send( + &mut self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result<AuthenticatorResponse> { + self.send_inner(message, authenticator_address).await + } + + async fn send_inner( + &mut self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result<AuthenticatorResponse> { + let request_id = self + .send_connect_request(message, authenticator_address) + .await?; + + debug!("Waiting for reply..."); + self.listen_for_connect_response(request_id).await + } + + async fn send_connect_request( + &self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result<u64> { + let (data, request_id) = message.bytes(self.our_nym_address)?; + + // We use 20 surbs for the connect request because typically the + // authenticator mixnet client on the nym-node is configured to have a min + // threshold of 10 surbs that it reserves for itself to request additional + // surbs. + let surbs = if message.use_surbs() { + match &message { + ClientMessage::Initial(_) => IncludedSurbs::new(20), + _ => IncludedSurbs::new(1), + } + } else { + IncludedSurbs::ExposeSelfAddress + }; + let input_message = create_input_message(authenticator_address, data, surbs); + + self.mixnet_sender + .send(input_message) + .await + .map_err(|e| Error::SendMixnetMessage(Box::new(e)))?; + + Ok(request_id) + } + + async fn listen_for_connect_response( + &mut self, + request_id: u64, + ) -> Result<AuthenticatorResponse> { + let timeout = tokio::time::sleep(Duration::from_secs(10)); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = &mut timeout => { + error!("Timed out waiting for reply to connect request"); + return Err(Error::TimeoutWaitingForConnectResponse); + } + msg = self.mixnet_listener.recv() => match msg { + Err(_) => { + return Err(Error::NoMixnetMessagesReceived); + } + Ok(msg) => { + if !check_if_authenticator_message(&msg) { + debug!("Received non-authenticator message while waiting for connect response"); + continue; + } + // Confirm that the version is correct + let version = check_auth_message_version(&msg)?; + + // Then we deserialize the message + debug!("AuthClient: got message while waiting for connect response with version {version:?}"); + let ret: Result<AuthenticatorResponse> = match version { + AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), + }; + let Ok(response) = ret else { + // This is ok, it's likely just one of our self-pings + debug!("Failed to deserialize reconstructed message"); + continue; + }; + + if response.id() == request_id { + debug!("Got response with matching id"); + return Ok(response); + } + } + } + } + } + } +} + +fn check_if_authenticator_message(message: &ReconstructedMessage) -> bool { + if let Some(msg_type) = message.message.get(1) { + ServiceProviderType::Authenticator as u8 == *msg_type + } else { + false + } +} + +fn check_auth_message_version(message: &ReconstructedMessage) -> Result<AuthenticatorVersion> { + // Assuing it's an Authenticator message, it will have a version as its first byte + if let Some(&version) = message.message.first() { + Ok(version.into()) + } else { + Err(Error::NoVersionInMessage) + } +} + +fn create_input_message( + recipient: Recipient, + data: Vec<u8>, + surbs: IncludedSurbs, +) -> nym_sdk::mixnet::InputMessage { + match surbs { + IncludedSurbs::Amount(surbs) => nym_sdk::mixnet::InputMessage::new_anonymous( + recipient, + data, + surbs, + TransmissionLane::General, + None, + ), + IncludedSurbs::ExposeSelfAddress => nym_sdk::mixnet::InputMessage::new_regular( + recipient, + data, + TransmissionLane::General, + None, + ), + } +} diff --git a/nym-authenticator-client/src/mixnet_listener.rs b/nym-authenticator-client/src/mixnet_listener.rs new file mode 100644 index 0000000000..113148b8ce --- /dev/null +++ b/nym-authenticator-client/src/mixnet_listener.rs @@ -0,0 +1,113 @@ +// Copyright 2025 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +// To remove with the Registration Client PR +#![allow(clippy::unwrap_used)] + +use std::sync::Arc; + +use futures::StreamExt; +use nym_sdk::mixnet::{MixnetClient, ReconstructedMessage}; +use tokio::{sync::broadcast, task::JoinHandle}; +use tokio_util::sync::CancellationToken; + +use crate::AuthenticatorMixnetClient; + +pub type SharedMixnetClient = Arc<tokio::sync::Mutex<Option<MixnetClient>>>; +pub type MixnetMessageBroadcastSender = broadcast::Sender<Arc<ReconstructedMessage>>; +pub type MixnetMessageBroadcastReceiver = broadcast::Receiver<Arc<ReconstructedMessage>>; + +// The AuthClientsMixnetListener listens to mixnet messages and rebroadcasts them to the +// AuthClients, or whoever else is interested. +// While it is running, it has a lock on the shared mixnet client. This is the reason it's +// designed to be able to start and stop, so that the lock can be released when it's not needed. +// +// NOTE: this is potentially bit wasteful. Ideally we should have proper channels where the +// recipient only gets messages they're interested in. +pub struct AuthClientMixnetListener { + // The shared mixnet client that we're listening to + mixnet_client: SharedMixnetClient, + + // Broadcast channel for the messages that we re-broadcast to the AuthClients + message_broadcast: MixnetMessageBroadcastSender, + + // Listen to cancel from the outside world + shutdown_token: CancellationToken, +} + +impl AuthClientMixnetListener { + pub fn new(mixnet_client: SharedMixnetClient, shutdown_token: CancellationToken) -> Self { + let (message_broadcast, _) = broadcast::channel(100); + Self { + mixnet_client, + message_broadcast, + shutdown_token, + } + } + + pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { + self.message_broadcast.subscribe() + } + + async fn run(self) { + let mut mixnet_client = self.mixnet_client.lock().await.take().unwrap(); + self.shutdown_token + .run_until_cancelled(async { + while let Some(event) = mixnet_client.next().await { + if let Err(err) = self.message_broadcast.send(Arc::new(event)) { + tracing::error!("Failed to broadcast mixnet message: {err}"); + } + } + tracing::error!("Mixnet client stream ended unexpectedly"); + }) + .await; + self.mixnet_client.lock().await.replace(mixnet_client); + } + + pub fn start(self) -> AuthClientMixnetListenerHandle { + let mixnet_client = self.mixnet_client.clone(); + let message_broadcast = self.message_broadcast.clone(); + let handle = tokio::spawn(self.run()); + + AuthClientMixnetListenerHandle { + mixnet_client, + message_broadcast, + handle, + } + } +} + +pub struct AuthClientMixnetListenerHandle { + mixnet_client: SharedMixnetClient, + message_broadcast: MixnetMessageBroadcastSender, + handle: JoinHandle<()>, +} + +impl AuthClientMixnetListenerHandle { + /// Returns new `AuthClient` or `None` if `MixnetClient` is already moved from shared reference. + pub async fn new_auth_client(&self) -> Option<AuthenticatorMixnetClient> { + let mixnet_client_guard = self.mixnet_client.lock().await; + let mixnet_client_ref = mixnet_client_guard.as_ref()?; + let mixnet_sender = mixnet_client_ref.split_sender(); + let nym_address = *mixnet_client_ref.nym_address(); + + Some( + AuthenticatorMixnetClient::new( + mixnet_sender, + self.message_broadcast.subscribe(), + nym_address, + ) + .await, + ) + } + + pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { + self.message_broadcast.subscribe() + } + + pub async fn wait(self) { + if let Err(err) = self.handle.await { + tracing::error!("Error waiting for auth clients mixnet listener to stop: {err}"); + } + } +} diff --git a/nym-browser-extension/.eslintrc b/nym-browser-extension/.eslintrc deleted file mode 100644 index 3ea1281af1..0000000000 --- a/nym-browser-extension/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": [ - "@nymproject/eslint-config-react-typescript" - ], - "parserOptions": { - "project": "./tsconfig.eslint.json" - } -} diff --git a/nym-browser-extension/.gitignore b/nym-browser-extension/.gitignore index fdfd79ec62..19ec59dff0 100644 --- a/nym-browser-extension/.gitignore +++ b/nym-browser-extension/.gitignore @@ -1,5 +1,14 @@ -# environment -.env.dev - -# error logs -yarn-error.log +dist/ +build/ +**/*.wasm +**/*.js.map +.env* +!.env.example +.vscode/ +.idea/ +*.swp +*.swo +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/nym-browser-extension/.nvmrc b/nym-browser-extension/.nvmrc deleted file mode 100644 index b6a7d89c68..0000000000 --- a/nym-browser-extension/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -16 diff --git a/nym-browser-extension/.prettierrc b/nym-browser-extension/.prettierrc deleted file mode 100644 index ccf75b89de..0000000000 --- a/nym-browser-extension/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "all", - "singleQuote": true, - "printWidth": 120, - "tabWidth": 2 -} diff --git a/nym-browser-extension/.sample.env b/nym-browser-extension/.sample.env deleted file mode 100644 index 85128503d8..0000000000 --- a/nym-browser-extension/.sample.env +++ /dev/null @@ -1,7 +0,0 @@ -RPC_URL= -VALIDATOR_URL= -PREFIX= -MIXNET_CONTRACT_ADDRESS= -VESTING_CONTRACT_ADDRESS= -DENOM= -BLOCK_EXPLORER_URL= \ No newline at end of file diff --git a/nym-browser-extension/README.md b/nym-browser-extension/README.md index a6ba4b6a97..1479abd955 100644 --- a/nym-browser-extension/README.md +++ b/nym-browser-extension/README.md @@ -1,40 +1,125 @@ -# Nym Browser Extension +# Nym Browser Extension Storage -The Nym browser extension lets you access your Nym wallet via the browser. +A WebAssembly-based storage component for securely managing mnemonics in extensions. This component provides encrypted storage functionality using IndexedDB with password-based encryption. -## Getting started +## Overview -You will need: +This storage component is built in Rust and compiled to WebAssembly, providing: -- NodeJS (use `nvm install` to automatically install the correct version) -- `npm` -- `yarn` +- **Secure mnemonic storage**: Password-encrypted storage of BIP39 mnemonics +- **IndexedDB integration**: Browser-native persistent storage +- **Multiple account support**: Store and manage multiple mnemonic phrases with custom names +- **Type-safe API**: Promise-based JavaScript API with proper error handling -> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them. +## Getting Started -From the [root of the repository](../README.md) run the following to build shared packages: +### Prerequisites -``` -yarn -yarn build +- Rust (latest stable) +- `wasm-pack` tool for building WebAssembly +- Node.js (for the demo server) + +### Building + +```bash +cd storage +make wasm-pack ``` -From the `nym-browser-extension` directory of the `nym` monorepo, run: +This will compile the Rust code to WebAssembly and generate the necessary JavaScript bindings. -`yarn dev` to run the extension in dev mode. +### Example Usage -You can then open a browser to http://localhost:9000 and start development. +See the [internal-dev example](./storage/internal-dev/index.js) for complete usage examples. -OR +Basic usage: -`yarn build` to build the extension. +```javascript +import init, { ExtensionStorage, set_panic_hook } from "@nymproject/extension-storage" -The extension will build to the `nym-browser-extension/dist` directory. +// Initialize the WASM module first +await init(); -## Load extension +// Set up better error handling +set_panic_hook(); -To load the extension into a Chrome browser +// Create storage instance with password +const storage = await new ExtensionStorage("your-secure-password"); -- Go to `settings > extensions > manage extensions` -- Select `Load unpacked` -- Select the `nym-browser-extension/dist` +// Store a mnemonic +const mnemonic = "your twenty four word mnemonic phrase goes here..."; +await storage.store_mnemonic("my-wallet", mnemonic); + +// Read a mnemonic +const retrievedMnemonic = await storage.read_mnemonic("my-wallet"); + +// Check if a mnemonic exists +const exists = await storage.has_mnemonic("my-wallet"); + +// Get all stored mnemonic keys +const allKeys = await storage.get_all_mnemonic_keys(); + +// Remove a mnemonic +await storage.remove_mnemonic("my-wallet"); +``` + +## Development + +To run the internal development example: + +```bash +cd storage + +make demo + +# Option 2: Manual server setup +cd internal-dev && node serve.js + +# Then open http://localhost:8000 in your browser +``` + +**Note**: The demo requires a server that properly serves WASM files with `application/wasm` MIME type. The Node.js server is recommended as it handles MIME types more reliably. + +## API Reference + +### Initialization +- `init()` - **Required**: Initialize the WASM module before using other functions + +### Constructor +- `new ExtensionStorage(password: string)` - Creates a new storage instance with the given password + +### Static Methods +- `ExtensionStorage.exists()` - Check if storage database exists + +### Instance Methods +- `store_mnemonic(name: string, mnemonic: string)` - Store a mnemonic with the given name +- `read_mnemonic(name: string)` - Retrieve a mnemonic by name (returns null if not found) +- `has_mnemonic(name: string)` - Check if a mnemonic with the given name exists +- `get_all_mnemonic_keys()` - Get all stored mnemonic names +- `remove_mnemonic(name: string)` - Remove a mnemonic by name + +### Error Handling +- `set_panic_hook()` - Set up better stack traces for Rust panics in development + +## Security Features + +- **Password-based encryption**: All data is encrypted using the provided password +- **BIP39 validation**: Mnemonics are validated before storage +- **Secure memory handling**: Sensitive data is zeroed from memory when no longer needed +- **Browser sandbox**: Runs within the browser's security model + +## Architecture + +The storage component consists of: + +- **Rust core** (`src/storage.rs`): Main storage implementation with encryption +- **WASM bindings** (`src/lib.rs`): WebAssembly interface layer +- **Error handling** (`src/error.rs`): Comprehensive error types +- **Build configuration** (`Cargo.toml`, `Makefile`): Build and dependency management + +## Important Notes + +1. **WASM Initialization**: Always call `await init()` before using any other functions +2. **MIME Types**: The demo requires a server that properly serves WASM files +3. **Browser Compatibility**: Requires modern browsers with WebAssembly support +4. **Module Loading**: Uses ES modules - ensure your build system supports them diff --git a/nym-browser-extension/package.json b/nym-browser-extension/package.json deleted file mode 100644 index 08fdc6f39c..0000000000 --- a/nym-browser-extension/package.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "@nym/browser-extension", - "version": "0.1.0", - "main": "index.js", - "license": "MIT", - "scripts": { - "dev": "yarn webpack serve --config webpack.dev.js", - "build": "yarn preinstall && webpack build --progress --config webpack.prod.js", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "lint:ts": "tsc --noEmit", - "storybook": "start-storybook -p 6006", - "storybook:build": "build-storybook" - }, - "dependencies": { - "@emotion/react": "^11.7.0", - "@emotion/styled": "^11.7.0", - "@hookform/resolvers": "^3.1.0", - "@mui/icons-material": "^5.11.11", - "@mui/material": "^5.11.15", - "@mui/system": "^5.11.15", - "@nymproject/mui-theme": "^1.0.0", - "@nymproject/nym-validator-client": ">=1.2.0-rc.0 || 1", - "@nymproject/react": "^1.0.0", - "@nymproject/types": "^1.0.0", - "@nymproject/extension-storage": ">=1.2.0-rc.0 || 1", - "@storybook/react": "^6.5.16", - "big.js": "^6.2.1", - "crypto-js": "^4.1.1", - "qrcode.react": "^3.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.3", - "react-hook-form": "^7.43.9", - "react-router-dom": "^6.9.0", - "zod": "^3.21.4" - }, - "devDependencies": { - "@nymproject/eslint-config-react-typescript": "^1.0.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", - "@svgr/webpack": "^6.1.1", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^14.0.0", - "@types/big.js": "^6.1.6", - "@types/crypto-js": "4.1.1", - "@types/jest": "^27.0.1", - "@types/node": "^18.16.1", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.10", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "copy-webpack-plugin": "^11.0.0", - "dotenv-webpack": "^8.0.1", - "eslint": "^8.10.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^16.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-root-import": "^1.0.4", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^26.1.1", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.2", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-storybook": "^0.5.12", - "favicons-webpack-plugin": "^5.0.2", - "html-webpack-plugin": "^5.3.2", - "jest": "^27.1.0", - "mini-css-extract-plugin": "^2.2.2", - "prettier": "^2.8.7", - "react-refresh": "^0.14.0", - "react-refresh-typescript": "^2.0.8", - "style-loader": "^3.3.1", - "ts-jest": "^27.0.5", - "ts-loader": "^9.4.2", - "tsconfig-paths-webpack-plugin": "^3.5.2", - "typescript": "^4.6.2", - "url-loader": "^4.1.1", - "util": "^0.12.5", - "webpack": "^5.75.0", - "webpack-cli": "^5.0.1", - "webpack-dev-server": "^4.5.0", - "webpack-favicons": "^1.3.8", - "webpack-merge": "^5.8.0" - } -} diff --git a/nym-browser-extension/src/App.tsx b/nym-browser-extension/src/App.tsx deleted file mode 100644 index a66ff64a2a..0000000000 --- a/nym-browser-extension/src/App.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme'; -import { AppRoutes } from './routes'; -import { AppLayout } from './layouts/AppLayout'; -import { AppProvider } from './context'; - -export const App = () => ( - <NymBrowserExtThemeWithMode mode="light"> - <AppProvider> - <AppLayout> - <AppRoutes /> - </AppLayout> - </AppProvider> - </NymBrowserExtThemeWithMode> -); diff --git a/nym-browser-extension/src/components/accounts/Accounts.tsx b/nym-browser-extension/src/components/accounts/Accounts.tsx deleted file mode 100644 index d5bf72e3ce..0000000000 --- a/nym-browser-extension/src/components/accounts/Accounts.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { useAppContext } from 'src/context'; -import { AccountActions } from './Actions'; - -const AccountItem = ({ - accountName, - disabled, - onSelect, -}: { - accountName: string; - disabled: boolean; - onSelect: () => void; -}) => ( - <ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider> - <ListItemButton onClick={onSelect} disabled={disabled}> - <ListItemAvatar> - <Avatar>{accountName[0]}</Avatar> - </ListItemAvatar> - <ListItemText primary={accountName} secondary={disabled && '(Selected)'} /> - </ListItemButton> - </ListItem> -); - -export const AccountList = () => { - const navigate = useNavigate(); - const { accounts, selectAccount, selectedAccount } = useAppContext(); - - const handleSelectAccount = async (accountName: string) => { - await selectAccount(accountName); - navigate('/user/balance'); - }; - - return ( - <> - {accounts.map((accountName) => ( - <AccountItem - disabled={selectedAccount === accountName} - accountName={accountName} - key={accountName} - onSelect={() => handleSelectAccount(accountName)} - /> - ))} - </> - ); -}; diff --git a/nym-browser-extension/src/components/accounts/Actions.tsx b/nym-browser-extension/src/components/accounts/Actions.tsx deleted file mode 100644 index 84d37881c1..0000000000 --- a/nym-browser-extension/src/components/accounts/Actions.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material'; -import { MoreVert, VisibilityOutlined } from '@mui/icons-material'; -import { useAppContext } from 'src/context'; - -type ActionType = { - title: string; - Icon: React.ReactNode; - onSelect: () => void; -}; - -const ActionItem = ({ action }: { action: ActionType }) => ( - <MenuItem dense onClick={action.onSelect}> - <ListItemIcon>{action.Icon}</ListItemIcon> - <ListItemText>{action.title}</ListItemText> - </MenuItem> -); - -export const AccountActions = ({ accountName }: { accountName: string }) => { - const { setShowSeedForAccount } = useAppContext(); - - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); - - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent<HTMLElement>) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - - const actions: Array<ActionType> = [ - { - title: 'View seed phrase', - Icon: <VisibilityOutlined />, - onSelect: () => { - setShowSeedForAccount(accountName); - }, - }, - ]; - - return ( - <> - <IconButton onClick={handleClick}> - <MoreVert /> - </IconButton> - <Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}> - {actions.map((action) => ( - <ActionItem action={action} key={action.title} /> - ))} - </Menu> - </> - ); -}; diff --git a/nym-browser-extension/src/components/accounts/ViewSeedPhrase.tsx b/nym-browser-extension/src/components/accounts/ViewSeedPhrase.tsx deleted file mode 100644 index 372c770934..0000000000 --- a/nym-browser-extension/src/components/accounts/ViewSeedPhrase.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useState } from 'react'; -import { Box, Card, CardContent, Typography } from '@mui/material'; -import { PasswordInput } from '@nymproject/react/textfields/Password'; -import { ExtensionStorage } from '@nymproject/extension-storage'; -import { Button, ConfirmationModal } from 'src/components/ui'; - -const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => ( - <Button fullWidth variant="contained" onClick={handleShowSeedPhrase}> - Show seed phrase - </Button> -); - -const DoneButton = ({ onDone }: { onDone: () => void }) => ( - <Button fullWidth variant="contained" onClick={onDone}> - Done - </Button> -); - -const Seed = ({ seed }: { seed: string }) => ( - <Card> - <CardContent> - <Typography>{seed}</Typography> - </CardContent> - </Card> -); - -export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => { - const [seed, setSeed] = useState<string>(); - const [password, setPassword] = useState(''); - const [error, setError] = useState<string>(); - - const handleShowSeedPhrase = async () => { - try { - const storage = await new ExtensionStorage(password); - const accountSeed = await storage.read_mnemonic(accountName); - setSeed(accountSeed); - } catch (e) { - setError('Could not retrieve seed phrase. Please check your password'); - } - }; - - return ( - <ConfirmationModal - open - onClose={onDone} - title={seed ? 'Account seed phrase' : 'Password'} - subtitle={seed ? '' : 'Enter your account password'} - ConfirmButton={ - seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} /> - } - > - {seed ? ( - <Seed seed={seed} /> - ) : ( - <Box sx={{ mt: 2 }}> - <PasswordInput - label="Password" - error={error} - password={password} - onUpdatePassword={(pw: string) => { - setPassword(pw); - }} - /> - </Box> - )} - </ConfirmationModal> - ); -}; diff --git a/nym-browser-extension/src/components/accounts/index.tsx b/nym-browser-extension/src/components/accounts/index.tsx deleted file mode 100644 index 4cc1e41842..0000000000 --- a/nym-browser-extension/src/components/accounts/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Accounts'; -export * from './Actions'; -export * from './ViewSeedPhrase'; diff --git a/nym-browser-extension/src/components/address/index.tsx b/nym-browser-extension/src/components/address/index.tsx deleted file mode 100644 index 79230b74f6..0000000000 --- a/nym-browser-extension/src/components/address/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; -import { Stack } from '@mui/system'; -import { ClientAddress } from '@nymproject/react/client-address/ClientAddress'; -import { useAppContext } from 'src/context'; - -export const Address = () => { - const { client } = useAppContext(); - - return ( - <Stack direction="row" justifyContent="space-between" alignItems="center"> - <Typography fontWeight={700}>Address</Typography> - <ClientAddress withCopy address={client?.address || ''} /> - </Stack> - ); -}; diff --git a/nym-browser-extension/src/components/balance/index.tsx b/nym-browser-extension/src/components/balance/index.tsx deleted file mode 100644 index a3b0391c22..0000000000 --- a/nym-browser-extension/src/components/balance/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { useEffect } from 'react'; -import { Stack, Typography } from '@mui/material'; -import { useAppContext } from 'src/context'; - -export const Balance = () => { - const { balance, fiatBalance, currency, getBalance } = useAppContext(); - - useEffect(() => { - getBalance(); - }, []); - - const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-'; - - return ( - <Stack alignItems="center" gap={1}> - <Typography sx={{ color: 'grey.600' }}>Available</Typography> - <Typography variant="h4" textAlign="center"> - {balance} NYM - </Typography> - <Typography sx={{ color: 'grey.600' }}>{fiat}</Typography> - </Stack> - ); -}; diff --git a/nym-browser-extension/src/components/index.ts b/nym-browser-extension/src/components/index.ts deleted file mode 100644 index 6838fdfd2b..0000000000 --- a/nym-browser-extension/src/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './accounts'; -export * from './address'; -export * from './balance'; -export * from './receive'; -export * from './send'; -export * from './ui'; diff --git a/nym-browser-extension/src/components/receive/ReceiveModal.tsx b/nym-browser-extension/src/components/receive/ReceiveModal.tsx deleted file mode 100644 index dc84aa2a33..0000000000 --- a/nym-browser-extension/src/components/receive/ReceiveModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material'; -import { QRCodeSVG } from 'qrcode.react'; -import { useAppContext } from 'src/context'; -import { ClientAddress } from '@nymproject/react/client-address/ClientAddress'; -import { Close } from '@mui/icons-material'; - -export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => { - const { client } = useAppContext(); - return ( - <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth> - <DialogTitle> - <Stack direction="row" justifyContent="space-between"> - <Typography fontWeight={700}>Receive</Typography> - <IconButton size="small" onClick={onClose} sx={{ padding: 0 }}> - <Close fontSize="small" /> - </IconButton> - </Stack> - </DialogTitle> - <DialogContent> - <Stack gap={1} alignItems="center"> - <Card elevation={3} sx={{ my: 2, width: 200 }}> - <CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> - <QRCodeSVG value={client?.address || ''} /> - </CardContent> - </Card> - <Typography variant="body2" fontWeight={700}> - Your Nym address - </Typography> - <ClientAddress address={client?.address || ''} withCopy smallIcons /> - </Stack> - </DialogContent> - </Dialog> - ); -}; diff --git a/nym-browser-extension/src/components/receive/index.ts b/nym-browser-extension/src/components/receive/index.ts deleted file mode 100644 index 356034db06..0000000000 --- a/nym-browser-extension/src/components/receive/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ReceiveModal'; diff --git a/nym-browser-extension/src/components/send/SendConfirmationModal.tsx b/nym-browser-extension/src/components/send/SendConfirmationModal.tsx deleted file mode 100644 index 59acb9a1dc..0000000000 --- a/nym-browser-extension/src/components/send/SendConfirmationModal.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { Link } from '@nymproject/react/link/Link'; -import { ConfirmationModal, Button } from 'src/components/ui'; - -export const SendConfirmationModal = ({ - amount, - txUrl, - onConfirm, -}: { - amount: string; - txUrl: string; - onConfirm: () => void; -}) => ( - <ConfirmationModal - open - fullWidth - title="You sent" - ConfirmButton={ - <Button fullWidth variant="contained" size="large" onClick={onConfirm}> - Done - </Button> - } - > - <Box> - <Typography variant="h6">{amount}</Typography> - <Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" /> - </Box> - </ConfirmationModal> -); diff --git a/nym-browser-extension/src/components/send/index.ts b/nym-browser-extension/src/components/send/index.ts deleted file mode 100644 index cb5dd64f44..0000000000 --- a/nym-browser-extension/src/components/send/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SendConfirmationModal'; diff --git a/nym-browser-extension/src/components/ui/AppBar/index.tsx b/nym-browser-extension/src/components/ui/AppBar/index.tsx deleted file mode 100644 index b6543c2862..0000000000 --- a/nym-browser-extension/src/components/ui/AppBar/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { AppBar as MUIAppBar } from '@mui/material/'; -import Box from '@mui/material/Box'; -import Toolbar from '@mui/material/Toolbar'; - -export const AppBar = ({ Action }: { Action: React.ReactNode }) => ( - <Box sx={{ flexGrow: 1 }}> - <MUIAppBar position="static" elevation={0} sx={{ bgcolor: 'rgba(103, 80, 164, 0.14)' }}> - <Toolbar variant="dense">{Action}</Toolbar> - </MUIAppBar> - </Box> -); - -export default AppBar; diff --git a/nym-browser-extension/src/components/ui/BackButton/index.tsx b/nym-browser-extension/src/components/ui/BackButton/index.tsx deleted file mode 100644 index 11594ab13e..0000000000 --- a/nym-browser-extension/src/components/ui/BackButton/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { ArrowBackIosRounded } from '@mui/icons-material'; -import { IconButton } from '@mui/material'; - -export const BackButton = ({ onBack }: { onBack?: () => void }) => { - const navigate = useNavigate(); - - const handleClick = () => { - if (onBack) { - onBack(); - } else { - navigate(-1); - } - return undefined; - }; - - return ( - <IconButton size="small" onClick={handleClick}> - <ArrowBackIosRounded fontSize="small" /> - </IconButton> - ); -}; diff --git a/nym-browser-extension/src/components/ui/Button/index.tsx b/nym-browser-extension/src/components/ui/Button/index.tsx deleted file mode 100644 index e44aca5c61..0000000000 --- a/nym-browser-extension/src/components/ui/Button/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { Button as MUIButton, ButtonProps } from '@mui/material'; - -export const Button = (props: ButtonProps) => ( - <MUIButton {...props} disableElevation sx={{ textTransform: 'initial' }} /> -); diff --git a/nym-browser-extension/src/components/ui/Logo/index.tsx b/nym-browser-extension/src/components/ui/Logo/index.tsx deleted file mode 100644 index 9b3d4ae513..0000000000 --- a/nym-browser-extension/src/components/ui/Logo/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react'; -import { NymLogoBW } from '@nymproject/react/logo/NymLogoBW'; - -export const Logo = ({ small }: { small?: boolean }) => ( - <NymLogoBW width={small ? '37.5px' : '75px'} height={small ? '37.5px' : '75px'} /> -); diff --git a/nym-browser-extension/src/components/ui/LogoWithText/index.tsx b/nym-browser-extension/src/components/ui/LogoWithText/index.tsx deleted file mode 100644 index 661cd0766d..0000000000 --- a/nym-browser-extension/src/components/ui/LogoWithText/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Stack, Typography } from '@mui/material'; -import { Logo } from '../Logo'; -import { Title } from '../Title'; - -export const LogoWithText = ({ - logoSmall, - title, - description, -}: { - logoSmall?: boolean; - title: string; - description?: string; -}) => ( - <Stack alignItems="center" justifyContent="center" gap={3}> - <Logo small={logoSmall} /> - <Title>{title} - {description} - -); diff --git a/nym-browser-extension/src/components/ui/MenuDrawer/index.tsx b/nym-browser-extension/src/components/ui/MenuDrawer/index.tsx deleted file mode 100644 index fcc0cafb95..0000000000 --- a/nym-browser-extension/src/components/ui/MenuDrawer/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react'; -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import { AccountBalanceWalletRounded, AccountCircleRounded, ArrowDownwardRounded } from '@mui/icons-material'; -import { Link } from 'react-router-dom'; - -const menuSchema = [ - { - title: 'Accounts', - Icon: , - path: '/user/accounts', - }, - { - title: 'Balance', - Icon: , - path: '/user/balance', - }, - { - title: 'Send', - Icon: , - path: '/user/send', - }, -]; - -export const MenuDrawer = ({ open, onClose }: { open: boolean; onClose: () => void }) => { - const list = () => ( - {}}> - - {menuSchema.map(({ title, Icon, path }) => ( - - - - {Icon} - - - - - ))} - - - ); - - return ( -
- - {list()} - -
- ); -}; diff --git a/nym-browser-extension/src/components/ui/Modal/ErrorModal.tsx b/nym-browser-extension/src/components/ui/Modal/ErrorModal.tsx deleted file mode 100644 index 713c83fdc9..0000000000 --- a/nym-browser-extension/src/components/ui/Modal/ErrorModal.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; -import { Button } from '../Button'; - -export interface ErrorModalProps { - open: boolean; - children?: React.ReactNode; - title: React.ReactNode | string; - subtitle?: React.ReactNode | string; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; - onClose?: () => void; -} - -export const ErrorModal = ({ - open, - onClose, - children, - title, - subtitle, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ErrorModalProps) => { - const Title = ( - - - {title} - - {subtitle && - (typeof subtitle === 'string' ? ( - - {subtitle} - - ) : ( - subtitle - ))} - - ); - - return ( - - {Title} - {children} - - - - - ); -}; diff --git a/nym-browser-extension/src/components/ui/Modal/LoadingModal.tsx b/nym-browser-extension/src/components/ui/Modal/LoadingModal.tsx deleted file mode 100644 index 82b0301c0f..0000000000 --- a/nym-browser-extension/src/components/ui/Modal/LoadingModal.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; - -const modalStyle: SxProps = { - position: 'absolute', - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', - width: 300, - bgcolor: 'background.paper', - boxShadow: 24, - borderRadius: '16px', - p: 4, -}; - -export const LoadingModal = ({ sx, backdropProps }: { sx?: SxProps; backdropProps?: object }) => ( - - `1px solid ${t.palette.grey[500]}`, ...modalStyle, ...sx }} textAlign="center"> - - - Please wait... - - - -); diff --git a/nym-browser-extension/src/components/ui/Modal/Modal.tsx b/nym-browser-extension/src/components/ui/Modal/Modal.tsx deleted file mode 100644 index 18f88e28d0..0000000000 --- a/nym-browser-extension/src/components/ui/Modal/Modal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { - Breakpoint, - Paper, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - SxProps, - Typography, -} from '@mui/material'; - -export interface ConfirmationModalProps { - open: boolean; - children?: React.ReactNode; - title: React.ReactNode | string; - subtitle?: React.ReactNode | string; - ConfirmButton: React.ReactNode; - sx?: SxProps; - fullWidth?: boolean; - maxWidth?: Breakpoint; - backdropProps?: object; - onClose?: () => void; -} - -export const ConfirmationModal = ({ - open, - onClose, - children, - title, - subtitle, - ConfirmButton, - sx, - fullWidth, - maxWidth, - backdropProps, -}: ConfirmationModalProps) => { - const Title = ( - - - {title} - - {subtitle && - (typeof subtitle === 'string' ? ( - - {subtitle} - - ) : ( - subtitle - ))} - - ); - - return ( - - {Title} - {children} - {ConfirmButton} - - ); -}; diff --git a/nym-browser-extension/src/components/ui/Modal/index.tsx b/nym-browser-extension/src/components/ui/Modal/index.tsx deleted file mode 100644 index 310ca4336c..0000000000 --- a/nym-browser-extension/src/components/ui/Modal/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Modal'; -export * from './LoadingModal'; -export * from './ErrorModal'; diff --git a/nym-browser-extension/src/components/ui/Title/index.tsx b/nym-browser-extension/src/components/ui/Title/index.tsx deleted file mode 100644 index 1bc6036d6e..0000000000 --- a/nym-browser-extension/src/components/ui/Title/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; - -const FONT_WEIGHT = 400; - -export const Title = ({ children }: { children: string }) => ( - - {children} - -); diff --git a/nym-browser-extension/src/components/ui/index.ts b/nym-browser-extension/src/components/ui/index.ts deleted file mode 100644 index 5bee731a18..0000000000 --- a/nym-browser-extension/src/components/ui/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './AppBar'; -export * from './Button'; -export * from './BackButton'; -export * from './Logo'; -export * from './LogoWithText'; -export * from './MenuDrawer'; -export * from './Modal'; -export * from './Title'; diff --git a/nym-browser-extension/src/config.ts b/nym-browser-extension/src/config.ts deleted file mode 100644 index f66efff545..0000000000 --- a/nym-browser-extension/src/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const config = { - rpcUrl: process.env.RPC_URL || '', - validatorUrl: process.env.VALIDATOR_URL || '', - prefix: process.env.PREFIX || '', - mixnetContractAddress: process.env.MIXNET_CONTRACT_ADDRESS || '', - vestingContractAddress: process.env.VESTING_CONTRACT_ADDRESS || '', - denom: process.env.DENOM || '', -}; diff --git a/nym-browser-extension/src/context/app.tsx b/nym-browser-extension/src/context/app.tsx deleted file mode 100644 index 9d052c1a60..0000000000 --- a/nym-browser-extension/src/context/app.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import ValidatorClient from '@nymproject/nym-validator-client'; -import { ExtensionStorage } from '@nymproject/extension-storage'; -import { connectToValidator } from 'src/validator-client'; -import { unymToNym } from 'src/utils/coin'; -import { Currency, getTokenPrice } from 'src/utils/price'; - -type TAppContext = { - client?: ValidatorClient; - accounts: string[]; - balance?: string; - fiatBalance?: number; - denom: 'NYM'; - minorDenom: 'unym'; - currency: Currency; - showSeedForAccount?: string; - selectedAccount: string; - storage?: ExtensionStorage; - selectAccount: (accountName: string) => Promise; - setAccounts: (accounts: string[]) => void; - setShowSeedForAccount: (accountName?: string) => void; - handleUnlockWallet: (password: string) => void; - getBalance: () => void; -}; - -type TBalanceInNYMs = string; - -const DEFAULT_ACCOUNT_NAME = 'Default account'; - -const AppContext = React.createContext({} as TAppContext); - -export const AppProvider = ({ children }: { children: React.ReactNode }) => { - const [client, setClient] = useState(); - const [selectedAccount, setSelected] = useState(DEFAULT_ACCOUNT_NAME); - const [balance, setBalance] = useState(); - const [fiatBalance, setFiatBalance] = useState(); - const [accounts, setAccounts] = useState([]); - const [showSeedForAccount, setShowSeedForAccount] = useState(); - const [storage, setStorage] = useState(); - - const denom = 'NYM'; - const minorDenom = 'unym'; - const currency = 'gbp'; - - const handleUnlockWallet = async (password: string) => { - const store = await new ExtensionStorage(password); - const mnemonic = await store.read_mnemonic(DEFAULT_ACCOUNT_NAME); - const userAccounts = await store.get_all_mnemonic_keys(); - const clientFromMnemonic = await connectToValidator(mnemonic); - - setStorage(store); - setAccounts(userAccounts); - setClient(clientFromMnemonic); - }; - - const selectAccount = async (accountName: string) => { - const mnemonic = await storage!.read_mnemonic(accountName); - const clientFromMnemonic = await connectToValidator(mnemonic); - setSelected(accountName); - setClient(clientFromMnemonic); - }; - - const getFiatBalance = async (bal: number) => { - const tokenPrice = await getTokenPrice('nym', currency); - const fiatBal = tokenPrice.nym.gbp * bal; - return fiatBal; - }; - - const getBalance = async () => { - const bal = await client?.getBalance(client.address); - if (bal) { - const nym = unymToNym(Number(bal.amount)); - const fiat = await getFiatBalance(nym); - setFiatBalance(fiat); - setBalance(nym.toString()); - } - }; - - useEffect(() => { - if (client) { - getBalance(); - } - }, [client]); - - const value = useMemo( - () => ({ - client, - accounts, - balance, - fiatBalance, - currency, - denom, - minorDenom, - selectedAccount, - storage, - handleUnlockWallet, - getBalance, - setShowSeedForAccount, - showSeedForAccount, - setAccounts, - selectAccount, - }), - [client, accounts, balance, fiatBalance, denom, minorDenom, selectedAccount, showSeedForAccount, storage], - ); - - return {children}; -}; - -export const useAppContext = () => React.useContext(AppContext); diff --git a/nym-browser-extension/src/context/index.tsx b/nym-browser-extension/src/context/index.tsx deleted file mode 100644 index 5bcd157c3c..0000000000 --- a/nym-browser-extension/src/context/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from './app'; -export * from './send'; -export * from './register'; diff --git a/nym-browser-extension/src/context/register.tsx b/nym-browser-extension/src/context/register.tsx deleted file mode 100644 index 2204b07d0b..0000000000 --- a/nym-browser-extension/src/context/register.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { ExtensionStorage } from '@nymproject/extension-storage'; - -const RegisterContext = React.createContext({} as TRegisterContext); - -type TRegisterContext = { - userPassword: string; - userMnemonic: string; - accountName: string; - checkAccountName: () => Promise; - setUserPassword: (password: string) => void; - setUserMnemonic: (mnemonic: string) => void; - setAccountName: (name: string) => void; - createAccount: (args: { mnemonic: string; password: string; accName: string }) => Promise; - importAccount: () => Promise; - resetState: () => void; -}; - -export const RegisterContextProvider = ({ children }: { children: React.ReactNode }) => { - const [userPassword, setUserPassword] = useState(''); - const [userMnemonic, setUserMnemonic] = useState(''); - const [accountName, setAccountName] = useState(''); - - const resetState = () => { - setUserMnemonic(''); - setUserPassword(''); - setAccountName(''); - }; - - const createAccount = async ({ - mnemonic, - password, - accName, - }: { - mnemonic: string; - password: string; - accName: string; - }) => { - const storage = await new ExtensionStorage(password); - await storage.store_mnemonic(accName, mnemonic); - }; - - const importAccount = async () => { - const storage = await new ExtensionStorage(userPassword); - await storage.store_mnemonic(accountName, userMnemonic); - const accounts = await storage.get_all_mnemonic_keys(); - return accounts; - }; - - const checkAccountName = async () => true; - - const value = useMemo( - () => ({ - userPassword, - setUserPassword, - userMnemonic, - accountName, - setAccountName, - setUserMnemonic, - createAccount, - checkAccountName, - importAccount, - resetState, - }), - [userPassword, userMnemonic, accountName], - ); - - return {children}; -}; - -export const useRegisterContext = () => React.useContext(RegisterContext); diff --git a/nym-browser-extension/src/context/send.tsx b/nym-browser-extension/src/context/send.tsx deleted file mode 100644 index a5c4295ff8..0000000000 --- a/nym-browser-extension/src/context/send.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { DecCoin } from '@nymproject/types'; -import { useNavigate } from 'react-router-dom'; -import { nymToUnym } from 'src/utils/coin'; -import { TTransaction } from 'src/types'; -import { Fee, useGetFee } from 'src/hooks/useGetFee'; -import { createFeeObject } from 'src/utils/fee'; -import { useAppContext } from './app'; - -type TSendContext = { - address?: string; - amount?: DecCoin; - transaction?: TTransaction; - fee?: Fee; - handleChangeAddress: (address?: string) => void; - handleChangeAmount: (amount?: DecCoin) => void; - handleSend: () => void; - resetTx: () => void; - onDone: () => void; - handleGetFee: (address: string, amount: string) => Promise; -}; - -const SendContext = React.createContext({} as TSendContext); - -export const SendProvider = ({ children }: { children: React.ReactNode }) => { - const [address, setAddress] = useState(); - const [amount, setAmount] = useState(); - const [transaction, setTransaction] = useState(); - - const { client, minorDenom } = useAppContext(); - const navigate = useNavigate(); - - const handleChangeAddress = (_address?: string) => setAddress(_address); - - const handleChangeAmount = (_amount?: DecCoin) => setAmount(_amount); - - const { getFee, fee } = useGetFee(); - - const handleGetFee = async (addressVal: string, amountVal: string) => { - const unym = nymToUnym(Number(amountVal)); - - if (client) { - // client loses its 'this' context when passing the method - // TODO find a better way of doing this. - getFee(client.simulateSend.bind(client), { - signingAddress: client.address, - from: client.address, - to: addressVal, - amount: [{ amount: unym.toString(), denom: minorDenom }], - }); - } - }; - - const handleSend = async () => { - setTransaction({ status: 'loading', type: 'send' }); - let unyms; - - if (!Number(amount?.amount)) { - setTransaction({ status: 'error', type: 'send', message: 'Amount is not a valid number' }); - } - - if (amount) { - unyms = nymToUnym(Number(amount.amount)); - } - - if (client && address && unyms) { - try { - const response = await client.send( - address, - [{ amount: unyms.toString(), denom: minorDenom }], - createFeeObject(fee?.unym), - ); - - setTransaction({ status: 'success', type: 'send', txHash: response?.transactionHash }); - } catch (e) { - setTransaction({ - status: 'error', - type: 'send', - message: e instanceof Error ? e.message : 'Error making send transaction. Please try again', - }); - } - } - }; - - const resetTx = () => { - setTransaction(undefined); - }; - - const onDone = () => { - navigate('/user/balance'); - }; - - const value = useMemo( - () => ({ - address, - amount, - transaction, - fee, - handleChangeAddress, - handleChangeAmount, - handleSend, - resetTx, - onDone, - handleGetFee, - }), - [address, amount, transaction, fee], - ); - - return {children}; -}; - -export const useSendContext = () => React.useContext(SendContext); diff --git a/nym-browser-extension/src/hooks/useCreatePassword.tsx b/nym-browser-extension/src/hooks/useCreatePassword.tsx deleted file mode 100644 index 33aa94ca59..0000000000 --- a/nym-browser-extension/src/hooks/useCreatePassword.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useState } from 'react'; - -export const useCreatePassword = () => { - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [isSafePassword, setIsSafePassword] = useState(false); - const [hasReadTerms, setHasReadTerms] = useState(false); - - const canProceed = isSafePassword && hasReadTerms && password === confirmPassword; - - return { - password, - setPassword, - confirmPassword, - setConfirmPassword, - setIsSafePassword, - canProceed, - setHasReadTerms, - hasReadTerms, - }; -}; diff --git a/nym-browser-extension/src/hooks/useGetFee.ts b/nym-browser-extension/src/hooks/useGetFee.ts deleted file mode 100644 index af6080a2f4..0000000000 --- a/nym-browser-extension/src/hooks/useGetFee.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Big from 'big.js'; -import { useState } from 'react'; -import { unymToNym } from 'src/utils/coin'; - -export type Fee = { nym: number; unym: number }; - -export function useGetFee() { - const [fee, setFee] = useState(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(); - - async function getFee(txReq: (args: T) => Promise, args: T) { - setError(undefined); - setIsLoading(true); - - try { - const txFee = await txReq(args); - - if (txFee) { - const feeWithMultiplyer = Big(txFee).mul(1); - console.log(fee); - - const txFeeInNyms = unymToNym(feeWithMultiplyer); - - setFee({ nym: Number(txFeeInNyms), unym: Number(feeWithMultiplyer) }); - } - - if (!txFee) { - setError('Unable to calculate fee'); - } - } catch (e) { - console.error(e); - setError(`Unable to get estimated fee: ${e}`); - } finally { - setIsLoading(false); - } - } - - return { fee, getFee, isLoading, error }; -} diff --git a/nym-browser-extension/src/index.html b/nym-browser-extension/src/index.html deleted file mode 100644 index 5c2e4f5473..0000000000 --- a/nym-browser-extension/src/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - Nym browser extension - - -
- - diff --git a/nym-browser-extension/src/index.tsx b/nym-browser-extension/src/index.tsx deleted file mode 100644 index 56859d9574..0000000000 --- a/nym-browser-extension/src/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; - -const rootDomElem = document.getElementById('root'); - -if (rootDomElem) { - const root = createRoot(rootDomElem); - root.render(); -} diff --git a/nym-browser-extension/src/layouts/AppLayout.tsx b/nym-browser-extension/src/layouts/AppLayout.tsx deleted file mode 100644 index be5a54398e..0000000000 --- a/nym-browser-extension/src/layouts/AppLayout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Container } from '@mui/material'; -import React from 'react'; - -export const AppLayout = ({ children }: { children: React.ReactNode }) => ( - - {children} - -); diff --git a/nym-browser-extension/src/layouts/CenteredLogo.tsx b/nym-browser-extension/src/layouts/CenteredLogo.tsx deleted file mode 100644 index 5a59bd3673..0000000000 --- a/nym-browser-extension/src/layouts/CenteredLogo.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { LogoWithText } from 'src/components/ui'; - -const layoutStyle = { - height: '100%', - display: 'grid', - gridTemplateColumns: '1fr', - gridTemplateRows: 'repeat(3, 1fr)', - gridColumnGap: '0px', - gridRowGap: '0px', - p: 2, -}; - -export const CenteredLogoLayout = ({ - title, - description, - Actions, -}: { - title: string; - description?: string; - Actions: React.ReactNode; -}) => ( - - - - {Actions} - -); diff --git a/nym-browser-extension/src/layouts/PageLayout.tsx b/nym-browser-extension/src/layouts/PageLayout.tsx deleted file mode 100644 index 07bd62797d..0000000000 --- a/nym-browser-extension/src/layouts/PageLayout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { Box, IconButton } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import { AppBar, BackButton, MenuDrawer } from 'src/components/ui'; -import { useLocation } from 'react-router-dom'; - -const layoutStyle = { - display: 'grid', - gridTemplateColumns: '1fr', - gridTemplateRows: '50px 1fr', - gridColumnGap: '0px', - gridRowGap: '0px', -}; - -export const PageLayout = ({ children, onBack }: { children: React.ReactNode; onBack?: () => void }) => { - const [menuOpen, setMenuOpen] = useState(false); - - const location = useLocation(); - - const MenuAction = useCallback( - () => ( - setMenuOpen(true)}> - - - ), - [], - ); - - const Action = location.pathname.includes('balance') ? MenuAction : BackButton; - - return ( - - } /> - setMenuOpen(false)} /> - {children} - - ); -}; diff --git a/nym-browser-extension/src/layouts/TopLogo.tsx b/nym-browser-extension/src/layouts/TopLogo.tsx deleted file mode 100644 index b8b9a75ccf..0000000000 --- a/nym-browser-extension/src/layouts/TopLogo.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { BackButton, LogoWithText } from 'src/components/ui'; - -const layoutStyle = { - height: '100%', - display: 'grid', - gridTemplateColumns: '1fr', - gridTemplaterows: '1fr 2fr 1fr', - gridColumnGap: '0px', - gridRowGap: '0px', - position: 'relative', - p: 2, -}; - -export const TopLogoLayout = ({ - title, - description, - children, - Actions, -}: { - title: string; - description?: string; - children: React.ReactNode; - Actions: React.ReactNode; -}) => ( - - - - - - - - {children} - {Actions} - -); diff --git a/nym-browser-extension/src/layouts/index.ts b/nym-browser-extension/src/layouts/index.ts deleted file mode 100644 index 1126750950..0000000000 --- a/nym-browser-extension/src/layouts/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './AppLayout'; -export * from './CenteredLogo'; -export * from './TopLogo'; diff --git a/nym-browser-extension/src/manifest.json b/nym-browser-extension/src/manifest.json deleted file mode 100644 index 47ced7fc78..0000000000 --- a/nym-browser-extension/src/manifest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "Nym browser extension", - "description": "Nym browser extension - Wallet & credentials", - "version": "0.1.0", - "manifest_version": 3, - "action": { - "default_popup": "index.html", - "default_title": "Nym - Browser extension" - }, - "icons": { - "16": "favicon-16x16.png", - "32": "favicon-32x32.png", - "48": "favicon-48x48.png" - }, - "content_security_policy": { - "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" - } -} diff --git a/nym-browser-extension/src/pages/accounts/Accounts.tsx b/nym-browser-extension/src/pages/accounts/Accounts.tsx deleted file mode 100644 index 00b8f065f2..0000000000 --- a/nym-browser-extension/src/pages/accounts/Accounts.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useEffect } from 'react'; -import { PageLayout } from 'src/layouts/PageLayout'; -import { Stack } from '@mui/material'; -import { Add, ArrowDownward } from '@mui/icons-material'; -import { AccountList, Button } from 'src/components'; -import { ViewSeedPhrase } from 'src/components/accounts/ViewSeedPhrase'; -import { useAppContext, useRegisterContext } from 'src/context'; -import { useLocation, useNavigate } from 'react-router-dom'; - -export const Accounts = () => { - const { showSeedForAccount, setShowSeedForAccount } = useAppContext(); - const { resetState } = useRegisterContext(); - - useEffect(() => { - resetState(); - }, []); - - const location = useLocation(); - const navigate = useNavigate(); - - const handleAddAccount = () => navigate(`${location.pathname}/add-account`); - - const handleImportAccount = () => navigate(`${location.pathname}/import-account`); - - const onBack = () => navigate('/user/balance'); - - return ( - - {showSeedForAccount && ( - setShowSeedForAccount(undefined)} /> - )} - - - - - - - ); -}; diff --git a/nym-browser-extension/src/pages/accounts/AddAccount.tsx b/nym-browser-extension/src/pages/accounts/AddAccount.tsx deleted file mode 100644 index 50849ea9d3..0000000000 --- a/nym-browser-extension/src/pages/accounts/AddAccount.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRegisterContext } from 'src/context'; -import { SeedPhraseTemplate } from 'src/pages/templates'; - -export const AddAccount = () => { - const { setUserMnemonic } = useRegisterContext(); - const navigate = useNavigate(); - - const onNext = (seedPhrase: string) => { - setUserMnemonic(seedPhrase); - navigate('/user/accounts/name-account'); - }; - - return ; -}; diff --git a/nym-browser-extension/src/pages/accounts/Complete.tsx b/nym-browser-extension/src/pages/accounts/Complete.tsx deleted file mode 100644 index b94616c531..0000000000 --- a/nym-browser-extension/src/pages/accounts/Complete.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { SetupCompleteTemplate } from 'src/pages/templates/Complete'; - -export const SetupComplete = () => { - const navigate = useNavigate(); - const handleOnDone = () => { - navigate('/user/accounts'); - }; - - return ( - - ); -}; diff --git a/nym-browser-extension/src/pages/accounts/ConfirmPassword.tsx b/nym-browser-extension/src/pages/accounts/ConfirmPassword.tsx deleted file mode 100644 index 10f3efd549..0000000000 --- a/nym-browser-extension/src/pages/accounts/ConfirmPassword.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useState } from 'react'; -import { PasswordInput } from '@nymproject/react/textfields/Password'; -import { Button } from 'src/components'; -import { useAppContext, useRegisterContext } from 'src/context'; -import { TopLogoLayout } from 'src/layouts'; -import { useNavigate } from 'react-router-dom'; - -export const ConfirmPassword = () => { - const { setAccounts } = useAppContext(); - const { userPassword, setUserPassword, importAccount } = useRegisterContext(); - const [error, setError] = useState(); - - const navigate = useNavigate(); - - const handleOnComplete = async () => { - try { - const accounts = await importAccount(); - setAccounts(accounts); - navigate('/user/accounts/complete'); - } catch (e) { - setError('Incorrect password. Please try again'); - } - }; - - const onChange = (password: string) => { - setError(undefined); - setUserPassword(password); - }; - - return ( - - Confirm - - } - > - - - ); -}; diff --git a/nym-browser-extension/src/pages/accounts/ImportAccount.tsx b/nym-browser-extension/src/pages/accounts/ImportAccount.tsx deleted file mode 100644 index b630eef986..0000000000 --- a/nym-browser-extension/src/pages/accounts/ImportAccount.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRegisterContext } from 'src/context/register'; -import { ImportAccountTemplate } from '../templates'; - -export const ImportAccount = () => { - const { userMnemonic, setUserMnemonic } = useRegisterContext(); - const navigate = useNavigate(); - - const handleOnNext = () => { - navigate('/user/accounts/name-account'); - }; - - return ( - - ); -}; diff --git a/nym-browser-extension/src/pages/accounts/NameAccount.tsx b/nym-browser-extension/src/pages/accounts/NameAccount.tsx deleted file mode 100644 index 7cb2bc9b66..0000000000 --- a/nym-browser-extension/src/pages/accounts/NameAccount.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useState } from 'react'; -import { TextField } from '@mui/material'; -import { Button } from 'src/components'; -import { useRegisterContext } from 'src/context/register'; -import { TopLogoLayout } from 'src/layouts'; -import { useNavigate } from 'react-router-dom'; -import { useAppContext } from 'src/context'; - -export const NameAccount = () => { - const { accountName, setAccountName } = useRegisterContext(); - const { storage } = useAppContext(); - const navigate = useNavigate(); - - const [error, setError] = useState(); - - const handleNext = async () => { - const accountNameExists = await storage?.has_mnemonic(accountName); - if (accountNameExists) { - setError('Account name already exists. Please choose another account name'); - } else { - navigate('/user/accounts/confirm-password'); - } - }; - - return ( - - Next - - } - > - { - setError(undefined); - setAccountName(e.target.value); - }} - error={!!error} - helperText={error} - /> - - ); -}; diff --git a/nym-browser-extension/src/pages/accounts/index.ts b/nym-browser-extension/src/pages/accounts/index.ts deleted file mode 100644 index ed37aac8ad..0000000000 --- a/nym-browser-extension/src/pages/accounts/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './Accounts'; -export * from './AddAccount'; -export * from './Complete'; -export * from './ConfirmPassword'; -export * from './ImportAccount'; -export * from './NameAccount'; diff --git a/nym-browser-extension/src/pages/auth/ForgotPassword.tsx b/nym-browser-extension/src/pages/auth/ForgotPassword.tsx deleted file mode 100644 index 5910a1d77f..0000000000 --- a/nym-browser-extension/src/pages/auth/ForgotPassword.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { Typography } from '@mui/material'; -import { Box } from '@mui/system'; -import { TopLogoLayout } from 'src/layouts'; - -const steps = [ - 'Make sure you have your mnemonic saved', - 'Uninstal Nym extension wallet', - 'Reinstal Nym extension wallet', - 'Import your account using seed phrase', - 'Create new password', -]; - -export const ForgotPassword = () => ( - }> - - {steps.map((step, index) => ( - - {`${index + 1}. ${step}`} - - ))} - - -); diff --git a/nym-browser-extension/src/pages/auth/Login.tsx b/nym-browser-extension/src/pages/auth/Login.tsx deleted file mode 100644 index 11db476a55..0000000000 --- a/nym-browser-extension/src/pages/auth/Login.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { Stack, TextField } from '@mui/material'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { Button } from 'src/components/ui'; -import { CenteredLogoLayout } from 'src/layouts/CenteredLogo'; -import { useAppContext } from 'src/context'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { validationSchema } from './validationSchema'; - -export const Login = () => { - const { handleUnlockWallet } = useAppContext(); - const navigate = useNavigate(); - const location = useLocation(); - - const { - register, - handleSubmit, - setError, - formState: { errors, isSubmitting }, - } = useForm({ resolver: zodResolver(validationSchema), defaultValues: { password: '' } }); - - const onSubmit = async (data: { password: string }) => { - try { - await handleUnlockWallet(data.password); - } catch (e) { - setError('password', { message: 'Incorrect password. Please try again.' }); - } - }; - - return ( - - - - - - - } - /> - ); -}; diff --git a/nym-browser-extension/src/pages/auth/index.tsx b/nym-browser-extension/src/pages/auth/index.tsx deleted file mode 100644 index 80aa38c3c1..0000000000 --- a/nym-browser-extension/src/pages/auth/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Login'; -export * from './ForgotPassword'; diff --git a/nym-browser-extension/src/pages/auth/validationSchema.ts b/nym-browser-extension/src/pages/auth/validationSchema.ts deleted file mode 100644 index a24b838a9c..0000000000 --- a/nym-browser-extension/src/pages/auth/validationSchema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as z from 'zod'; - -export const validationSchema = z.object({ - password: z.string().min(1, { message: 'Required' }), -}); diff --git a/nym-browser-extension/src/pages/balance/index.tsx b/nym-browser-extension/src/pages/balance/index.tsx deleted file mode 100644 index a4fc0cd4e6..0000000000 --- a/nym-browser-extension/src/pages/balance/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Box, Stack, IconButton, Typography } from '@mui/material'; -import { ArrowDownwardRounded, ArrowUpwardRounded, TollRounded } from '@mui/icons-material'; -import { PageLayout } from 'src/layouts/PageLayout'; -import { Address, Balance, ReceiveModal } from 'src/components'; - -type ActionsSchema = Array<{ - title: string; - Icon: React.ReactNode; - onClick: () => void; -}>; - -const Actions = ({ actionsSchema }: { actionsSchema: ActionsSchema }) => ( - - {actionsSchema.map(({ title, Icon, onClick }) => ( - - - {Icon} - - {title} - - ))} - -); - -export const BalancePage = () => { - const [showReceiveModal, setShowReceiveModal] = useState(false); - const navigate = useNavigate(); - - const actionsSchema = [ - { - title: 'Send', - Icon: , - onClick: () => navigate('/user/send'), - }, - { - title: 'Receive', - Icon: , - onClick: () => setShowReceiveModal(true), - }, - { - title: 'Buy', - Icon: , - onClick: () => navigate('/user/balance'), - }, - ]; - - return ( - - - setShowReceiveModal(false)} /> -
- - - - - ); -}; diff --git a/nym-browser-extension/src/pages/delegation/index.tsx b/nym-browser-extension/src/pages/delegation/index.tsx deleted file mode 100644 index 4e89134021..0000000000 --- a/nym-browser-extension/src/pages/delegation/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export const Delegation = () =>

Delegation

; diff --git a/nym-browser-extension/src/pages/home/index.tsx b/nym-browser-extension/src/pages/home/index.tsx deleted file mode 100644 index 21cc23d920..0000000000 --- a/nym-browser-extension/src/pages/home/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { Stack } from '@mui/system'; -import { Button } from 'src/components/ui'; -import { CenteredLogoLayout } from 'src/layouts'; -import { Link } from 'react-router-dom'; - -export const Home = () => ( - - - - - - - - - } - /> -); diff --git a/nym-browser-extension/src/pages/index.ts b/nym-browser-extension/src/pages/index.ts deleted file mode 100644 index 47c840f752..0000000000 --- a/nym-browser-extension/src/pages/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './accounts'; -export * from './auth'; -export * from './balance'; -export * from './home'; -export * from './receive'; -export * from './send'; -export * from './settings'; -export * from './delegation'; diff --git a/nym-browser-extension/src/pages/receive/index.tsx b/nym-browser-extension/src/pages/receive/index.tsx deleted file mode 100644 index cdccb23d3e..0000000000 --- a/nym-browser-extension/src/pages/receive/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export const Receive = () =>

Receive

; diff --git a/nym-browser-extension/src/pages/register/Complete.tsx b/nym-browser-extension/src/pages/register/Complete.tsx deleted file mode 100644 index 601e355910..0000000000 --- a/nym-browser-extension/src/pages/register/Complete.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import { SetupCompleteTemplate } from '../templates/Complete'; - -export const SetupComplete = ({ onDone }: { onDone: () => void }) => ( - -); diff --git a/nym-browser-extension/src/pages/register/CreatePasswordOnExistingAccount.tsx b/nym-browser-extension/src/pages/register/CreatePasswordOnExistingAccount.tsx deleted file mode 100644 index 77a19b7e5e..0000000000 --- a/nym-browser-extension/src/pages/register/CreatePasswordOnExistingAccount.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { useCreatePassword } from 'src/hooks/useCreatePassword'; -import { useRegisterContext } from 'src/context/register'; -import { CreatePasswordTemplate } from 'src/pages/templates/CreatePassword'; - -export const CreatePasswordOnExistingAccount = ({ onComplete }: { onComplete: () => void }) => { - const passwordState = useCreatePassword(); - const { createAccount, userMnemonic } = useRegisterContext(); - - const handleOnComplete = async () => { - await createAccount({ mnemonic: userMnemonic, password: passwordState.password, accName: 'Default account' }); - onComplete(); - }; - - return ; -}; diff --git a/nym-browser-extension/src/pages/register/CreatePasswordOnNewAccount.tsx b/nym-browser-extension/src/pages/register/CreatePasswordOnNewAccount.tsx deleted file mode 100644 index 2a5190ac7c..0000000000 --- a/nym-browser-extension/src/pages/register/CreatePasswordOnNewAccount.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { useCreatePassword } from 'src/hooks/useCreatePassword'; -import { useRegisterContext } from 'src/context/register'; -import { CreatePasswordTemplate } from 'src/pages/templates/CreatePassword'; - -export const CreatePasswordOnNewAccount = ({ onNext }: { onNext: () => void }) => { - const passwordState = useCreatePassword(); - const { setUserPassword } = useRegisterContext(); - - const handleCreateAccount = async () => { - await setUserPassword(passwordState.password); - onNext(); - }; - - return ; -}; diff --git a/nym-browser-extension/src/pages/register/ImportAccount.tsx b/nym-browser-extension/src/pages/register/ImportAccount.tsx deleted file mode 100644 index 76cf420471..0000000000 --- a/nym-browser-extension/src/pages/register/ImportAccount.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useRegisterContext } from 'src/context/register'; -import { ImportAccountTemplate } from '../templates/ImportAccount'; - -export const ImportAccount = () => { - const navigate = useNavigate(); - const location = useLocation(); - - const { setUserMnemonic, userMnemonic } = useRegisterContext(); - - const handleNext = async () => { - navigate(`${location.pathname}/create-password`); - }; - - return ( - - ); -}; diff --git a/nym-browser-extension/src/pages/register/SeedPhrase.tsx b/nym-browser-extension/src/pages/register/SeedPhrase.tsx deleted file mode 100644 index 7218aa8da7..0000000000 --- a/nym-browser-extension/src/pages/register/SeedPhrase.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRegisterContext } from 'src/context/register'; -import { SeedPhraseTemplate } from '../templates/SeedPhrase'; - -export const SeedPhrase = () => { - const navigate = useNavigate(); - - const { createAccount, userPassword } = useRegisterContext(); - - const handleEncryptSeedPhrase = async (seedPhrase: string) => { - await createAccount({ mnemonic: seedPhrase, password: userPassword, accName: 'Default account' }); - navigate('/register/complete'); - }; - - return ; -}; diff --git a/nym-browser-extension/src/pages/register/index.ts b/nym-browser-extension/src/pages/register/index.ts deleted file mode 100644 index 691cc9ea35..0000000000 --- a/nym-browser-extension/src/pages/register/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Complete'; -export * from './CreatePasswordOnExistingAccount'; -export * from './CreatePasswordOnNewAccount'; -export * from './ImportAccount'; -export * from './SeedPhrase'; diff --git a/nym-browser-extension/src/pages/send/Confirmation.tsx b/nym-browser-extension/src/pages/send/Confirmation.tsx deleted file mode 100644 index 2e6d305f4c..0000000000 --- a/nym-browser-extension/src/pages/send/Confirmation.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { Box, Divider, ListItem, ListItemText, Stack, Typography } from '@mui/material'; -import { Button } from 'src/components'; -import { PageLayout } from 'src/layouts/PageLayout'; -import { useAppContext, useSendContext } from 'src/context'; -import { ErrorModal, LoadingModal } from 'src/components/ui/Modal'; -import { SendConfirmationModal } from 'src/components/send/SendConfirmationModal'; -import { blockExplorerUrl } from 'src/urls'; - -const InfoItem = ({ label, value }: { label: string; value: string }) => ( - - - - {label} - - } - secondary={ - - {value} - - } - /> - - - -); - -export const SendConfirmationPage = ({ onCancel }: { onCancel: () => void }) => { - const { client, denom } = useAppContext(); - const { address, amount, fee, handleSend, transaction, resetTx, onDone } = useSendContext(); - - const calculateTotal = () => (Number(fee?.nym) + Number(amount?.amount)).toString(); - - return ( - - {transaction?.status === 'success' && ( - - )} - {transaction?.status === 'loading' && } - {transaction?.status === 'error' && ( - - {transaction.message} - - )} - - - - - - - - - - - - - ); -}; diff --git a/nym-browser-extension/src/pages/send/index.tsx b/nym-browser-extension/src/pages/send/index.tsx deleted file mode 100644 index c6aa42bbe4..0000000000 --- a/nym-browser-extension/src/pages/send/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useState } from 'react'; -import { Box, Divider, Stack, Typography } from '@mui/material'; -import { WalletAddressFormField } from '@nymproject/react/account/WalletAddressFormField'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { DecCoin } from '@nymproject/types'; -import { Address, Button } from 'src/components'; -import { PageLayout } from 'src/layouts/PageLayout'; -import { SendProvider, useAppContext, useSendContext } from 'src/context'; -import { SendConfirmationPage } from './Confirmation'; - -const SendPage = ({ onConfirm }: { onConfirm: () => void }) => { - const [isValidAddress, setIsValidAddress] = useState(false); - const [isValidAmount, setIsValidAmount] = useState(false); - - const { address, amount, handleChangeAddress, handleChangeAmount, handleGetFee } = useSendContext(); - const { balance } = useAppContext(); - - const handleNext = async () => { - if (address && amount) { - await handleGetFee(address, amount.amount); - onConfirm(); - } - }; - - return ( - - -
- handleChangeAddress(_address)} - onValidate={setIsValidAddress} - initialValue={address} - /> - handleChangeAmount(_amount)} - onValidate={(_: any, isValid: boolean) => setIsValidAmount(isValid)} - /> - - - Account balance - {balance} NYM - - - - Est. fee for this transaction will be calculated on the next page - - - - - - ); -}; - -export const Send = () => { - const [showConfirmation, setShowConfirmation] = useState(false); - - return ( - - {showConfirmation ? ( - setShowConfirmation(false)} /> - ) : ( - setShowConfirmation(true)} /> - )} - - ); -}; diff --git a/nym-browser-extension/src/pages/settings/index.tsx b/nym-browser-extension/src/pages/settings/index.tsx deleted file mode 100644 index 0beaaf3dc1..0000000000 --- a/nym-browser-extension/src/pages/settings/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export const Settings = () =>

Settings

; diff --git a/nym-browser-extension/src/pages/templates/Complete.tsx b/nym-browser-extension/src/pages/templates/Complete.tsx deleted file mode 100644 index a774516d4c..0000000000 --- a/nym-browser-extension/src/pages/templates/Complete.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { Button } from 'src/components/ui'; -import { CenteredLogoLayout } from 'src/layouts'; - -export const SetupCompleteTemplate = ({ - title, - description, - onDone, -}: { - title: string; - description: string; - onDone: () => void; -}) => ( - - - - } - /> -); diff --git a/nym-browser-extension/src/pages/templates/CreatePassword.tsx b/nym-browser-extension/src/pages/templates/CreatePassword.tsx deleted file mode 100644 index cb866d545b..0000000000 --- a/nym-browser-extension/src/pages/templates/CreatePassword.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { FormControlLabel, Checkbox, Stack, Typography, Box } from '@mui/material'; -import { TopLogoLayout } from 'src/layouts/TopLogo'; -import { PasswordInput } from '@nymproject/react/textfields/Password'; -import { PasswordStrength } from '@nymproject/react/password-strength/PasswordStrength'; -import { Button } from 'src/components/ui'; - -type TCreatePassword = { - canProceed: boolean; - password: string; - confirmPassword: string; - hasReadTerms: boolean; - setHasReadTerms: (hasReadTerms: boolean) => void; - setIsSafePassword: (isSafe: boolean) => void; - setConfirmPassword: (password: string) => void; - onNext: () => void; - setPassword: (password: string) => void; -}; - -export const CreatePasswordTemplate = ({ - canProceed, - onNext, - password, - setPassword, - confirmPassword, - setIsSafePassword, - setConfirmPassword, - setHasReadTerms, - hasReadTerms, -}: TCreatePassword) => ( - - Next - - } - > - - setPassword(_password)} - label="Password" - /> - - setIsSafePassword(isSafe)} /> - - - - setConfirmPassword(_password)} - label="Confirm password" - /> - - - I have read and agree with the Terms of use} - control={ setHasReadTerms(checked)} />} - /> - -); diff --git a/nym-browser-extension/src/pages/templates/ImportAccount.tsx b/nym-browser-extension/src/pages/templates/ImportAccount.tsx deleted file mode 100644 index 03a560f7e3..0000000000 --- a/nym-browser-extension/src/pages/templates/ImportAccount.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { TextField } from '@mui/material'; -import { Button } from 'src/components'; -import { TopLogoLayout } from 'src/layouts'; - -export const ImportAccountTemplate = ({ - userMnemonic, - onChangeUserMnemonic, - onNext, -}: { - userMnemonic: string; - onChangeUserMnemonic: (mnemonic: string) => void; - onNext: () => void; -}) => ( - - Next - - } - > - onChangeUserMnemonic(e.target.value)} - multiline - autoFocus={false} - fullWidth - inputProps={{ - style: { - height: '160px', - }, - }} - InputLabelProps={{ shrink: true }} - sx={{ - 'input::-webkit-textfield-decoration-container': { - alignItems: 'start', - }, - }} - /> - -); diff --git a/nym-browser-extension/src/pages/templates/SeedPhrase.tsx b/nym-browser-extension/src/pages/templates/SeedPhrase.tsx deleted file mode 100644 index f8c47150a8..0000000000 --- a/nym-browser-extension/src/pages/templates/SeedPhrase.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useRef, useState } from 'react'; -import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material'; -import { TopLogoLayout } from 'src/layouts/TopLogo'; -import { Button } from 'src/components/ui'; -import { generateMnemonmic } from 'src/validator-client'; - -export const SeedPhraseTemplate = ({ onNext }: { onNext: (seedPhrase: string) => void }) => { - const [isConfirmed, setIsconfirmed] = useState(false); - - const seedPhrase = useRef(generateMnemonmic()); - - return ( - onNext(seedPhrase.current)} - > - Next - - } - > - - - Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the future - - - - - setIsconfirmed(checked)} />} - /> - - - ); -}; diff --git a/nym-browser-extension/src/pages/templates/index.ts b/nym-browser-extension/src/pages/templates/index.ts deleted file mode 100644 index b7c8c43756..0000000000 --- a/nym-browser-extension/src/pages/templates/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Complete'; -export * from './CreatePassword'; -export * from './ImportAccount'; -export * from './SeedPhrase'; diff --git a/nym-browser-extension/src/routes/index.tsx b/nym-browser-extension/src/routes/index.tsx deleted file mode 100644 index a6cd93be4f..0000000000 --- a/nym-browser-extension/src/routes/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { BrowserRouter, MemoryRouter, Route, Routes } from 'react-router-dom'; -import { Home } from 'src/pages'; -import { ExtensionStorage } from '@nymproject/extension-storage'; -import { RegisterRoutes } from './register'; -import { UserRoutes } from './user'; -import { LoginRoutes } from './login'; - -const Router = process.env.NODE_ENV === 'development' ? BrowserRouter : MemoryRouter; - -export const AppRoutes = () => { - const [userHasAccount, setUserHasAccount] = useState(null); - - useEffect(() => { - const checkUserHasAccount = async () => { - const hasAccount = await ExtensionStorage.exists(); - setUserHasAccount(hasAccount); - }; - - checkUserHasAccount(); - }, []); - - if (userHasAccount === null) return null; - - return ( - - - : } /> - } /> - } /> - } /> - - - ); -}; diff --git a/nym-browser-extension/src/routes/login/index.tsx b/nym-browser-extension/src/routes/login/index.tsx deleted file mode 100644 index c61c45c095..0000000000 --- a/nym-browser-extension/src/routes/login/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useEffect } from 'react'; -import { Route, Routes, useNavigate } from 'react-router-dom'; -import { useAppContext } from 'src/context'; -import { ForgotPassword, Login } from 'src/pages/auth'; - -export const LoginRoutes = () => { - const { client } = useAppContext(); - const navigate = useNavigate(); - - useEffect(() => { - let route = '/login'; - - if (client) { - route = '/user/balance'; - } - - navigate(route); - }, [client]); - - return ( - - } /> - } /> - - ); -}; diff --git a/nym-browser-extension/src/routes/register/index.tsx b/nym-browser-extension/src/routes/register/index.tsx deleted file mode 100644 index 27210900e6..0000000000 --- a/nym-browser-extension/src/routes/register/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Route, Routes, useNavigate } from 'react-router-dom'; -import { RegisterContextProvider } from 'src/context/register'; -import { ImportAccount, SeedPhrase, SetupComplete } from 'src/pages/register'; -import { CreatePasswordOnExistingAccount } from 'src/pages/register/CreatePasswordOnExistingAccount'; -import { CreatePasswordOnNewAccount } from 'src/pages/register/CreatePasswordOnNewAccount'; - -export const RegisterRoutes = () => { - const navigate = useNavigate(); - - const handleSetUpComplete = () => { - navigate('/login'); - }; - - return ( - - - navigate('/register/seed-phrase')} />} - /> - } /> - } /> - { - navigate('/register/complete'); - }} - /> - } - /> - } /> - - - ); -}; diff --git a/nym-browser-extension/src/routes/user/accounts/accounts.tsx b/nym-browser-extension/src/routes/user/accounts/accounts.tsx deleted file mode 100644 index 63ef6e2abf..0000000000 --- a/nym-browser-extension/src/routes/user/accounts/accounts.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Route, Routes } from 'react-router-dom'; -import { RegisterContextProvider } from 'src/context/register'; -import { Accounts, AddAccount, ConfirmPassword, ImportAccount, NameAccount, SetupComplete } from 'src/pages'; - -export const AccountRoutes = () => ( - - - } /> - } /> - } /> - } /> - } /> - } /> - - -); diff --git a/nym-browser-extension/src/routes/user/index.tsx b/nym-browser-extension/src/routes/user/index.tsx deleted file mode 100644 index 30e9ea1e11..0000000000 --- a/nym-browser-extension/src/routes/user/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useEffect } from 'react'; -import { Route, Routes, useNavigate } from 'react-router-dom'; -import { useAppContext } from 'src/context'; -import { Delegation, BalancePage, Receive, Send, Settings } from 'src/pages'; -import { AccountRoutes } from './accounts/accounts'; - -export const UserRoutes = () => { - const { client } = useAppContext(); - const navigate = useNavigate(); - - useEffect(() => { - if (!client) navigate('/login'); - }, [client]); - - return ( - - } /> - } /> - } /> - } /> - } /> - } /> - - ); -}; diff --git a/nym-browser-extension/src/theme/NymBrowserExtensionTheme.tsx b/nym-browser-extension/src/theme/NymBrowserExtensionTheme.tsx deleted file mode 100644 index dfde09cbc0..0000000000 --- a/nym-browser-extension/src/theme/NymBrowserExtensionTheme.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { CssBaseline, PaletteMode } from '@mui/material'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; -import { getDesignTokens } from './theme'; -import '@assets/fonts/non-variable/fonts.css'; - -type TNymBrowserExtThemeProps = { mode: PaletteMode; children: React.ReactNode }; - -export const NymBrowserExtThemeWithMode = ({ mode, children }: TNymBrowserExtThemeProps) => { - const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]); - - return ( - - - {children} - - ); -}; diff --git a/nym-browser-extension/src/theme/mui-theme.d.ts b/nym-browser-extension/src/theme/mui-theme.d.ts deleted file mode 100644 index 6145e0d330..0000000000 --- a/nym-browser-extension/src/theme/mui-theme.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */ -import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles'; -import { PaletteMode } from '@mui/material'; - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This interface defines a palette used across Nym for branding - */ - interface NymPalette { - background: { light: string; dark: string }; - } - - interface NymPaletteVariant { - mode: PaletteMode; - } - - /** - * A palette definition only for the Nym Browser Extension that extends the Nym palette - */ - interface NymBrowserExtPalette { - nymBrowserExt: NymPaletteVariant; - } - - interface NymPaletteAndNymBrowserExtPalette { - nym: NymPalette & NymBrowserExtPalette; - } - - type NymPaletteAndNymBrowserExtPaletteOptions = Partial; - - /** - * Add anything not palette related to the theme here - */ - interface NymTheme {} - - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme {} - interface ThemeOptions extends Partial {} - interface Palette extends NymPaletteAndNymBrowserExtPalette {} - interface PaletteOptions extends NymPaletteAndNymBrowserExtPaletteOptions {} -} diff --git a/nym-browser-extension/src/theme/theme.ts b/nym-browser-extension/src/theme/theme.ts deleted file mode 100644 index b4ab3ee20e..0000000000 --- a/nym-browser-extension/src/theme/theme.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { PaletteMode } from '@mui/material'; -import { - PaletteOptions, - NymPalette, - NymBrowserExtPalette, - ThemeOptions, - createTheme, - NymPaletteVariant, -} from '@mui/material/styles'; - -//----------------------------------------------------------------------------------------------- -// Nym palette type definitions -// - -/** - * The Nym palette. - * - * IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct - * colours for dark/light mode. - */ -const nymPalette: NymPalette = { - /** emphasises important elements */ - background: { light: '#F4F6F8', dark: '#1D2125' }, -}; - -const darkMode: NymPaletteVariant = { - mode: 'dark', -}; - -const lightMode: NymPaletteVariant = { - mode: 'light', -}; - -/** - * Nym palette specific to the Nym Wallet - * - * IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct - * colours for dark/light mode. - */ -const nymBrowserExtPalette = (variant: NymPaletteVariant): NymBrowserExtPalette => ({ - nymBrowserExt: variant, -}); - -//----------------------------------------------------------------------------------------------- -// Nym palettes for light and dark mode -// - -/** - * Map a Nym palette variant onto the MUI palette - */ -const variantToMUIPalette = (_: NymPaletteVariant): PaletteOptions => ({ - primary: { - main: '#6750A4', - }, -}); - -/** - * Returns the Network Explorer palette for light mode. - */ -const createLightModePalette = (): PaletteOptions => ({ - nym: { - ...nymPalette, - ...nymBrowserExtPalette(lightMode), - }, - ...variantToMUIPalette(lightMode), -}); - -/** - * Returns the Network Explorer palette for dark mode. - */ -const createDarkModePalette = (): PaletteOptions => ({ - nym: { - ...nymPalette, - ...nymBrowserExtPalette(darkMode), - }, - ...variantToMUIPalette(darkMode), -}); - -/** - * IMPORANT: if you need to get the default MUI theme, use the following - * - * import { createTheme as systemCreateTheme } from '@mui/system'; - * - * // get the MUI system defaults for light mode - * const systemTheme = systemCreateTheme({ palette: { mode: 'light' } }); - * - * - * return { - * // change `primary` to default MUI `success` - * primary: { - * main: systemTheme.palette.success.main, - * }, - * nym: { - * ...nymPalette, - * ...nymWalletPalette, - * }, - * }; - */ - -//----------------------------------------------------------------------------------------------- -// Nym theme overrides -// - -/** - * Gets the theme options to be passed to `createTheme`. - * - * Based on pattern from https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette. - * - * @param mode The theme mode: 'light' or 'dark' - */ -export const getDesignTokens = (mode: PaletteMode): ThemeOptions => { - // first, create the palette from user's choice of light or dark mode - const { palette } = createTheme({ - palette: { - mode, - ...(mode === 'light' ? createLightModePalette() : createDarkModePalette()), - }, - }); - - // then customise theme and components - return { - typography: { - fontFamily: [ - 'Open Sans', - 'sans-serif', - 'BlinkMacSystemFont', - 'Roboto', - 'Oxygen', - 'Ubuntu', - 'Helvetica Neue', - ].join(','), - }, - shape: { - borderRadius: 8, - }, - palette, - }; -}; diff --git a/nym-browser-extension/src/types/global.ts b/nym-browser-extension/src/types/global.ts deleted file mode 100644 index 370aa18e22..0000000000 --- a/nym-browser-extension/src/types/global.ts +++ /dev/null @@ -1,5 +0,0 @@ -export declare namespace NodeJS { - interface ProcessEnv { - development: string; - } -} diff --git a/nym-browser-extension/src/types/index.ts b/nym-browser-extension/src/types/index.ts deleted file mode 100644 index cbb355622f..0000000000 --- a/nym-browser-extension/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tx'; diff --git a/nym-browser-extension/src/types/tx.ts b/nym-browser-extension/src/types/tx.ts deleted file mode 100644 index afe264e119..0000000000 --- a/nym-browser-extension/src/types/tx.ts +++ /dev/null @@ -1,9 +0,0 @@ -// TODO Add other transaction types later -type TTransactionType = 'send'; - -export type TTransaction = { - type: TTransactionType; - txHash?: string; - status: 'loading' | 'success' | 'error'; - message?: string; -}; diff --git a/nym-browser-extension/src/typings/jpeg.d.ts b/nym-browser-extension/src/typings/jpeg.d.ts deleted file mode 100644 index cfe3695fd5..0000000000 --- a/nym-browser-extension/src/typings/jpeg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.jpeg' { - const value: any; - export default value; -} diff --git a/nym-browser-extension/src/typings/json.d.ts b/nym-browser-extension/src/typings/json.d.ts deleted file mode 100644 index b72dd46ee5..0000000000 --- a/nym-browser-extension/src/typings/json.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.json' { - const content: any; - export default content; -} diff --git a/nym-browser-extension/src/typings/png.d.ts b/nym-browser-extension/src/typings/png.d.ts deleted file mode 100644 index dd84df40a4..0000000000 --- a/nym-browser-extension/src/typings/png.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.png' { - const content: any; - export default content; -} diff --git a/nym-browser-extension/src/typings/svg.d.ts b/nym-browser-extension/src/typings/svg.d.ts deleted file mode 100644 index 091d25e210..0000000000 --- a/nym-browser-extension/src/typings/svg.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.svg' { - const content: any; - export default content; -} diff --git a/nym-browser-extension/src/urls/index.ts b/nym-browser-extension/src/urls/index.ts deleted file mode 100644 index 10ab2d6aab..0000000000 --- a/nym-browser-extension/src/urls/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const blockExplorerUrl = process.env.BLOCK_EXPLORER_URL; -export const coinGeckoPriceAPI = 'https://api.coingecko.com/api/v3/simple/price?'; diff --git a/nym-browser-extension/src/utils/coin.ts b/nym-browser-extension/src/utils/coin.ts deleted file mode 100644 index 6b46d9b56e..0000000000 --- a/nym-browser-extension/src/utils/coin.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Big from 'big.js'; - -export const unymToNym = (unym: number | Big, dp = 4) => { - let nym; - try { - nym = Big(unym).div(1_000_000).toFixed(dp); - } catch (e: any) { - console.warn(`${unym} not a valid decimal number: ${e}`); - } - return Number(nym); -}; - -export const nymToUnym = (nym: number) => { - let unym; - try { - unym = Big(nym).mul(1_000_000); - } catch (e: any) { - console.warn(`unable to convert nym to unym: ${e}`); - } - return Number(unym); -}; diff --git a/nym-browser-extension/src/utils/crypto.ts b/nym-browser-extension/src/utils/crypto.ts deleted file mode 100644 index 122149ac60..0000000000 --- a/nym-browser-extension/src/utils/crypto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import cryptojs from 'crypto-js'; - -const encrypt = (mnemonic: string, password: string) => cryptojs.AES.encrypt(mnemonic, password).toString(); - -const decrypt = (cipher: string, password: string) => - cryptojs.AES.decrypt(cipher, password).toString(cryptojs.enc.Utf8); - -export { encrypt, decrypt }; diff --git a/nym-browser-extension/src/utils/fee.ts b/nym-browser-extension/src/utils/fee.ts deleted file mode 100644 index 7357c6181f..0000000000 --- a/nym-browser-extension/src/utils/fee.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const createFeeObject = (feeInUnyms?: number) => { - if (!feeInUnyms) return undefined; - - return { - amount: [{ amount: feeInUnyms.toString(), denom: 'unym' }], - gas: '100000', - }; -}; diff --git a/nym-browser-extension/src/utils/price.ts b/nym-browser-extension/src/utils/price.ts deleted file mode 100644 index 8f38ca18db..0000000000 --- a/nym-browser-extension/src/utils/price.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { coinGeckoPriceAPI } from 'src/urls'; - -export type Currency = 'gbp' | 'usd'; -export type TokenId = 'nym'; - -type ResponseMap = { [token in T]: { [currency in C]: number } }; - -const constructUrl = (tokenId: TokenId, currency: Currency) => - `${coinGeckoPriceAPI}ids=${tokenId}&vs_currencies=${currency}`; - -export async function getTokenPrice( - tokenId: T, - currency: C, -): Promise> { - const response = await fetch(constructUrl(tokenId, currency)); - const json = await response.json(); - return json; -} diff --git a/nym-browser-extension/src/validator-client/index.tsx b/nym-browser-extension/src/validator-client/index.tsx deleted file mode 100644 index 0e007e3c26..0000000000 --- a/nym-browser-extension/src/validator-client/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import ValidatorClient from '@nymproject/nym-validator-client'; -import { config } from 'src/config'; - -export const generateMnemonmic = () => ValidatorClient.randomMnemonic(); - -export const connectToValidator = async (mnemonic: string) => - ValidatorClient.connect( - mnemonic, - config.rpcUrl, - config.validatorUrl, - config.prefix, - config.mixnetContractAddress, - config.vestingContractAddress, - config.denom, - ); diff --git a/nym-browser-extension/storage/.gitignore b/nym-browser-extension/storage/.gitignore index 39e6cafa5c..821d2e38f2 100644 --- a/nym-browser-extension/storage/.gitignore +++ b/nym-browser-extension/storage/.gitignore @@ -1,2 +1,36 @@ pkg -target \ No newline at end of file +target +Cargo.lock + +*.wasm +*.js +*.ts +*.d.ts + +.vscode/ +.idea/ +*.swp +*.swo +*~ + +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + + +internal-dev/wasm/ \ No newline at end of file diff --git a/nym-browser-extension/storage/Cargo.toml b/nym-browser-extension/storage/Cargo.toml index f36511e735..ec61e945b8 100644 --- a/nym-browser-extension/storage/Cargo.toml +++ b/nym-browser-extension/storage/Cargo.toml @@ -1,9 +1,11 @@ [package] name = "extension-storage" -version = "1.3.0-rc.0" -edition = "2021" +version = "1.4.0-rc.0" +edition = "2024" license = "Apache-2.0" repository = "https://github.com/nymtech/nym" +description = "WebAssembly-based secure storage for browser extension mnemonics" +authors = ["Nym Technologies SA "] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,15 +13,17 @@ repository = "https://github.com/nymtech/nym" crate-type = ["cdylib", "rlib"] [dependencies] -bip39 = { workspace = true } -js-sys = { workspace = true } -serde-wasm-bindgen = { workspace = true } -thiserror = { workspace = true } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } -zeroize = { workspace = true } +bip39 = { workspace = true } +zeroize = { workspace = true } -console_error_panic_hook = { workspace = true, optional = true } +js-sys = { workspace = true } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +serde-wasm-bindgen = { workspace = true } + +thiserror = { workspace = true } + +console_error_panic_hook = { workspace = true, optional = true } wasm-utils = { path = "../../common/wasm/utils" } wasm-storage = { path = "../../common/wasm/storage" } diff --git a/nym-browser-extension/storage/Makefile b/nym-browser-extension/storage/Makefile index 80be2ad9da..137652b242 100644 --- a/nym-browser-extension/storage/Makefile +++ b/nym-browser-extension/storage/Makefile @@ -1,2 +1,65 @@ +# Nym Extension Storage - Build Configuration +# +# This Makefile helps build the WebAssembly storage component +# and run development examples. + wasm-pack: - wasm-pack build --scope nymproject --out-dir ../../dist/wasm/extension-storage + @echo "🔨 Building WebAssembly package..." + unset RUSTC_WRAPPER && wasm-pack build --target web --scope nymproject --out-dir ../../dist/wasm/extension-storage + @echo "✅ Build complete! Output in ../../dist/wasm/extension-storage" + +wasm-pack-dev: + @echo "🔨 Building WebAssembly package (development mode)..." + unset RUSTC_WRAPPER && wasm-pack build --dev --target web --scope nymproject --out-dir ../../dist/wasm/extension-storage + @echo "✅ Development build complete!" + + +copy-wasm-to-demo: + @echo "📁 Copying WASM files to demo directory..." + @mkdir -p internal-dev/wasm + @cp ../../dist/wasm/extension-storage/extension_storage.js internal-dev/wasm/ + @cp ../../dist/wasm/extension-storage/extension_storage_bg.wasm internal-dev/wasm/ + @cp ../../dist/wasm/extension-storage/extension_storage.d.ts internal-dev/wasm/ + @echo "✅ WASM files copied to internal-dev/wasm/" + + +clean: + @echo "🧹 Cleaning build artifacts..." + cargo clean + rm -rf ../../dist/wasm/extension-storage + rm -rf pkg + rm -rf internal-dev/wasm + @echo "✅ Clean complete!" + +demo: wasm-pack-dev copy-wasm-to-demo + cd internal-dev && node serve.js + + +check-deps: + @echo "🔍 Checking dependencies..." + @command -v cargo >/dev/null 2>&1 || { echo "❌ cargo is required but not installed. Please install Rust."; exit 1; } + @command -v wasm-pack >/dev/null 2>&1 || { echo "❌ wasm-pack is required but not installed. Run: cargo install wasm-pack"; exit 1; } + @echo "✅ All dependencies are installed!" + +help: + @echo "Nym Extension Storage Build Commands:" + @echo "" + @echo "📦 Building:" + @echo " make wasm-pack - Build optimized WASM package for production" + @echo " make wasm-pack-dev - Build WASM package with debug symbols" + @echo " make copy-wasm-to-demo - Copy WASM files to demo directory" + @echo "" + @echo "🧪 Development:" + @echo " make demo - Build and serve the interactive demo (Node.js)" + @echo " make check-deps - Verify all required tools are installed" + @echo "" + @echo "🧹 Maintenance:" + @echo " make clean - Remove all build artifacts" + @echo " make help - Show this help message" + @echo "" + @echo "💡 Quick start: make demo" + + +.DEFAULT_GOAL := help + +.PHONY: wasm-pack wasm-pack-dev copy-wasm-to-demo clean demo check-deps help diff --git a/nym-browser-extension/storage/internal-dev/index.html b/nym-browser-extension/storage/internal-dev/index.html new file mode 100644 index 0000000000..bcbf4bbd53 --- /dev/null +++ b/nym-browser-extension/storage/internal-dev/index.html @@ -0,0 +1,161 @@ + + + + + + Nym Extension Storage - Demo + + + +
+

🚀 Nym Extension Storage Demo

+ +
+ ⚠️ Demo Environment
+ This is a development demo. The mnemonics used here are for testing only and should never be used in production. +
+ +
+ 📋 How to use this demo:
+ 1. Open your browser's developer console (F12)
+ 2. The demo will run automatically when the page loads
+ 3. You can also use the interactive functions shown below
+ 4. Check the IndexedDB in your browser's dev tools to see stored data +
+ +
+ ✅ What this demo shows:
+ • Creating encrypted storage instances
+ • Storing and retrieving BIP39 mnemonics
+ • Managing multiple wallets
+ • Error handling for invalid data
+ • Cleaning up stored data +
+ +

Interactive Functions

+

Open the browser console and try these commands:

+ +
+ // Create a storage instance
+ const storage = await window.nymStorageDemo.createStorage();
+
+ // Run a quick test
+ await window.nymStorageDemo.quickTest(storage);
+
+ // Access test mnemonics
+ console.log(window.nymStorageDemo.mnemonics);
+
+ // Manual operations
+ await storage.store_mnemonic("my-wallet", "your mnemonic here...");
+
+ const mnemonic = await storage.read_mnemonic("my-wallet");
+ const exists = await storage.has_mnemonic("my-wallet");
+ const allKeys = await storage.get_all_mnemonic_keys();
+ await storage.remove_mnemonic("my-wallet"); +
+ +
+ + +
+ +

Architecture Overview

+

This storage component uses:

+
    +
  • Rust/WASM - Core storage logic compiled to WebAssembly
  • +
  • IndexedDB - Browser-native persistent storage
  • +
  • AES Encryption - Password-based encryption for sensitive data
  • +
  • BIP39 Validation - Ensures mnemonic phrases are valid
  • +
+ +
+ 🔒 Security Notes:
+ • All data is encrypted with your password before storage
+ • Mnemonics are validated against BIP39 standards
+ • Sensitive data is zeroed from memory when no longer needed
+ • Storage is isolated per browser origin +
+
+ + + + + \ No newline at end of file diff --git a/nym-browser-extension/storage/internal-dev/index.js b/nym-browser-extension/storage/internal-dev/index.js index ab69f6e686..86e2ea016f 100644 --- a/nym-browser-extension/storage/internal-dev/index.js +++ b/nym-browser-extension/storage/internal-dev/index.js @@ -1,73 +1,303 @@ +/** + * Nym Extension Storage - Internal Development Example + * + * This file demonstrates how to use the ExtensionStorage WASM module + * for securely storing and managing BIP39 mnemonics in browser extensions. + * + * To run this example: + * 1. Build the WASM module: `cd .. && make wasm-pack` + * 2. Serve this directory: `python3 -m http.server 8000` + * 3. Open http://localhost:8000 in your browser + * 4. Open the browser's developer console to see the output + */ -import { +import init, { ExtensionStorage, set_panic_hook -} from "@nymproject/storage-extension" +} from "./wasm/extension_storage.js" -// // current limitation of rust-wasm for async stuff : ( -// let client = null +/** + * Test mnemonics for demonstration + * Note: These are for testing only - never use these in production! + */ +const TEST_MNEMONICS = { + valid: { + wallet1: "figure aspect pill salute review sponsor army city muffin engine army kid rival chunk unit insect blouse paddle velvet shallow box crawl grace never", + wallet2: "salmon picture danger pill tomato hour hand chaos tray bargain frequent fuel scheme coil divert season lucky ginger mom stem mistake blanket lake suffer", + wallet3: "cat quiz circle letter trade unhappy quarter garlic sting gravity zone stock scatter merge account barrel forward fame club chest camp under crop connect", + wallet4: "mammal fashion rice two marble high brain achieve first harsh infant timber flush cloud hunt address brand immune tip identify aspect call beyond once" + }, + invalid: "this is not a valid mnemonic phrase" +}; -async function main() { - // // sets up better stack traces in case of in-rust panics - set_panic_hook(); +const STORAGE_PASSWORD = "my-super-secure-password-123"; - let storage = await new ExtensionStorage("my super duper password"); - - const goodMnemonic = "figure aspect pill salute review sponsor army city muffin engine army kid rival chunk unit insect blouse paddle velvet shallow box crawl grace never" - const badMnemonic = "foomp" - - let readEmpty = await storage.read_mnemonic("my-mnemonic1") - console.log("value initial:", readEmpty); - - try { - await storage.store_mnemonic("my-mnemonic1", badMnemonic); - } catch (e) { - console.log("store error: ",e) +/** + * Helper function to log results with better formatting + */ +function logResult(operation, result, error = null) { + const timestamp = new Date().toISOString(); + console.group(`🔍 [${timestamp}] ${operation}`); + + if (error) { + console.error("❌ Error:", error); + } else { + console.log("✅ Result:", result); } - - let anotherRead = await storage.read_mnemonic("my-mnemonic1") - console.log("value bad store:", anotherRead); - - await storage.store_mnemonic("my-mnemonic1", goodMnemonic) - - let yetAnotherRead = await storage.read_mnemonic("my-mnemonic1") - console.log("value good store:", yetAnotherRead); - - await storage.remove_mnemonic("my-mnemonic1") - - let finalRead = await storage.read_mnemonic("my-mnemonic1") - console.log("value removed:", finalRead); - - const anotherMnemonic = "salmon picture danger pill tomato hour hand chaos tray bargain frequent fuel scheme coil divert season lucky ginger mom stem mistake blanket lake suffer"; - const oneMore = "cat quiz circle letter trade unhappy quarter garlic sting gravity zone stock scatter merge account barrel forward fame club chest camp under crop connect" - - const key1 = "my-amazing-mnemonic" - const key2 = "my-other-mnemonic" - - await storage.store_mnemonic(key1, anotherMnemonic) - await storage.store_mnemonic(key2, oneMore) - - let allKeys = await storage.get_all_mnemonic_keys() - console.log("keys:", allKeys) - - const anotherOne = "mammal fashion rice two marble high brain achieve first harsh infant timber flush cloud hunt address brand immune tip identify aspect call beyond once" - const anotherKey = "some-mnemonic" - - let isPresent = await storage.has_mnemonic(anotherKey); - console.log("has mnemonic: ", isPresent) - - await storage.store_mnemonic(anotherKey, anotherOne) - - let isPresentNew = await storage.has_mnemonic(anotherKey); - console.log("has mnemonic: ", isPresentNew) - - await storage.remove_mnemonic(anotherKey) - - let isPresentEvenNewer = await storage.has_mnemonic(anotherKey); - console.log("has mnemonic: ", isPresentEvenNewer) - + + console.groupEnd(); } +/** + * Test basic storage operations + */ +async function testBasicOperations(storage) { + console.log("\n📦 Testing Basic Storage Operations"); + console.log("=" .repeat(50)); -// Let's get started! -main(); \ No newline at end of file + // Test storing a valid mnemonic + try { + await storage.store_mnemonic("test-wallet", TEST_MNEMONICS.valid.wallet1); + logResult("Store valid mnemonic", "Successfully stored"); + } catch (error) { + logResult("Store valid mnemonic", null, error); + } + + // Test reading the stored mnemonic + try { + const result = await storage.read_mnemonic("test-wallet"); + logResult("Read stored mnemonic", result); + } catch (error) { + logResult("Read stored mnemonic", null, error); + } + + // Test reading a non-existent mnemonic + try { + const result = await storage.read_mnemonic("non-existent"); + logResult("Read non-existent mnemonic", result); + } catch (error) { + logResult("Read non-existent mnemonic", null, error); + } + + // Test storing an invalid mnemonic + try { + await storage.store_mnemonic("invalid-wallet", TEST_MNEMONICS.invalid); + logResult("Store invalid mnemonic", "Should not reach here"); + } catch (error) { + logResult("Store invalid mnemonic", null, error.toString()); + } + + // Clean up + try { + await storage.remove_mnemonic("test-wallet"); + logResult("Remove test wallet", "Successfully removed"); + } catch (error) { + logResult("Remove test wallet", null, error); + } +} + +/** + * Test multiple wallet management + */ +async function testMultipleWallets(storage) { + console.log("\n👥 Testing Multiple Wallet Management"); + console.log("=" .repeat(50)); + + // Store multiple wallets + const walletNames = Object.keys(TEST_MNEMONICS.valid); + const walletMnemonics = Object.values(TEST_MNEMONICS.valid); + + for (let i = 0; i < walletNames.length; i++) { + try { + await storage.store_mnemonic(walletNames[i], walletMnemonics[i]); + logResult(`Store wallet: ${walletNames[i]}`, "Success"); + } catch (error) { + logResult(`Store wallet: ${walletNames[i]}`, null, error); + } + } + + // Get all wallet keys + try { + const allKeys = await storage.get_all_mnemonic_keys(); + logResult("Get all wallet keys", allKeys); + } catch (error) { + logResult("Get all wallet keys", null, error); + } + + // Check if specific wallets exist + for (const walletName of walletNames) { + try { + const exists = await storage.has_mnemonic(walletName); + logResult(`Check wallet exists: ${walletName}`, exists); + } catch (error) { + logResult(`Check wallet exists: ${walletName}`, null, error); + } + } + + // Read each wallet + for (const walletName of walletNames) { + try { + const mnemonic = await storage.read_mnemonic(walletName); + logResult(`Read wallet: ${walletName}`, `${mnemonic.substring(0, 20)}...`); + } catch (error) { + logResult(`Read wallet: ${walletName}`, null, error); + } + } +} + +/** + * Test wallet removal and cleanup + */ +async function testWalletRemoval(storage) { + console.log("\n🗑️ Testing Wallet Removal"); + console.log("=" .repeat(50)); + + const walletNames = Object.keys(TEST_MNEMONICS.valid); + + // Remove wallets one by one + for (const walletName of walletNames) { + try { + await storage.remove_mnemonic(walletName); + logResult(`Remove wallet: ${walletName}`, "Success"); + } catch (error) { + logResult(`Remove wallet: ${walletName}`, null, error); + } + + // Verify removal + try { + const exists = await storage.has_mnemonic(walletName); + logResult(`Verify removal: ${walletName}`, `Exists: ${exists}`); + } catch (error) { + logResult(`Verify removal: ${walletName}`, null, error); + } + } + + // Check final state + try { + const allKeys = await storage.get_all_mnemonic_keys(); + logResult("Final wallet count", `${allKeys.length} wallets remaining`); + } catch (error) { + logResult("Final wallet count", null, error); + } +} + +/** + * Test storage existence check + */ +async function testStorageExistence() { + console.log("\n💾 Testing Storage Existence"); + console.log("=" .repeat(50)); + + try { + const exists = await ExtensionStorage.exists(); + logResult("Storage database exists", exists); + } catch (error) { + logResult("Storage database exists", null, error); + } +} + +/** + * Main demonstration function + */ +async function runDemo() { + console.log("🚀 Nym Extension Storage Demo"); + console.log("=" .repeat(60)); + console.log("This demo shows how to use the ExtensionStorage WASM module"); + console.log("Check the browser console for detailed output\n"); + + try { + // Initialize the WASM module + console.log("🔧 Initializing WASM module..."); + await init(); + console.log("✅ WASM module initialized successfully\n"); + + // Set up better stack traces for Rust panics + set_panic_hook(); + } catch (error) { + console.error("💥 Failed to initialize WASM module:", error); + return; + } + + // Test storage existence before creating instance + await testStorageExistence(); + + try { + // Create storage instance with password + console.log("🔐 Creating storage instance with password..."); + const storage = await new ExtensionStorage(STORAGE_PASSWORD); + console.log("✅ Storage instance created successfully\n"); + + // Run all tests + await testBasicOperations(storage); + await testMultipleWallets(storage); + await testWalletRemoval(storage); + + // Test storage existence after operations + await testStorageExistence(); + + console.log("\n🎉 Demo completed successfully!"); + console.log("Check the IndexedDB in your browser's developer tools to see the stored data."); + + } catch (error) { + console.error("💥 Fatal error during demo:", error); + } +} + +/** + * Additional utility functions for interactive testing + */ +window.nymStorageDemo = { + /** + * Create a new storage instance for manual testing + */ + async createStorage(password = STORAGE_PASSWORD) { + try { + await init(); + set_panic_hook(); + return await new ExtensionStorage(password); + } catch (error) { + console.error("Failed to initialize WASM or create storage:", error); + throw error; + } + }, + + /** + * Quick test with a storage instance + */ + async quickTest(storage, name = "test", mnemonic = TEST_MNEMONICS.valid.wallet1) { + console.log(`Testing with wallet: ${name}`); + + await storage.store_mnemonic(name, mnemonic); + console.log("✅ Stored"); + + const retrieved = await storage.read_mnemonic(name); + console.log("✅ Retrieved:", retrieved); + + const exists = await storage.has_mnemonic(name); + console.log("✅ Exists:", exists); + + await storage.remove_mnemonic(name); + console.log("✅ Removed"); + + return "Test completed"; + }, + + /** + * Available test mnemonics + */ + mnemonics: TEST_MNEMONICS +}; + +// Start the demo when the page loads +document.addEventListener('DOMContentLoaded', () => { + console.log("📚 Interactive Demo Functions Available:"); + console.log("- window.nymStorageDemo.createStorage() - Create storage instance"); + console.log("- window.nymStorageDemo.quickTest(storage) - Run quick test"); + console.log("- window.nymStorageDemo.mnemonics - Test mnemonic phrases"); + console.log("\nStarting automated demo...\n"); + + runDemo(); +}); + +// Export for module usage +export { runDemo, TEST_MNEMONICS, STORAGE_PASSWORD }; \ No newline at end of file diff --git a/nym-browser-extension/storage/src/storage.rs b/nym-browser-extension/storage/src/storage.rs index 190105816a..ba484f46aa 100644 --- a/nym-browser-extension/storage/src/storage.rs +++ b/nym-browser-extension/storage/src/storage.rs @@ -18,7 +18,7 @@ use wasm_utils::check_promise_result; use wasm_utils::error::{PromisableResult, PromisableResultError}; use zeroize::Zeroizing; -const STORAGE_NAME: &str = "nym-wallet-extension"; +const STORAGE_NAME: &str = "nym-extension-storage"; const STORAGE_VERSION: u32 = 1; // v1 tables diff --git a/nym-browser-extension/tsconfig.eslint.json b/nym-browser-extension/tsconfig.eslint.json deleted file mode 100644 index d1da615e58..0000000000 --- a/nym-browser-extension/tsconfig.eslint.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx", - "noEmit": true - }, - "include": [ - ".storybook/*.js", - "webpack.*.js", - "src/**/*.js", - "src/**/*.jsx", - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.stories.*" - ], - "exclude": ["node_modules", "dist"] -} diff --git a/nym-browser-extension/tsconfig.json b/nym-browser-extension/tsconfig.json deleted file mode 100644 index 213619ac53..0000000000 --- a/nym-browser-extension/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "noFallthroughCasesInSwitch": true, - "skipLibCheck": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": false, - "jsx": "react-jsx", - "sourceMap": true, - "baseUrl": ".", - "paths": { - "@assets/*": ["../assets/*"] - }, - "noEmit": true - }, - "exclude": [ - "node_modules", - "dist", - "jest.config.js", - "webpack.config.js", - "webpack.prod.js", - "webpack.common.js", - "tsconfig.json" - ] -} diff --git a/nym-browser-extension/webpack.common.js b/nym-browser-extension/webpack.common.js deleted file mode 100644 index 3ca6344af8..0000000000 --- a/nym-browser-extension/webpack.common.js +++ /dev/null @@ -1,26 +0,0 @@ -const path = require('path'); -const { mergeWithRules } = require('webpack-merge'); -const { webpackCommon } = require('@nymproject/webpack'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(webpackCommon(__dirname), { - entry: path.resolve(__dirname, 'src/index.tsx'), - output: { - clean: true, - path: path.resolve(__dirname, 'dist'), - publicPath: '/', - }, - resolve: { - fallback: { - crypto: 'crypto-browserify', - stream: 'stream-browserify', - }, - }, - experiments: { asyncWebAssembly: true }, -}); diff --git a/nym-browser-extension/webpack.dev.js b/nym-browser-extension/webpack.dev.js deleted file mode 100644 index 4554968e15..0000000000 --- a/nym-browser-extension/webpack.dev.js +++ /dev/null @@ -1,68 +0,0 @@ -const { mergeWithRules } = require('webpack-merge'); -const webpack = require('webpack'); -const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); -const ReactRefreshTypeScript = require('react-refresh-typescript'); -const Dotenv = require('dotenv-webpack'); -const commonConfig = require('./webpack.common'); - -module.exports = mergeWithRules({ - module: { - rules: { - test: 'match', - use: 'replace', - }, - }, -})(commonConfig, { - mode: 'development', - devtool: 'inline-source-map', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - options: { - getCustomTransformers: () => ({ - before: [ReactRefreshTypeScript()], - }), - // `ts-loader` does not work with HMR unless `transpileOnly` is used. - // If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative. - transpileOnly: true, - }, - }, - ], - }, - plugins: [ - new ReactRefreshWebpackPlugin(), - - // this can be included automatically by the dev server, however build mode fails if missing - new webpack.HotModuleReplacementPlugin(), - new Dotenv({ path: './env.dev' }), - ], - - // recommended for faster rebuild - optimization: { - runtimeChunk: true, - removeAvailableModules: false, - removeEmptyChunks: false, - splitChunks: false, - }, - - cache: { - type: 'filesystem', - buildDependencies: { - // restart on config change - config: ['./webpack.dev.js'], - }, - }, - - devServer: { - port: 9000, - compress: true, - historyApiFallback: true, - hot: true, - client: { - overlay: false, - }, - }, -}); diff --git a/nym-browser-extension/webpack.prod.js b/nym-browser-extension/webpack.prod.js deleted file mode 100644 index 359dc9f9a5..0000000000 --- a/nym-browser-extension/webpack.prod.js +++ /dev/null @@ -1,21 +0,0 @@ -const path = require('path'); -const { default: merge } = require('webpack-merge'); -const common = require('./webpack.common'); -const CopyPlugin = require('copy-webpack-plugin'); -const Dotenv = require('dotenv-webpack'); - -module.exports = merge(common, { - mode: 'production', - entry: path.resolve(__dirname, 'src/index.tsx'), - plugins: [ - new CopyPlugin({ - patterns: [ - { - from: './src/manifest.json', - to: './', - }, - ], - }), - new Dotenv({ path: './.env' }), - ], -}); diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index ed887405d2..9aadebbe50 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -36,6 +36,6 @@ features = ["tokio"] [features] default = ["query-types"] -query-types = ["nym-http-api-common"] -openapi = ["utoipa"] +query-types = ["nym-http-api-common", "nym-http-api-common/output"] +openapi = ["utoipa", "nym-http-api-common/utoipa"] tsify = ["dep:tsify", "wasm-bindgen"] diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs index 22f04a7483..061b40a37d 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs @@ -268,12 +268,20 @@ pub struct WebhookTicketbookWalletSharesRequest { #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] #[cfg(feature = "query-types")] #[serde(default, rename_all = "kebab-case")] -pub struct TicketbookObtainQueryParams { - pub output: Option, - +pub struct TicketbookObtainParams { #[serde(default)] pub skip_webhook: bool, + #[serde(default)] + #[serde(flatten)] + pub global: GlobalDataParams, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct GlobalDataParams { pub include_master_verification_key: bool, pub include_coin_index_signatures: bool, @@ -281,6 +289,18 @@ pub struct TicketbookObtainQueryParams { pub include_expiration_date_signatures: bool, } +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct TicketbookObtainQueryParams { + pub output: Option, + + #[serde(default)] + #[serde(flatten)] + pub obtain_params: TicketbookObtainParams, +} + #[derive(Default, Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] #[cfg(feature = "query-types")] @@ -288,9 +308,7 @@ pub struct TicketbookObtainQueryParams { pub struct SharesQueryParams { pub output: Option, - pub include_master_verification_key: bool, - - pub include_coin_index_signatures: bool, - - pub include_expiration_date_signatures: bool, + #[serde(default)] + #[serde(flatten)] + pub global: GlobalDataParams, } diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs index c55dd4f914..d9247f15b8 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs @@ -26,8 +26,9 @@ pub fn new_client( base_url: impl IntoUrl, bearer_token: impl Into, ) -> Result { + let url = base_url.into_url()?; Ok(VpnApiClient { - inner: Client::builder(base_url)? + inner: Client::builder(url)? .with_user_agent(format!( "nym-credential-proxy-requests/{}", env!("CARGO_PKG_VERSION") diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 0d322ad34e..cba8c39d50 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,25 +1,23 @@ [package] name = "nym-credential-proxy" -version = "0.1.7" +version = "0.1.8" authors.workspace = true repository.workspace = true homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait.workspace = true axum.workspace = true anyhow.workspace = true bip39 = { workspace = true, features = ["zeroize"] } bs58.workspace = true cfg-if = { workspace = true } -colored.workspace = true clap = { workspace = true, features = ["derive", "env"] } -dotenvy.workspace = true futures.workspace = true humantime.workspace = true rand.workspace = true @@ -33,7 +31,6 @@ time.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } tokio-util = { workspace = true, features = ["rt"] } -tower.workspace = true tower-http = { workspace = true, features = ["cors"], optional = true } tracing.workspace = true url.workspace = true @@ -49,19 +46,26 @@ nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", " nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-ecash-contract-common = { path = "../../common/cosmwasm-smart-contracts/ecash-contract" } -nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } +nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa", "middleware"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = ["openapi"] } +nym-ecash-signer-check = { path = "../../common/ecash-signer-check" } + +nym-credential-proxy-lib = { path = "../../common/credential-proxy" } [dev-dependencies] tempfile = { workspace = true } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } [features] default = ["cors"] cors = ["tower-http"] + +[lints] +workspace = true \ No newline at end of file diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index cf5b25cf23..f500bd064f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -2,15 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::default_database_filepath; -use crate::webhook::ZkNymWebHookConfig; use clap::builder::ArgPredicate; -use clap::Parser; +use clap::{Args, Parser}; use nym_bin_common::bin_info; +use nym_credential_proxy_lib::error::CredentialProxyError; +use nym_credential_proxy_lib::webhook::ZkNymWebhook; use std::fs::create_dir_all; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::OnceLock; +use std::time::Duration; use tracing::info; +use url::Url; fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); @@ -64,10 +67,53 @@ pub struct Cli { )] pub(crate) max_concurrent_deposits: usize, + /// Specify the size of the deposits buffer the credential proxy should have available at any time + /// (default: 256) + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_DEPOSITS_BUFFER", + default_value_t = 256 + )] + pub(crate) deposits_buffer_size: usize, + + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_QUORUM_CHECK_INTERVAL", + default_value = "5m", + value_parser = humantime::parse_duration + )] + pub(crate) quorum_check_interval: Duration, + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] pub(crate) persistent_storage_path: Option, } +#[derive(Args, Debug, Clone)] +pub struct ZkNymWebHookConfig { + #[clap(long, env = "WEBHOOK_ZK_NYMS_URL")] + pub webhook_url: Url, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_ID")] + pub webhook_client_id: String, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_SECRET")] + pub webhook_client_secret: String, +} + +impl TryFrom for ZkNymWebhook { + type Error = CredentialProxyError; + + fn try_from(cfg: ZkNymWebHookConfig) -> Result { + Ok(ZkNymWebhook { + webhook_client_url: cfg + .webhook_url + .join(&cfg.webhook_client_id) + .map_err(|_| CredentialProxyError::InvalidWebhookUrl)?, + webhook_client_secret: cfg.webhook_client_secret, + }) + } +} + impl Cli { pub fn bind_address(&self) -> SocketAddr { // SAFETY: @@ -90,10 +136,7 @@ impl Cli { create_dir_all(parent).unwrap(); } - info!( - "setting the storage path path to {}", - default_path.display() - ); + info!("setting the storage path to {}", default_path.display()); default_path }) diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs deleted file mode 100644 index a6ee988de5..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -pub mod ticketbook; diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs deleted file mode 100644 index 5e2c858b26..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::deposit_maker::{DepositRequest, DepositResponse}; -use crate::error::VpnApiError; -use crate::http::state::ApiState; -use crate::storage::models::BlindedShares; -use futures::{stream, StreamExt}; -use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, - TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, - WebhookTicketbookWalletSharesRequest, -}; -use nym_credentials::IssuanceTicketBook; -use nym_credentials_interface::Base58; -use nym_crypto::asymmetric::ed25519; -use nym_validator_client::ecash::BlindSignRequestBody; -use nym_validator_client::nyxd::Coin; -use rand::rngs::OsRng; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; -use time::OffsetDateTime; -use tokio::sync::{oneshot, Mutex}; -use tokio::time::{timeout, Instant}; -use tracing::{debug, error, info, instrument, warn}; -use uuid::Uuid; - -// use the same type alias as our contract without importing the whole thing just for this single line -pub type NodeId = u64; - -#[instrument(skip(state), ret, err(Display))] -async fn make_deposit( - state: &ApiState, - pub_key: ed25519::PublicKey, - deposit_amount: &Coin, -) -> Result { - let start = Instant::now(); - let (on_done_tx, on_done_rx) = oneshot::channel(); - let request = DepositRequest::new(pub_key, deposit_amount, on_done_tx); - state.request_deposit(request).await; - - let time_taken = start.elapsed(); - let formatted = humantime::format_duration(time_taken); - - let Ok(deposit_response) = on_done_rx.await else { - error!("failed to receive deposit response: the corresponding sender channel got dropped by the DepositMaker!"); - return Err(VpnApiError::DepositFailure); - }; - - if time_taken > Duration::from_secs(20) { - warn!("attempting to resolve deposit request took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") - } else { - debug!("attempting to resolve deposit request took {formatted}") - } - - deposit_response.ok_or(VpnApiError::DepositFailure) -} - -#[instrument( - skip(state, request_data, request, requested_on), - fields( - expiration_date = %request_data.expiration_date, - ticketbook_type = %request_data.ticketbook_type - ) -)] -pub(crate) async fn try_obtain_wallet_shares( - state: &ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookRequest, -) -> Result, VpnApiError> { - let mut rng = OsRng; - - let ed25519_keypair = ed25519::KeyPair::new(&mut rng); - - let epoch = state.current_epoch_id().await?; - let deposit_amount = state.deposit_amount().await?; - let threshold = state.ecash_threshold(epoch).await?; - let expiration_date = request_data.expiration_date; - - // before we commit to making the deposit, ensure we have required signatures cached and stored - let _ = state.master_verification_key(Some(epoch)).await?; - let _ = state.master_coin_index_signatures(Some(epoch)).await?; - let _ = state - .master_expiration_date_signatures(expiration_date) - .await?; - let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); - - let DepositResponse { - deposit_id, - tx_hash, - } = make_deposit(state, *ed25519_keypair.public_key(), &deposit_amount).await?; - - info!(deposit_id = %deposit_id, "deposit finished"); - - // store the deposit information so if we fail, we could perhaps still reuse it for another issuance - state - .storage() - .insert_deposit_data( - deposit_id, - tx_hash, - requested_on, - request, - deposit_amount, - &request_data.ecash_pubkey, - &ed25519_keypair, - ) - .await?; - - let plaintext = - IssuanceTicketBook::request_plaintext(&request_data.withdrawal_request, deposit_id); - let signature = ed25519_keypair.private_key().sign(plaintext); - - let credential_request = BlindSignRequestBody::new( - request_data.withdrawal_request.into(), - deposit_id, - signature, - request_data.ecash_pubkey, - request_data.expiration_date, - request_data.ticketbook_type, - ); - - let wallet_shares = Arc::new(Mutex::new(HashMap::new())); - - info!("attempting to contract all nym-apis for the partial wallets..."); - stream::iter(ecash_api_clients) - .for_each_concurrent(None, |client| async { - // move the client into the block - let client = client; - - debug!("contacting {client} for blinded partial wallet"); - let res = timeout( - Duration::from_secs(5), - client.api_client.blind_sign(&credential_request), - ) - .await - .map_err(|_| VpnApiError::EcashApiRequestTimeout { - client_repr: client.to_string(), - }) - .and_then(|res| res.map_err(Into::into)); - - // 1. try to store it - if let Err(err) = state - .storage() - .insert_partial_wallet_share( - deposit_id, - epoch, - expiration_date, - client.node_id, - &res, - ) - .await - { - error!("failed to persist issued partial share: {err}") - } - - // 2. add it to the map - match res { - Ok(share) => { - wallet_shares - .lock() - .await - .insert(client.node_id, share.blinded_signature); - } - Err(err) => { - error!("failed to obtain partial blinded wallet share from {client}: {err}") - } - } - }) - .await; - - // SAFETY: the futures have completed, so we MUST have the only arc reference - #[allow(clippy::unwrap_used)] - let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner(); - let shares = wallet_shares.len(); - - if shares < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfCredentials { - available: shares, - threshold, - }); - } - - Ok(wallet_shares - .into_iter() - .map(|(node_index, share)| WalletShare { - node_index, - bs58_encoded_share: share.to_bs58(), - }) - .collect()) -} - -// same as try_obtain_wallet_shares, but writes failures into the db -async fn try_obtain_wallet_shares_async( - state: &ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookRequest, - device_id: &str, - credential_id: &str, -) -> Result, VpnApiError> { - let shares = match try_obtain_wallet_shares(state, request, requested_on, request_data).await { - Ok(shares) => shares, - Err(err) => { - let obtained = match err { - VpnApiError::InsufficientNumberOfCredentials { available, .. } => available, - _ => 0, - }; - - // currently there's no retry mechanisms, but, who knows, that might change - if let Err(err) = state - .storage() - .update_pending_async_blinded_shares_error( - obtained, - device_id, - credential_id, - &err.to_string(), - ) - .await - { - error!("failed to update database with the error information: {err}") - } - return Err(err); - } - }; - - Ok(shares) -} - -async fn try_obtain_blinded_ticketbook_async_inner( - state: &ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookAsyncRequest, - params: TicketbookObtainQueryParams, - pending: &BlindedShares, -) -> Result<(), VpnApiError> { - let epoch_id = state.current_epoch_id().await?; - - let device_id = &request_data.device_id; - let credential_id = &request_data.credential_id; - let secret = request_data.secret.clone(); - - // 1. try to obtain global data - let ( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - ) = state - .global_data( - params.include_master_verification_key, - params.include_coin_index_signatures, - params.include_expiration_date_signatures, - epoch_id, - request_data.inner.expiration_date, - ) - .await?; - - // 2. try to obtain shares (failures are written to the DB) - let shares = try_obtain_wallet_shares_async( - state, - request, - requested_on, - request_data.inner, - device_id, - credential_id, - ) - .await?; - - // 3. update the storage, if possible - // (as long as we can trigger webhook, we should still be good) - if let Err(err) = state - .storage() - .update_pending_async_blinded_shares_issued(shares.len(), device_id, credential_id) - .await - { - error!(uuid = %request, "failed to update db with issued information: {err}") - } - - // 4. build the webhook request body - let data = Some(TicketbookWalletSharesResponse { - epoch_id, - shares, - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - }); - - let ticketbook_wallet_shares = WebhookTicketbookWalletShares { - id: pending.id, - status: pending.status.to_string(), - device_id: device_id.clone(), - credential_id: credential_id.clone(), - data, - error_message: None, - created: pending.created, - updated: pending.updated, - }; - - let webhook_request = WebhookTicketbookWalletSharesRequest { - ticketbook_wallet_shares, - secret, - }; - - // 5. call the webhook - state - .zk_nym_web_hook() - .try_trigger(request, &webhook_request) - .await; - - Ok(()) -} - -async fn try_trigger_webhook_request_for_error( - state: &ApiState, - request: Uuid, - request_data: TicketbookAsyncRequest, - pending: &BlindedShares, - error_message: String, -) -> Result<(), VpnApiError> { - let device_id = &request_data.device_id; - let credential_id = &request_data.credential_id; - let secret = request_data.secret.clone(); - - let ticketbook_wallet_shares = WebhookTicketbookWalletShares { - id: pending.id, - status: "error".to_string(), - device_id: device_id.clone(), - credential_id: credential_id.clone(), - data: None, - error_message: Some(error_message), - created: pending.created, - updated: pending.updated, - }; - - let webhook_request = WebhookTicketbookWalletSharesRequest { - ticketbook_wallet_shares, - secret, - }; - - state - .zk_nym_web_hook() - .try_trigger(request, &webhook_request) - .await; - - Ok(()) -} - -#[instrument(skip_all, fields(credential_id = %request_data.credential_id, device_id = %request_data.device_id))] -#[allow(clippy::too_many_arguments)] -pub(crate) async fn try_obtain_blinded_ticketbook_async( - state: ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookAsyncRequest, - params: TicketbookObtainQueryParams, - pending: BlindedShares, -) { - let skip_webhook = params.skip_webhook; - if let Err(err) = try_obtain_blinded_ticketbook_async_inner( - &state, - request, - requested_on, - request_data.clone(), - params, - &pending, - ) - .await - { - if skip_webhook { - info!(uuid = %request,"the webhook is not going to be called for this request"); - return; - } - - // post to the webhook to notify of errors on this side - if let Err(webhook_err) = try_trigger_webhook_request_for_error( - &state, - request, - request_data, - &pending, - format!("Failed to get ticketbook: {err}"), - ) - .await - { - error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}") - } - error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") - } else { - info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") - } -} diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs deleted file mode 100644 index 2ec685d656..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::VpnApiError; -use crate::http::state::ChainClient; -use nym_crypto::asymmetric::ed25519; -use nym_ecash_contract_common::deposit::DepositId; -use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; -use nym_validator_client::nyxd::{Coin, Hash}; -use tokio::sync::{mpsc, oneshot}; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -#[derive(Debug)] -pub(crate) struct DepositResponse { - pub tx_hash: Hash, - pub deposit_id: DepositId, -} - -pub(crate) struct DepositRequest { - pubkey: ed25519::PublicKey, - deposit_amount: Coin, - on_done: oneshot::Sender>, -} - -impl DepositRequest { - pub(crate) fn new( - pubkey: ed25519::PublicKey, - deposit_amount: &Coin, - on_done: oneshot::Sender>, - ) -> Self { - DepositRequest { - pubkey, - deposit_amount: deposit_amount.clone(), - on_done, - } - } -} - -pub(crate) type DepositRequestReceiver = mpsc::Receiver; - -pub(crate) fn new_control_channels( - max_concurrent_deposits: usize, -) -> (DepositRequestSender, DepositRequestReceiver) { - let (tx, rx) = mpsc::channel(max_concurrent_deposits); - (tx.into(), rx) -} - -#[derive(Debug, Clone)] -pub struct DepositRequestSender(mpsc::Sender); - -impl From> for DepositRequestSender { - fn from(inner: mpsc::Sender) -> Self { - DepositRequestSender(inner) - } -} - -impl DepositRequestSender { - pub(crate) async fn request_deposit(&self, request: DepositRequest) { - if self.0.send(request).await.is_err() { - error!("failed to request deposit: the DepositMaker must have died!") - } - } -} - -pub(crate) struct DepositMaker { - client: ChainClient, - max_concurrent_deposits: usize, - deposit_request_sender: DepositRequestSender, - deposit_request_receiver: DepositRequestReceiver, - short_sha: &'static str, - cancellation_token: CancellationToken, -} - -impl DepositMaker { - pub(crate) fn new( - short_sha: &'static str, - client: ChainClient, - max_concurrent_deposits: usize, - cancellation_token: CancellationToken, - ) -> Self { - let (deposit_request_sender, deposit_request_receiver) = - new_control_channels(max_concurrent_deposits); - - DepositMaker { - client, - max_concurrent_deposits, - deposit_request_sender, - deposit_request_receiver, - short_sha, - cancellation_token, - } - } - - pub(crate) fn deposit_request_sender(&self) -> DepositRequestSender { - self.deposit_request_sender.clone() - } - - pub(crate) async fn process_deposit_requests( - &mut self, - requests: Vec, - ) -> Result<(), VpnApiError> { - let chain_write_permit = self.client.start_chain_tx().await; - - info!("starting deposits"); - let mut contents = Vec::new(); - let mut replies = Vec::new(); - for request in requests { - // check if the channel is still open in case the receiver client has cancelled the request - if request.on_done.is_closed() { - warn!( - "the request for deposit from {} got cancelled", - request.pubkey - ); - continue; - } - - contents.push((request.pubkey.to_base58_string(), request.deposit_amount)); - replies.push(request.on_done); - } - - let deposits_res = chain_write_permit - .make_deposits(self.short_sha, contents) - .await; - let execute_res = match deposits_res { - Ok(res) => res, - Err(err) => { - // we have to let requesters know the deposit(s) failed - for reply in replies { - if reply.send(None).is_err() { - warn!("one of the deposit requesters has been terminated") - } - } - return Err(err); - } - }; - - let tx_hash = execute_res.transaction_hash; - info!("{} deposits made in transaction: {tx_hash}", replies.len()); - - let contract_data = match execute_res.to_contract_data() { - Ok(contract_data) => contract_data, - Err(err) => { - // that one is tricky. deposits technically got made, but we somehow failed to parse response, - // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted - // because it requires some serious MANUAL intervention - error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); - self.cancellation_token.cancel(); - return Err(VpnApiError::DepositFailure); - } - }; - - if contract_data.len() != replies.len() { - // another critical failure, that one should be quite impossible and thus has to be manually inspected - error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len(), replies.len()); - self.cancellation_token.cancel(); - return Err(VpnApiError::DepositFailure); - } - - for (reply_channel, response) in replies.into_iter().zip(contract_data) { - let response_index = response.message_index; - let deposit_id = match response.parse_singleton_u32_contract_data() { - Ok(deposit_id) => deposit_id, - Err(err) => { - // another impossibility - error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); - self.cancellation_token.cancel(); - return Err(VpnApiError::DepositFailure); - } - }; - - if reply_channel - .send(Some(DepositResponse { - deposit_id, - tx_hash, - })) - .is_err() - { - warn!("one of the deposit requesters has been terminated. deposit {deposit_id} will remain unclaimed!"); - // this shouldn't happen as the requester task shouldn't be killed, but it's not a critical failure - // we just lost some tokens, but it's not an undefined on-chain behaviour - } - } - - Ok(()) - } - - pub async fn run_forever(mut self) { - info!("starting the deposit maker task"); - loop { - let mut receive_buffer = Vec::with_capacity(self.max_concurrent_deposits); - tokio::select! { - _ = self.cancellation_token.cancelled() => { - break - } - received = self.deposit_request_receiver.recv_many(&mut receive_buffer, self.max_concurrent_deposits) => { - debug!("received {received} deposit requests"); - if let Err(err) = self.process_deposit_requests(receive_buffer).await { - error!("failed to process received deposit requests: {err}") - } - } - } - } - } -} diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index 8a528ff575..d4d3742939 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -1,59 +1,12 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::{cli::Cli, http::HttpServer}; use nym_bin_common::bin_info; -use time::OffsetDateTime; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -use crate::{ - cli::Cli, - deposit_maker::DepositMaker, - error::VpnApiError, - http::{ - state::{ApiState, ChainClient}, - HttpServer, - }, - storage::VpnApiStorage, - tasks::StoragePruner, -}; - -pub struct LockTimer { - created: OffsetDateTime, - message: String, -} - -impl LockTimer { - pub fn new>(message: S) -> Self { - LockTimer { - message: message.into(), - ..Default::default() - } - } -} - -impl Drop for LockTimer { - fn drop(&mut self) { - let time_taken = OffsetDateTime::now_utc() - self.created; - let time_taken_formatted = humantime::format_duration(time_taken.unsigned_abs()); - if time_taken > time::Duration::SECOND * 10 { - warn!(time_taken = %time_taken_formatted, "{}", self.message) - } else if time_taken > time::Duration::SECOND * 5 { - info!(time_taken = %time_taken_formatted, "{}", self.message) - } else { - debug!(time_taken = %time_taken_formatted, "{}", self.message) - }; - } -} - -impl Default for LockTimer { - fn default() -> Self { - LockTimer { - created: OffsetDateTime::now_utc(), - message: "released the lock".to_string(), - } - } -} +use nym_credential_proxy_lib::error::CredentialProxyError; +use nym_credential_proxy_lib::storage::CredentialProxyStorage; +use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager; +use tracing::{error, info}; pub async fn wait_for_signal() { use tokio::signal::unix::{signal, SignalKind}; @@ -77,6 +30,7 @@ pub async fn wait_for_signal() { } } +#[allow(clippy::panic)] fn build_sha_short() -> &'static str { let bin_info = bin_info!(); if bin_info.commit_sha.len() < 7 { @@ -91,51 +45,34 @@ fn build_sha_short() -> &'static str { &bin_info.commit_sha[..7] } -pub(crate) async fn run_api(cli: Cli) -> Result<(), VpnApiError> { - // create the tasks +pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { let bind_address = cli.bind_address(); - - let storage = VpnApiStorage::init(cli.persistent_storage_path()).await?; + let storage = CredentialProxyStorage::init(cli.persistent_storage_path()).await?; let mnemonic = cli.mnemonic; let auth_token = cli.http_auth_token; let webhook_cfg = cli.webhook; - let chain_client = ChainClient::new(mnemonic)?; - let cancellation_token = CancellationToken::new(); - let deposit_maker = DepositMaker::new( + let ticketbook_manager = TicketbookManager::new( build_sha_short(), - chain_client.clone(), + cli.quorum_check_interval, + cli.deposits_buffer_size, cli.max_concurrent_deposits, - cancellation_token.clone(), - ); - - let deposit_request_sender = deposit_maker.deposit_request_sender(); - let api_state = ApiState::new( - storage.clone(), - webhook_cfg, - chain_client, - deposit_request_sender, - cancellation_token.clone(), + storage, + mnemonic, + webhook_cfg.try_into()?, ) .await?; - let http_server = HttpServer::new( - bind_address, - api_state.clone(), - auth_token, - cancellation_token.clone(), - ); - let storage_pruner = StoragePruner::new(cancellation_token, storage); - // spawn all the tasks - api_state.try_spawn(http_server.run_forever()); - api_state.try_spawn(storage_pruner.run_forever()); - api_state.try_spawn(deposit_maker.run_forever()); + let http_server = HttpServer::new(bind_address, ticketbook_manager.clone(), auth_token); + + // spawn the http server as a separate task / thread(-ish) + http_server.spawn_as_task(); // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) wait_for_signal().await; // cancel all the tasks and wait for all task to terminate - api_state.cancel_and_wait().await; + ticketbook_manager.cancel_and_wait().await; Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs deleted file mode 100644 index 844e1510c0..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::VpnApiError; -use crate::http::types::RequestError; -use axum::http::StatusCode; -use rand::rngs::OsRng; -use rand::RngCore; -use tracing::warn; -use uuid::Uuid; - -pub fn random_uuid() -> Uuid { - let mut bytes = [0u8; 16]; - let mut rng = OsRng; - rng.fill_bytes(&mut bytes); - Uuid::from_bytes(bytes) -} - -pub fn db_failure(err: VpnApiError, uuid: Uuid) -> Result { - warn!("db failure: {err}"); - Err(RequestError::new_with_uuid( - format!("oh no, something went wrong {err}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )) -} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs index e9359ac16a..de37dbba58 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs @@ -1,56 +1,54 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; use crate::http::router::build_router; -use crate::http::state::ApiState; -use axum::Router; +use nym_credential_proxy_lib::error::CredentialProxyError; +use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager; use std::net::SocketAddr; -use tokio_util::sync::CancellationToken; use tracing::info; -pub mod helpers; pub mod router; pub mod state; -pub mod types; pub struct HttpServer { bind_address: SocketAddr, - cancellation: CancellationToken, - router: Router, + ticketbook_manager: TicketbookManager, + auth_token: String, } impl HttpServer { pub fn new( bind_address: SocketAddr, - state: ApiState, + ticketbook_manager: TicketbookManager, auth_token: String, - cancellation: CancellationToken, ) -> Self { HttpServer { bind_address, - cancellation, - router: build_router(state, auth_token), + ticketbook_manager, + auth_token, } } - pub async fn run_forever(self) -> Result<(), VpnApiError> { - let address = self.bind_address; - info!("starting the http server on http://{address}"); + pub fn spawn_as_task(self) { + let cancellation = self.ticketbook_manager.shutdown_token(); - let listener = tokio::net::TcpListener::bind(address) + let ticketbook_manager = self.ticketbook_manager.clone(); + ticketbook_manager.try_spawn_in_background(async move { + let address = self.bind_address; + let router = build_router(self.ticketbook_manager, self.auth_token); + info!("starting the http server on http://{address}"); + + let listener = tokio::net::TcpListener::bind(address) + .await + .map_err(|source| CredentialProxyError::SocketBindFailure { address, source })?; + + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async move { cancellation.cancelled().await }) .await - .map_err(|source| VpnApiError::SocketBindFailure { address, source })?; - - let cancellation = self.cancellation; - - axum::serve( - listener, - self.router - .into_make_service_with_connect_info::(), - ) - .with_graceful_shutdown(async move { cancellation.cancelled().await }) - .await - .map_err(|source| VpnApiError::HttpServerFailure { source }) + .map_err(|source| CredentialProxyError::HttpServerFailure { source }) + }); } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs index 530d2bdad1..e09531af1e 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs @@ -1,27 +1,19 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::credentials::ticketbook::{ - try_obtain_blinded_ticketbook_async, try_obtain_wallet_shares, -}; -use crate::http::helpers::random_uuid; use crate::http::state::ApiState; -use crate::http::types::RequestError; -use crate::nym_api_helpers::ensure_sane_expiration_date; use axum::extract::{Query, State}; -use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Json, Router}; -use nym_compact_ecash::Base58; +use nym_credential_proxy_lib::helpers::random_uuid; +use nym_credential_proxy_lib::http_helpers::RequestError; use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKey, + CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse, }; use nym_credential_proxy_requests::routes::api::v1::ticketbook; use nym_http_api_common::{FormattedResponse, OutputParams}; -use time::OffsetDateTime; -use tracing::{error, info, span, warn, Instrument, Level}; pub(crate) mod shares; @@ -68,61 +60,15 @@ pub(crate) async fn obtain_ticketbook_shares( Json(payload): Json, ) -> Result { let uuid = random_uuid(); - let requested_on = OffsetDateTime::now_utc(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); - async move { - info!(""); + let response = state + .inner_state() + .obtain_ticketbook_shares(uuid, payload, params.obtain_params.global) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - let output = params.output.unwrap_or_default(); - - state.ensure_not_in_epoch_transition(Some(uuid)).await?; - let epoch_id = state - .current_epoch_id() - .await - .map_err(|err| RequestError::new_server_error(err, uuid))?; - - if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { - warn!("failure due to invalid expiration date"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::BAD_REQUEST, - )); - } - - // if additional data was requested, grab them first in case there are any cache/network issues - let ( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - ) = state - .response_global_data( - params.include_master_verification_key, - params.include_expiration_date_signatures, - params.include_coin_index_signatures, - epoch_id, - payload.expiration_date, - uuid, - ) - .await?; - - let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) - .await - .inspect_err(|err| warn!("request failure: {err}")) - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - - info!("request was successful!"); - Ok(output.to_response(TicketbookWalletSharesResponse { - epoch_id, - shares, - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - })) - } - .instrument(span) - .await + Ok(output.to_response(response)) } /// Attempt to obtain blinded shares of an ecash ticketbook wallet asynchronously @@ -159,72 +105,15 @@ pub(crate) async fn obtain_ticketbook_shares_async( Json(payload): Json, ) -> Result { let uuid = random_uuid(); - let requested_on = OffsetDateTime::now_utc(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); - async move { - info!(""); - let output = params.output.unwrap_or_default(); + let response = state + .inner_state() + .obtain_ticketbook_shares_async(uuid, payload, params.obtain_params) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // 1. perform basic validation - state.ensure_not_in_epoch_transition(Some(uuid)).await?; - - if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { - warn!("failure due to invalid expiration date"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::BAD_REQUEST, - )); - } - - // 2. store the request to retrieve the id - let pending = match state - .storage() - .insert_new_pending_async_shares_request( - uuid, - &payload.device_id, - &payload.credential_id, - ) - .await - { - Err(err) => { - error!("failed to insert new pending async shares: {err}"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::CONFLICT, - )); - } - Ok(pending) => pending, - }; - let id = pending.id; - - // 3. try to spawn a new task attempting to resolve the request - if state - .try_spawn(try_obtain_blinded_ticketbook_async( - state.clone(), - uuid, - requested_on, - payload, - params, - pending, - )) - .is_none() - { - // we're going through the shutdown - return Err(RequestError::new_with_uuid( - "server shutdown in progress", - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); - } - - // 4. in the meantime, return the id to the user - Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) - } - .instrument(span) - .await + Ok(output.to_response(response)) } /// Obtain the current value of the bandwidth voucher deposit @@ -251,15 +140,14 @@ pub(crate) async fn current_deposit( State(state): State, ) -> Result { let output = output.output.unwrap_or_default(); - let current_deposit = state - .deposit_amount() - .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(output.to_response(DepositResponse { - current_deposit_amount: current_deposit.amount, - current_deposit_denom: current_deposit.denom, - })) + let response = state + .inner_state() + .current_deposit() + .await + .map_err(RequestError::new_plain_error)?; + + Ok(output.to_response(response)) } /// Obtain partial verification keys of all signers for the current epoch. @@ -288,28 +176,13 @@ pub(crate) async fn partial_verification_keys( ) -> Result { let output = output.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(None).await?; - - let epoch_id = state - .current_epoch_id() + let response = state + .inner_state() + .partial_verification_keys() .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + .map_err(RequestError::new_plain_error)?; - let signers = state - .ecash_clients(epoch_id) - .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - - Ok(output.to_response(PartialVerificationKeysResponse { - epoch_id, - keys: signers - .iter() - .map(|signer| PartialVerificationKey { - node_index: signer.node_id, - bs58_encoded_key: signer.verification_key.to_bs58(), - }) - .collect(), - })) + Ok(output.to_response(response)) } /// Obtain the master verification key for the current epoch. @@ -338,22 +211,13 @@ pub(crate) async fn master_verification_key( ) -> Result { let output = output.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(None).await?; - - let epoch_id = state - .current_epoch_id() + let response = state + .inner_state() + .master_verification_key() .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + .map_err(RequestError::new_plain_error)?; - let key = state - .master_verification_key(Some(epoch_id)) - .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - - Ok(output.to_response(MasterVerificationKeyResponse { - epoch_id, - bs58_encoded_key: key.to_bs58(), - })) + Ok(output.to_response(response)) } /// Obtain the id of the current epoch. @@ -383,14 +247,13 @@ pub(crate) async fn current_epoch( ) -> Result { let output = output.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(None).await?; - - let epoch_id = state - .current_epoch_id() + let response = state + .inner_state() + .current_epoch() .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + .map_err(RequestError::new_plain_error)?; - Ok(output.to_response(CurrentEpochResponse { epoch_id })) + Ok(output.to_response(response)) } pub(crate) fn routes() -> Router { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs index 8b3dacb82d..b41173e47e 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs @@ -1,76 +1,18 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; -use crate::http::helpers::{db_failure, random_uuid}; use crate::http::router::api::v1::ticketbook::FormattedTicketbookWalletSharesResponse; use crate::http::state::ApiState; -use crate::http::types::RequestError; -use crate::storage::models::MinimalWalletShare; use axum::extract::{Path, Query, State}; -use axum::http::StatusCode; use axum::routing::get; use axum::Router; +use nym_credential_proxy_lib::helpers::random_uuid; +use nym_credential_proxy_lib::http_helpers::RequestError; use nym_credential_proxy_requests::api::v1::ticketbook::models::{ SharesQueryParams, TicketbookWalletSharesResponse, }; use nym_credential_proxy_requests::routes::api::v1::ticketbook::shares; use nym_http_api_common::OutputParams; -use nym_validator_client::nym_api::EpochId; -use tracing::{debug, span, Instrument, Level}; -use uuid::Uuid; - -async fn shares_to_response( - state: ApiState, - uuid: Uuid, - shares: Vec, - params: SharesQueryParams, -) -> Result { - // in all calls we ensured the shares are non-empty - #[allow(clippy::unwrap_used)] - let first = shares.first().unwrap(); - let expiration_date = first.expiration_date; - let epoch_id = first.epoch_id as EpochId; - - let threshold = state.response_ecash_threshold(uuid, epoch_id).await?; - if shares.len() < threshold as usize { - return Err(RequestError::new_server_error( - VpnApiError::InsufficientNumberOfCredentials { - available: shares.len(), - threshold, - }, - uuid, - )); - } - - // grab any requested additional data - let ( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - ) = state - .response_global_data( - params.include_master_verification_key, - params.include_expiration_date_signatures, - params.include_coin_index_signatures, - epoch_id, - expiration_date, - uuid, - ) - .await?; - - // finally produce a response - Ok(params - .output - .unwrap_or_default() - .to_response(TicketbookWalletSharesResponse { - epoch_id, - shares: shares.into_iter().map(Into::into).collect(), - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - })) -} /// Query by id for blinded shares of a bandwidth voucher #[utoipa::path( @@ -98,53 +40,15 @@ pub(crate) async fn query_for_shares_by_id( Path(share_id): Path, ) -> Result { let uuid = random_uuid(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); - async move { - debug!(""); + let response = state + .inner_state() + .query_for_shares_by_id(uuid, params.global, share_id) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // TODO: edge case: this will **NOT** work if shares got created in epoch X, - // but this query happened in epoch X+1 - let shares = match state - .storage() - .load_wallet_shares_by_shares_id(share_id) - .await - { - Ok(shares) => { - if shares.is_empty() { - debug!("shares not found"); - - // check for explicit error - match state - .storage() - .load_shares_error_by_shares_id(share_id) - .await - { - Ok(maybe_error_message) => { - if let Some(error_message) = maybe_error_message { - return Err(RequestError::new_with_uuid( - format!("failed to obtain wallet shares: {error_message} - share_id = {share_id}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); - } - } - Err(err) => return db_failure(err, uuid), - } - - return Err(RequestError::new_with_uuid( - format!("not found - share_id = {share_id}"), - uuid, - StatusCode::NOT_FOUND, - )); - } - shares - } - Err(err) => return db_failure(err, uuid), - }; - - shares_to_response(state, uuid, shares, params).await - }.instrument(span).await + Ok(output.to_response(response)) } /// Query by id for blinded wallet shares of a ticketbook @@ -173,53 +77,20 @@ pub(crate) async fn query_for_shares_by_device_id_and_credential_id( Path((device_id, credential_id)): Path<(String, String)>, ) -> Result { let uuid = random_uuid(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); - async move { - debug!(""); + let response = state + .inner_state() + .query_for_shares_by_device_id_and_credential_id( + uuid, + params.global, + device_id, + credential_id, + ) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // TODO: edge case: this will **NOT** work if shares got created in epoch X, - // but this query happened in epoch X+1 - let shares = match state - .storage() - .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) - .await - { - Ok(shares) => { - if shares.is_empty() { - debug!("shares not found"); - - // check for explicit error - match state - .storage() - .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) - .await - { - Ok(maybe_error_message) => { - if let Some(error_message) = maybe_error_message { - return Err(RequestError::new_with_uuid( - format!("failed to obtain wallet shares: {error_message} - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); - } - } - Err(err) => return db_failure(err, uuid), - } - - return Err(RequestError::new_with_uuid( - format!("not found - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::NOT_FOUND, - )); - } - shares - } - Err(err) => return db_failure(err, uuid), - }; - - shares_to_response(state, uuid, shares, params).await - }.instrument(span).await + Ok(output.to_response(response)) } pub(crate) fn routes() -> Router { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs index c7030b470d..292d27a486 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs @@ -18,7 +18,7 @@ fn swagger_redirect() -> MethodRouter { get(|| async { Redirect::to("/api/v1/swagger/") }) } -pub fn build_router(state: ApiState, auth_token: String) -> Router { +pub fn build_router(state: impl Into, auth_token: String) -> Router { // let auth_layer = from_extractor::(); let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(auth_token))); @@ -31,8 +31,8 @@ pub fn build_router(state: ApiState, auth_token: String) -> Router { .nest(routes::API, api::routes(auth_middleware)) // we don't have to be using middleware, but we already had that code // we might want something like: https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs#L44 instead - .layer(axum::middleware::from_fn(logging::logger)) - .with_state(state); + .layer(axum::middleware::from_fn(logging::log_request_info)) + .with_state(state.into()); cfg_if::cfg_if! { if #[cfg(feature = "cors")] { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index 392fa71992..a7c1b5b2b5 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -1,768 +1,21 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::deposit_maker::{DepositRequest, DepositRequestSender}; -use crate::error::VpnApiError; -use crate::helpers::LockTimer; -use crate::http::types::RequestError; -use crate::nym_api_helpers::{ - ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, - CachedImmutableItems, -}; -use crate::storage::VpnApiStorage; -use crate::webhook::ZkNymWebHookConfig; -use axum::http::StatusCode; -use bip39::Mnemonic; -use nym_compact_ecash::scheme::coin_indices_signatures::{ - aggregate_annotated_indices_signatures, CoinIndexSignatureShare, -}; -use nym_compact_ecash::scheme::expiration_date_signatures::{ - aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, -}; -use nym_compact_ecash::Base58; -use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, - MasterVerificationKeyResponse, -}; -use nym_credentials::ecash::utils::{ecash_today, EcashTime}; -use nym_credentials::{ - AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, -}; -use nym_credentials_interface::VerificationKeyAuth; -use nym_ecash_contract_common::msg::ExecuteMsg; -use nym_validator_client::coconut::EcashApiError; -use nym_validator_client::nym_api::EpochId; -use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; -use nym_validator_client::nyxd::contract_traits::{ - DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient, -}; -use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; -use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient}; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient}; -use std::future::Future; -use std::ops::Deref; -use std::sync::Arc; -use std::time::Duration; -use time::{Date, OffsetDateTime}; -use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use tokio::task::JoinHandle; -use tokio::time::Instant; -use tokio_util::sync::CancellationToken; -use tokio_util::task::TaskTracker; -use tracing::{debug, info, warn}; -use uuid::Uuid; +use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager; -// currently we need to hold our keypair so that we could request a freepass credential #[derive(Clone)] pub struct ApiState { - inner: Arc, + inner: TicketbookManager, +} + +impl From for ApiState { + fn from(inner: TicketbookManager) -> Self { + Self { inner } + } } -// a lot of functionalities, mostly to do with caching and storage is just copy-pasted from nym-api, -// since we have to do more or less the same work impl ApiState { - pub async fn new( - storage: VpnApiStorage, - zk_nym_web_hook_config: ZkNymWebHookConfig, - client: ChainClient, - deposit_requester: DepositRequestSender, - cancellation_token: CancellationToken, - ) -> Result { - let state = ApiState { - inner: Arc::new(ApiStateInner { - storage, - client, - ecash_state: EcashState::default(), - zk_nym_web_hook_config, - task_tracker: TaskTracker::new(), - deposit_requester, - cancellation_token, - }), - }; - - // since this is startup, - // might as well do all the needed network queries to establish needed global signatures - // if we don't already have them - state.build_initial_cache().await?; - - Ok(state) - } - - async fn build_initial_cache(&self) -> Result<(), VpnApiError> { - let today = ecash_today().date(); - - let epoch_id = self.current_epoch_id().await?; - let _ = self.deposit_amount().await?; - let _ = self.master_verification_key(Some(epoch_id)).await?; - let _ = self.ecash_threshold(epoch_id).await?; - let _ = self.ecash_clients(epoch_id).await?; - let _ = self.master_coin_index_signatures(Some(epoch_id)).await?; - let _ = self.master_expiration_date_signatures(today).await?; - - Ok(()) - } - - pub(crate) fn try_spawn(&self, task: F) -> Option> - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - // don't spawn new task if we've received cancellation token - if self.inner.cancellation_token.is_cancelled() { - None - } else { - self.inner.task_tracker.reopen(); - // TODO: later use a task queue since most requests will be blocked waiting on chain permit anyway - let join_handle = self.inner.task_tracker.spawn(task); - self.inner.task_tracker.close(); - Some(join_handle) - } - } - - pub(crate) async fn cancel_and_wait(&self) { - self.inner.cancellation_token.cancel(); - self.inner.task_tracker.wait().await - } - - pub(crate) fn zk_nym_web_hook(&self) -> &ZkNymWebHookConfig { - &self.inner.zk_nym_web_hook_config - } - - async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> { - let epoch = self.current_epoch().await?; - - if epoch.state.is_final() { - Ok(()) - } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { - // SAFETY: the timestamp values in our DKG contract should be valid timestamps, - // otherwise it means the chain is seriously misbehaving - #[allow(clippy::unwrap_used)] - let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); - - Err(VpnApiError::CredentialsNotYetIssuable { - availability: finish_dt, - }) - } else if epoch.state.is_waiting_initialisation() { - return Err(VpnApiError::UninitialisedDkg); - } else { - Err(VpnApiError::UnknownEcashFailure) - } - } - - pub(crate) fn storage(&self) -> &VpnApiStorage { - &self.inner.storage - } - - pub async fn deposit_amount(&self) -> Result { - let read_guard = self.inner.ecash_state.required_deposit_cache.read().await; - if read_guard.is_valid() { - return Ok(read_guard.required_amount.clone()); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.required_deposit_cache.write().await; - let deposit_amount = self - .query_chain() - .await - .get_required_deposit_amount() - .await?; - - write_guard.update(deposit_amount.clone().into()); - - Ok(deposit_amount.into()) - } - - async fn current_epoch(&self) -> Result { - let read_guard = self.inner.ecash_state.cached_epoch.read().await; - if read_guard.is_valid() { - return Ok(read_guard.current_epoch); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; - let epoch = self.query_chain().await.get_current_epoch().await?; - - write_guard.update(epoch); - Ok(epoch) - } - - pub async fn current_epoch_id(&self) -> Result { - let read_guard = self.inner.ecash_state.cached_epoch.read().await; - if read_guard.is_valid() { - return Ok(read_guard.current_epoch.epoch_id); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; - let epoch = self.query_chain().await.get_current_epoch().await?; - - write_guard.update(epoch); - Ok(epoch.epoch_id) - } - - pub(crate) async fn query_chain(&self) -> RwLockReadGuard { - self.inner.client.query_chain().await - } - - pub(crate) async fn request_deposit(&self, request: DepositRequest) { - let start = Instant::now(); - self.inner.deposit_requester.request_deposit(request).await; - - let time_taken = start.elapsed(); - let formatted = humantime::format_duration(time_taken); - if time_taken > Duration::from_secs(10) { - warn!("attempting to push new deposit request onto the queue took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") - } else { - debug!("attempting to push new deposit request onto the queue took {formatted}") - } - } - - pub(crate) async fn global_data( - &self, - include_master_verification_key: bool, - include_expiration_date_signatures: bool, - include_coin_index_signatures: bool, - epoch_id: EpochId, - expiration_date: Date, - ) -> Result< - ( - Option, - Option, - Option, - ), - VpnApiError, - > { - let master_verification_key = if include_master_verification_key { - debug!("including master verification key in the response"); - Some( - self.master_verification_key(Some(epoch_id)) - .await - .map(|key| MasterVerificationKeyResponse { - epoch_id, - bs58_encoded_key: key.to_bs58(), - }) - .inspect_err(|err| warn!("request failure: {err}"))?, - ) - } else { - None - }; - - let aggregated_expiration_date_signatures = if include_expiration_date_signatures { - debug!("including expiration date signatures in the response"); - Some( - self.master_expiration_date_signatures(expiration_date) - .await - .map(|signatures| AggregatedExpirationDateSignaturesResponse { - signatures: signatures.clone(), - }) - .inspect_err(|err| warn!("request failure: {err}"))?, - ) - } else { - None - }; - - let aggregated_coin_index_signatures = if include_coin_index_signatures { - debug!("including coin index signatures in the response"); - Some( - self.master_coin_index_signatures(Some(epoch_id)) - .await - .map(|signatures| AggregatedCoinIndicesSignaturesResponse { - signatures: signatures.clone(), - }) - .inspect_err(|err| warn!("request failure: {err}"))?, - ) - } else { - None - }; - - Ok(( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - )) - } - - pub(crate) async fn response_global_data( - &self, - include_master_verification_key: bool, - include_expiration_date_signatures: bool, - include_coin_index_signatures: bool, - epoch_id: EpochId, - expiration_date: Date, - uuid: Uuid, - ) -> Result< - ( - Option, - Option, - Option, - ), - RequestError, - > { - self.global_data( - include_master_verification_key, - include_expiration_date_signatures, - include_coin_index_signatures, - epoch_id, - expiration_date, - ) - .await - .map_err(|err| RequestError::new_server_error(err, uuid)) - } - - pub async fn ensure_not_in_epoch_transition( - &self, - uuid: Option, - ) -> Result<(), RequestError> { - if let Err(err) = self.ensure_credentials_issuable().await { - return if let Some(uuid) = uuid { - Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::SERVICE_UNAVAILABLE, - )) - } else { - Err(RequestError::new( - err.to_string(), - StatusCode::SERVICE_UNAVAILABLE, - )) - }; - } - Ok(()) - } - - pub(crate) async fn ecash_clients( - &self, - epoch_id: EpochId, - ) -> Result>, VpnApiError> { - self.inner - .ecash_state - .epoch_clients - .get_or_init(epoch_id, || async { - Ok(self - .query_chain() - .await - .get_all_verification_key_shares(epoch_id) - .await? - .into_iter() - .map(TryInto::try_into) - .collect::, EcashApiError>>()?) - }) - .await - } - - pub(crate) async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { - self.inner - .ecash_state - .threshold_values - .get_or_init(epoch_id, || async { - if let Some(threshold) = self - .query_chain() - .await - .get_epoch_threshold(epoch_id) - .await? - { - Ok(threshold) - } else { - Err(VpnApiError::UnavailableThreshold { epoch_id }) - } - }) - .await - .map(|t| *t) - } - - pub(crate) async fn response_ecash_threshold( - &self, - uuid: Uuid, - epoch_id: EpochId, - ) -> Result { - self.ecash_threshold(epoch_id) - .await - .map_err(|err| RequestError::new_server_error(err, uuid)) - } - - pub(crate) async fn master_verification_key( - &self, - epoch_id: Option, - ) -> Result, VpnApiError> { - let epoch_id = match epoch_id { - Some(id) => id, - None => self.current_epoch_id().await?, - }; - - self.inner - .ecash_state - .master_verification_key - .get_or_init(epoch_id, || async { - // 1. check the storage - if let Some(stored) = self - .inner - .storage - .get_master_verification_key(epoch_id) - .await? - { - return Ok(stored.key); - } - - info!("attempting to establish master verification key for epoch {epoch_id}..."); - - // 2. perform actual aggregation - let all_apis = self.ecash_clients(epoch_id).await?; - let threshold = self.ecash_threshold(epoch_id).await?; - - if all_apis.len() < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfSigners { - threshold, - available: all_apis.len(), - }); - } - - let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?; - - let epoch = EpochVerificationKey { - epoch_id, - key: master_key, - }; - - // 3. save the key in the storage for when we reboot - self.inner - .storage - .insert_master_verification_key(&epoch) - .await?; - - Ok(epoch.key) - }) - .await - } - - pub(crate) async fn master_coin_index_signatures( - &self, - epoch_id: Option, - ) -> Result, VpnApiError> { - let epoch_id = match epoch_id { - Some(id) => id, - None => self.current_epoch_id().await?, - }; - - self.inner - .ecash_state - .coin_index_signatures - .get_or_init(epoch_id, || async { - // 1. check the storage - if let Some(master_sigs) = self - .inner - .storage - .get_master_coin_index_signatures(epoch_id) - .await? - { - return Ok(master_sigs); - } - - info!( - "attempting to establish master coin index signatures for epoch {epoch_id}..." - ); - - // 2. go around APIs and attempt to aggregate the data - let master_vk = self.master_verification_key(Some(epoch_id)).await?; - let all_apis = self.ecash_clients(epoch_id).await?; - let threshold = self.ecash_threshold(epoch_id).await?; - - let get_partial_signatures = |api: EcashApiClient| async { - // move the api into the closure - let api = api; - let node_index = api.node_id; - let partial_vk = api.verification_key; - - let partial = api - .api_client - .partial_coin_indices_signatures(Some(epoch_id)) - .await? - .signatures; - Ok(CoinIndexSignatureShare { - index: node_index, - key: partial_vk, - signatures: partial, - }) - }; - - let shares = - query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) - .await?; - - let aggregated = aggregate_annotated_indices_signatures( - nym_credentials_interface::ecash_parameters(), - &master_vk, - &shares, - )?; - - let sigs = AggregatedCoinIndicesSignatures { - epoch_id, - signatures: aggregated, - }; - - // 3. save the signatures in the storage for when we reboot - self.inner - .storage - .insert_master_coin_index_signatures(&sigs) - .await?; - - Ok(sigs) - }) - .await - } - - pub(crate) async fn master_expiration_date_signatures( - &self, - expiration_date: Date, - ) -> Result, VpnApiError> { - self.inner - .ecash_state - .expiration_date_signatures - .get_or_init(expiration_date, || async { - // 1. sanity check to see if the expiration_date is not nonsense - ensure_sane_expiration_date(expiration_date)?; - - // 2. check the storage - if let Some(master_sigs) = self - .inner - .storage - .get_master_expiration_date_signatures(expiration_date) - .await? - { - return Ok(master_sigs); - } - - - info!( - "attempting to establish master expiration date signatures for {expiration_date}..." - ); - - // 3. go around APIs and attempt to aggregate the data - let epoch_id = self.current_epoch_id().await?; - let master_vk = self.master_verification_key(Some(epoch_id)).await?; - let all_apis = self.ecash_clients(epoch_id).await?; - let threshold = self.ecash_threshold(epoch_id).await?; - - let get_partial_signatures = |api: EcashApiClient| async { - // move the api into the closure - let api = api; - let node_index = api.node_id; - let partial_vk = api.verification_key; - - let partial = api - .api_client - .partial_expiration_date_signatures(Some(expiration_date)) - .await? - .signatures; - Ok(ExpirationDateSignatureShare { - index: node_index, - key: partial_vk, - signatures: partial, - }) - }; - - let shares = - query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) - .await?; - - let aggregated = aggregate_annotated_expiration_signatures( - &master_vk, - expiration_date.ecash_unix_timestamp(), - &shares, - )?; - - let sigs = AggregatedExpirationDateSignatures { - epoch_id, - expiration_date, - signatures: aggregated, - }; - - // 4. save the signatures in the storage for when we reboot - self.inner - .storage - .insert_master_expiration_date_signatures(&sigs) - .await?; - - Ok(sigs) - }) - .await - } -} - -#[derive(Clone)] -pub struct ChainClient(Arc>); - -impl ChainClient { - pub fn new(mnemonic: Mnemonic) -> Result { - let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - - let nyxd_url = network_details - .endpoints - .first() - .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? - .nyxd_url - .as_str(); - - let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; - - if client.ecash_contract_address().is_none() { - return Err(VpnApiError::UnavailableEcashContract); - } - - if client.dkg_contract_address().is_none() { - return Err(VpnApiError::UnavailableDKGContract); - } - - Ok(ChainClient(Arc::new(RwLock::new(client)))) - } - - pub(crate) async fn query_chain(&self) -> ChainReadPermit { - let _acquire_timer = LockTimer::new("acquire chain query permit"); - self.0.read().await - } - - pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { - let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); - - ChainWritePermit { - lock_timer: LockTimer::new("exclusive chain access permit"), - inner: self.0.write().await, - } - } -} - -// - -struct ApiStateInner { - storage: VpnApiStorage, - - client: ChainClient, - - deposit_requester: DepositRequestSender, - - zk_nym_web_hook_config: ZkNymWebHookConfig, - - ecash_state: EcashState, - - task_tracker: TaskTracker, - - cancellation_token: CancellationToken, -} - -pub(crate) struct CachedDeposit { - valid_until: OffsetDateTime, - required_amount: Coin, -} - -impl CachedDeposit { - const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; - - fn is_valid(&self) -> bool { - self.valid_until > OffsetDateTime::now_utc() - } - - fn update(&mut self, required_amount: Coin) { - self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; - self.required_amount = required_amount; - } -} - -impl Default for CachedDeposit { - fn default() -> Self { - CachedDeposit { - valid_until: OffsetDateTime::UNIX_EPOCH, - required_amount: Coin { - amount: u128::MAX, - denom: "unym".to_string(), - }, - } - } -} - -#[derive(Default)] -pub(crate) struct EcashState { - pub(crate) required_deposit_cache: RwLock, - - pub(crate) cached_epoch: RwLock, - - pub(crate) master_verification_key: CachedImmutableEpochItem, - - pub(crate) threshold_values: CachedImmutableEpochItem, - - pub(crate) epoch_clients: CachedImmutableEpochItem>, - - pub(crate) coin_index_signatures: CachedImmutableEpochItem, - - pub(crate) expiration_date_signatures: - CachedImmutableItems, -} - -pub(crate) type ChainReadPermit<'a> = RwLockReadGuard<'a, DirectSigningHttpRpcNyxdClient>; - -// explicitly wrap the WriteGuard for extra information regarding time taken -pub(crate) struct ChainWritePermit<'a> { - // it's not really dead, we only care about it being dropped - #[allow(dead_code)] - lock_timer: LockTimer, - inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, -} - -impl ChainWritePermit<'_> { - pub(crate) async fn make_deposits( - self, - short_sha: &'static str, - info: Vec<(String, Coin)>, - ) -> Result { - let address = self.inner.address(); - let starting_sequence = self.inner.get_sequence(&address).await?.sequence; - - let deposits = info.len(); - - let ecash_contract = self - .inner - .ecash_contract_address() - .ok_or(VpnApiError::UnavailableEcashContract)?; - let deposit_messages = info - .into_iter() - .map(|(identity_key, amount)| { - ( - ExecuteMsg::DepositTicketBookFunds { identity_key }, - vec![amount], - ) - }) - .collect::>(); - - let res = self - .inner - .execute_multiple( - ecash_contract, - deposit_messages, - None, - format!("cp-{short_sha}: performing {deposits} deposits"), - ) - .await?; - - loop { - let updated_sequence = self.inner.get_sequence(&address).await?.sequence; - - if updated_sequence > starting_sequence { - break; - } - warn!("wrong sequence number... waiting before releasing chain lock"); - tokio::time::sleep(Duration::from_millis(50)).await; - } - - Ok(res) - } -} - -impl Deref for ChainWritePermit<'_> { - type Target = DirectSigningHttpRpcNyxdClient; - - fn deref(&self) -> &Self::Target { - self.inner.deref() + pub(crate) fn inner_state(&self) -> &TicketbookManager { + &self.inner } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index 1ca9ccf316..8b85fef400 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -1,11 +1,6 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] - cfg_if::cfg_if! { if #[cfg(unix)] { use crate::cli::Cli; @@ -17,15 +12,8 @@ cfg_if::cfg_if! { pub mod cli; pub mod config; - pub mod credentials; - mod deposit_maker; - pub mod error; pub mod helpers; pub mod http; - pub mod nym_api_helpers; - pub mod storage; - pub mod tasks; - mod webhook; } } @@ -38,7 +26,6 @@ async fn main() -> anyhow::Result<()> { // ); let cli = Cli::parse(); - cli.webhook.ensure_valid_client_url()?; trace!("args: {cli:#?}"); setup_env(cli.config_env_file.as_ref()); @@ -55,5 +42,6 @@ async fn main() -> anyhow::Result<()> { #[tokio::main] async fn main() -> anyhow::Result<()> { eprintln!("This tool is only supported on Unix systems"); + #[allow(clippy::exit)] std::process::exit(1) } diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json index 17032501e8..2eb37c3aa2 100644 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json @@ -32,7 +32,7 @@ "hello-wasm-pack": "^0.1.0", "webpack": "^5.70.0", "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4" + "webpack-dev-server": "^5.2.1" }, "dependencies": { "@nymproject/nym-vpn-api-lib-wasm": "file:../../../dist/wasm/nym-vpn-api-lib-wasm" diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock index 43e804cb6e..455d7dda07 100644 --- a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock @@ -47,6 +47,26 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jsonjoy.com/base64@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/json-pack@^1.0.3": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz#e658900e81d194903171c42546e1aa27f446846a" + integrity sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA== + dependencies: + "@jsonjoy.com/base64" "^1.1.1" + "@jsonjoy.com/util" "^1.1.2" + hyperdyperid "^1.2.0" + thingies "^1.20.0" + +"@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.6.0.tgz#23991b2fe12cb3a006573d9dc97c768d3ed2c9f1" + integrity sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" @@ -84,14 +104,14 @@ "@types/connect" "*" "@types/node" "*" -"@types/bonjour@^3.5.9": +"@types/bonjour@^3.5.13": version "3.5.13" resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" -"@types/connect-history-api-fallback@^1.3.5": +"@types/connect-history-api-fallback@^1.5.4": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== @@ -111,20 +131,20 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.19.5" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" - integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/send" "*" -"@types/express@*", "@types/express@^4.17.13": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== +"@types/express@*", "@types/express@^4.17.21": + version "4.17.22" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.22.tgz#14cfcf120f7eb56ebb8ca77b7fa9a14d21de7c96" + integrity sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" @@ -177,10 +197,10 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== "@types/send@*": version "0.17.4" @@ -190,14 +210,14 @@ "@types/mime" "^1" "@types/node" "*" -"@types/serve-index@^1.9.1": +"@types/serve-index@^1.9.4": version "1.9.4" resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== dependencies: "@types/express" "*" -"@types/serve-static@*", "@types/serve-static@^1.13.10": +"@types/serve-static@*", "@types/serve-static@^1.15.5": version "1.15.7" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== @@ -206,17 +226,17 @@ "@types/node" "*" "@types/send" "*" -"@types/sockjs@^0.3.33": +"@types/sockjs@^0.3.36": version "0.3.36" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== dependencies: "@types/node" "*" -"@types/ws@^8.5.5": - version "8.5.12" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" - integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== +"@types/ws@^8.5.10": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" @@ -443,11 +463,6 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -458,10 +473,10 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -471,27 +486,19 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" -bonjour-service@^1.0.11: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" - integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== +bonjour-service@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== dependencies: fast-deep-equal "^3.1.3" multicast-dns "^7.2.5" -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -514,6 +521,13 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -524,23 +538,28 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" caniuse-lite@^1.0.30001646: version "1.0.30001653" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== -chokidar@^3.5.3: +chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -604,11 +623,6 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - connect-history-api-fallback@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" @@ -631,10 +645,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== copy-webpack-plugin@^11.0.0: version "11.0.0" @@ -676,26 +690,23 @@ debug@^4.1.0: dependencies: ms "2.1.2" -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" + bundle-name "^4.1.0" + default-browser-id "^5.0.0" -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== depd@2.0.0: version "2.0.0" @@ -731,6 +742,15 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -746,6 +766,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.17.1: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -759,12 +784,10 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" @@ -776,6 +799,13 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -826,52 +856,37 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -express@^4.17.3: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== +express@^4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.6.0" + cookie "0.7.1" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -930,13 +945,13 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -971,16 +986,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-monkey@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" - integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -991,21 +996,29 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" @@ -1026,18 +1039,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - globby@^13.1.1: version "13.2.2" resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" @@ -1049,12 +1050,10 @@ globby@^13.1.1: merge2 "^1.4.1" slash "^4.0.0" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" @@ -1071,24 +1070,12 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -hasown@^2.0.0, hasown@^2.0.2: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -1110,11 +1097,6 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -html-entities@^2.3.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" - integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== - http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -1146,10 +1128,10 @@ http-parser-js@>=0.5.1: resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== -http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== +http-proxy-middleware@^2.0.7: + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -1166,10 +1148,10 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== iconv-lite@0.4.24: version "0.4.24" @@ -1191,24 +1173,16 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== +inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -1219,7 +1193,7 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -ipaddr.js@^2.0.1: +ipaddr.js@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== @@ -1238,10 +1212,10 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.2" -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== is-extglob@^2.1.1: version "2.1.1" @@ -1255,6 +1229,18 @@ is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" + integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -1272,17 +1258,12 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== dependencies: - is-docker "^2.0.0" + is-inside-container "^1.0.0" isarray@~1.0.0: version "1.0.0" @@ -1328,10 +1309,10 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -launch-editor@^2.6.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.1.tgz#3bda72af213ec9b46b170e39661916ec66c2f463" - integrity sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA== +launch-editor@^2.6.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.10.0.tgz#5ca3edfcb9667df1e8721310f3a40f1127d4bc42" + integrity sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA== dependencies: picocolors "^1.0.0" shell-quote "^1.8.1" @@ -1348,22 +1329,30 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memfs@^3.4.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" - integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== +memfs@^4.6.0: + version "4.17.2" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.17.2.tgz#1f71a6d85c8c53b4f1b388234ed981a690c7e227" + integrity sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg== dependencies: - fs-monkey "^1.0.4" + "@jsonjoy.com/json-pack" "^1.0.3" + "@jsonjoy.com/util" "^1.3.0" + tree-dump "^1.0.1" + tslib "^2.0.0" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -1410,23 +1399,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -1475,24 +1452,17 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -1504,28 +1474,15 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== +open@^10.0.3: + version "10.1.2" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.2.tgz#d5df40984755c9a9c3c93df8156a12467e882925" + integrity sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw== dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^8.0.9: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" p-limit@^2.2.0: version "2.3.0" @@ -1541,12 +1498,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== +p-retry@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.1.tgz#81828f8dc61c6ef5a800585491572cc9892703af" + integrity sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ== dependencies: - "@types/retry" "0.12.0" + "@types/retry" "0.12.2" + is-network-error "^1.0.0" retry "^0.13.1" p-try@^2.0.0: @@ -1564,12 +1522,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -1579,10 +1532,10 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-type@^4.0.0: version "4.0.0" @@ -1624,12 +1577,12 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" queue-microtask@^1.2.2: version "1.2.3" @@ -1735,12 +1688,10 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== run-parallel@^1.1.9: version "1.2.0" @@ -1773,10 +1724,10 @@ schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== +schema-utils@^4.0.0, schema-utils@^4.2.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -1788,7 +1739,7 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== -selfsigned@^2.1.1: +selfsigned@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== @@ -1796,10 +1747,10 @@ selfsigned@^2.1.1: "@types/node-forge" "^1.3.0" node-forge "^1" -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -1835,27 +1786,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" - -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" + send "0.19.0" setprototypeof@1.1.0: version "1.1.0" @@ -1891,20 +1830,45 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -side-channel@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" -signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" slash@^4.0.0: version "4.0.0" @@ -1980,11 +1944,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - supports-color@^8.0.0: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" @@ -2023,6 +1982,11 @@ terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" +thingies@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" + integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -2040,6 +2004,16 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tree-dump@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.3.tgz#2f0e42e77354714418ed7ab44291e435ccdb0f80" + integrity sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg== + +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -2126,52 +2100,51 @@ webpack-cli@^4.9.2: rechoir "^0.7.0" webpack-merge "^5.7.3" -webpack-dev-middleware@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" - integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== +webpack-dev-middleware@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz#40e265a3d3d26795585cff8207630d3a8ff05877" + integrity sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA== dependencies: colorette "^2.0.10" - memfs "^3.4.3" + memfs "^4.6.0" mime-types "^2.1.31" + on-finished "^2.4.1" range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.7.4: - version "4.15.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" - integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== +webpack-dev-server@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz#049072d6e19cbda8cf600b9e364e6662d61218ba" + integrity sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ== dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.5" + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/express-serve-static-core" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" + bonjour-service "^1.2.1" + chokidar "^3.6.0" colorette "^2.0.10" compression "^1.7.4" connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" + express "^4.21.2" graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" + http-proxy-middleware "^2.0.7" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + schema-utils "^4.2.0" + selfsigned "^2.4.1" serve-index "^1.9.1" sockjs "^0.3.24" spdy "^4.0.2" - webpack-dev-middleware "^5.3.4" - ws "^8.13.0" + webpack-dev-middleware "^7.4.2" + ws "^8.18.0" webpack-merge@^5.7.3: version "5.10.0" @@ -2242,12 +2215,7 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@^8.13.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +ws@^8.18.0: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== diff --git a/nym-ip-packet-client/Cargo.toml b/nym-ip-packet-client/Cargo.toml new file mode 100644 index 0000000000..7a66b1358b --- /dev/null +++ b/nym-ip-packet-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nym-ip-packet-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +bytes.workspace = true +futures.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +tokio.workspace = true +tracing.workspace = true + +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-ip-packet-requests = { path = "../common/ip-packet-requests" } diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs new file mode 100644 index 0000000000..f499e2373f --- /dev/null +++ b/nym-ip-packet-client/src/connect.rs @@ -0,0 +1,199 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// To remove with the Registration Client PR +#![allow(clippy::unwrap_used)] + +use std::{sync::Arc, time::Duration}; + +use nym_ip_packet_requests::IpPair; +use nym_sdk::mixnet::{ + InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient, + TransmissionLane, +}; +use tokio::time::sleep; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error}; + +use crate::{ + current::{ + request::IpPacketRequest, + response::{ + ConnectResponse, ConnectResponseReply, ControlResponse, IpPacketResponse, + IpPacketResponseData, + }, + }, + error::{Error, Result}, + helpers::check_ipr_message_version, +}; + +pub type SharedMixnetClient = Arc>>; + +const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ConnectionState { + Disconnected, + Connecting, + Connected, + #[allow(unused)] + Disconnecting, +} + +pub struct IprClientConnect { + // During connection we need the mixnet client, but once connected we expect to setup a channel + // from the main mixnet listener at the top-level. + // As such, we drop the shared mixnet client once we're connected. + mixnet_client: SharedMixnetClient, + mixnet_sender: MixnetClientSender, + connected: ConnectionState, + cancel_token: CancellationToken, +} + +impl IprClientConnect { + pub async fn new(mixnet_client: SharedMixnetClient, cancel_token: CancellationToken) -> Self { + let mixnet_sender = mixnet_client.lock().await.as_ref().unwrap().split_sender(); + Self { + mixnet_client, + mixnet_sender, + connected: ConnectionState::Disconnected, + cancel_token, + } + } + + pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result { + if self.connected != ConnectionState::Disconnected { + return Err(Error::AlreadyConnected); + } + + tracing::info!("Connecting to exit gateway"); + self.connected = ConnectionState::Connecting; + match self.connect_inner(ip_packet_router_address).await { + Ok(ips) => { + debug!("Successfully connected to the ip-packet-router"); + self.connected = ConnectionState::Connected; + Ok(ips) + } + Err(err) => { + error!("Failed to connect to the ip-packet-router: {:?}", err); + self.connected = ConnectionState::Disconnected; + Err(err) + } + } + } + + async fn connect_inner(&mut self, ip_packet_router_address: Recipient) -> Result { + let request_id = self.send_connect_request(ip_packet_router_address).await?; + + debug!("Waiting for reply..."); + self.listen_for_connect_response(request_id).await + } + + async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result { + let (request, request_id) = IpPacketRequest::new_connect_request(None); + + // We use 20 surbs for the connect request because typically the IPR is configured to have + // a min threshold of 10 surbs that it reserves for itself to request additional surbs. + let surbs = 20; + self.mixnet_sender + .send(create_input_message( + ip_packet_router_address, + request, + surbs, + )) + .await + .map_err(|err| Error::SdkError(Box::new(err)))?; + + Ok(request_id) + } + + async fn handle_connect_response(&self, response: ConnectResponse) -> Result { + debug!("Handling dynamic connect response"); + match response.reply { + ConnectResponseReply::Success(r) => Ok(r.ips), + ConnectResponseReply::Failure(reason) => Err(Error::ConnectRequestDenied { reason }), + } + } + + async fn handle_ip_packet_router_response(&self, response: IpPacketResponse) -> Result { + let control_response = match response.data { + IpPacketResponseData::Control(control_response) => control_response, + _ => { + error!("Received non-control response while waiting for connect response"); + return Err(Error::UnexpectedConnectResponse); + } + }; + + match *control_response { + ControlResponse::Connect(resp) => self.handle_connect_response(resp).await, + response => { + error!("Unexpected response: {response:?}"); + Err(Error::UnexpectedConnectResponse) + } + } + } + + async fn listen_for_connect_response(&self, request_id: u64) -> Result { + // Connecting is basically synchronous from the perspective of the mixnet client, so it's safe + // to just grab ahold of the mutex and keep it until we get the response. + let mut mixnet_client_handle = self.mixnet_client.lock().await; + let mixnet_client = mixnet_client_handle.as_mut().unwrap(); + + let timeout = sleep(IPR_CONNECT_TIMEOUT); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = self.cancel_token.cancelled() => { + error!("Cancelled while waiting for reply to connect request"); + return Err(Error::Cancelled); + }, + _ = &mut timeout => { + error!("Timed out waiting for reply to connect request"); + return Err(Error::TimeoutWaitingForConnectResponse); + }, + msgs = mixnet_client.wait_for_messages() => match msgs { + None => { + return Err(Error::NoMixnetMessagesReceived); + } + Some(msgs) => { + for msg in msgs { + // Confirm that the version is correct + if let Err(err) = check_ipr_message_version(&msg) { + tracing::info!("Mixnet message version mismatch: {err}"); + continue; + } + + // Then we deserialize the message + tracing::debug!("IprClient: got message while waiting for connect response"); + let Ok(response) = IpPacketResponse::from_reconstructed_message(&msg) else { + // This is ok, it's likely just one of our self-pings + tracing::debug!("Failed to deserialize mixnet message"); + continue; + }; + + if response.id() == Some(request_id) { + tracing::debug!("Got response with matching id"); + return self.handle_ip_packet_router_response(response).await; + } + } + } + } + } + } + } +} + +fn create_input_message( + recipient: Recipient, + request: IpPacketRequest, + surbs: u32, +) -> InputMessage { + InputMessage::new_anonymous( + recipient, + request.to_bytes().unwrap(), + surbs, + TransmissionLane::General, + None, + ) +} diff --git a/nym-ip-packet-client/src/error.rs b/nym-ip-packet-client/src/error.rs new file mode 100644 index 0000000000..0979602dc2 --- /dev/null +++ b/nym-ip-packet-client/src/error.rs @@ -0,0 +1,52 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::current::response::ConnectFailureReason; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("nym sdk")] + SdkError(#[source] Box), + + #[error( + "received response with version v{received}, the client is too new and can only understand v{expected}" + )] + ReceivedResponseWithOldVersion { expected: u8, received: u8 }, + + #[error( + "received response with version v{received}, the client is too old and can only understand v{expected}" + )] + ReceivedResponseWithNewVersion { expected: u8, received: u8 }, + + #[error("got reply for connect request, but it appears intended for the wrong address?")] + GotReplyIntendedForWrongAddress, + + #[error("unexpected connect response")] + UnexpectedConnectResponse, + + #[error("mixnet client stopped returning responses")] + NoMixnetMessagesReceived, + + #[error("timeout waiting for connect response from exit gateway (ipr)")] + TimeoutWaitingForConnectResponse, + + #[error("connection cancelled")] + Cancelled, + + #[error("connect request denied: {reason}")] + ConnectRequestDenied { reason: ConnectFailureReason }, + + #[error("failed to get version from message")] + NoVersionInMessage, + + #[error("already connected to the mixnet")] + AlreadyConnected, + + #[error("failed to create connect request")] + FailedToCreateConnectRequest { + source: nym_ip_packet_requests::sign::SignatureError, + }, +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-ip-packet-client/src/helpers.rs b/nym-ip-packet-client/src/helpers.rs new file mode 100644 index 0000000000..2f1cf718cb --- /dev/null +++ b/nym-ip-packet-client/src/helpers.rs @@ -0,0 +1,30 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::cmp::Ordering; + +use nym_sdk::mixnet::ReconstructedMessage; + +use crate::{current::VERSION as CURRENT_VERSION, error::Result, Error}; + +pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Result<()> { + // Assuming it's a IPR message, it will have a version as its first byte + if let Some(version) = message.message.first() { + match version.cmp(&CURRENT_VERSION) { + Ordering::Greater => Err(Error::ReceivedResponseWithNewVersion { + expected: CURRENT_VERSION, + received: *version, + }), + Ordering::Less => Err(Error::ReceivedResponseWithOldVersion { + expected: CURRENT_VERSION, + received: *version, + }), + Ordering::Equal => { + // We're good + Ok(()) + } + } + } else { + Err(Error::NoVersionInMessage) + } +} diff --git a/nym-ip-packet-client/src/lib.rs b/nym-ip-packet-client/src/lib.rs new file mode 100644 index 0000000000..8aa5ffdf42 --- /dev/null +++ b/nym-ip-packet-client/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod connect; +mod error; +mod helpers; +mod listener; + +pub use connect::IprClientConnect; +pub use error::Error; +pub use listener::{IprListener, MixnetMessageOutcome}; + +// Re-export the currently used version +pub use nym_ip_packet_requests::v8 as current; diff --git a/nym-ip-packet-client/src/listener.rs b/nym-ip-packet-client/src/listener.rs new file mode 100644 index 0000000000..7380300175 --- /dev/null +++ b/nym-ip-packet-client/src/listener.rs @@ -0,0 +1,121 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use bytes::Bytes; +use futures::StreamExt; +use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v8::response::ControlResponse}; +use nym_sdk::mixnet::ReconstructedMessage; +use tokio_util::codec::FramedRead; +use tracing::{debug, error, info, warn}; + +use crate::{ + current::{ + request::{ControlRequest, IpPacketRequest, IpPacketRequestData}, + response::{InfoLevel, IpPacketResponse, IpPacketResponseData}, + }, + helpers::check_ipr_message_version, +}; + +pub enum MixnetMessageOutcome { + IpPackets(Vec), + MixnetSelfPing, + Disconnect, +} + +pub struct IprListener {} + +#[derive(Debug, thiserror::Error)] +pub enum IprListenerError { + #[error(transparent)] + IprClientError(#[from] crate::Error), +} + +impl IprListener { + pub fn new() -> Self { + Self {} + } + + fn is_mix_ping(&self, request: &IpPacketRequest) -> bool { + match request.data { + IpPacketRequestData::Control(ref control) => { + matches!(**control, ControlRequest::Ping(_)) + } + _ => { + debug!("Received unexpected request: {request:?}"); + false + } + } + } + + pub async fn handle_reconstructed_message( + &mut self, + message: ReconstructedMessage, + ) -> Result, IprListenerError> { + check_ipr_message_version(&message)?; + + match IpPacketResponse::from_reconstructed_message(&message) { + Ok(response) => { + match response.data { + IpPacketResponseData::Data(data_response) => { + // Un-bundle the mixnet message and send the individual IP packets + // to the tun device + let framed_reader = FramedRead::new( + data_response.ip_packet.as_ref(), + MultiIpPacketCodec::new(), + ); + let responses: Vec = framed_reader + .filter_map(|res| async { res.ok().map(|packet| packet.into_bytes()) }) + .collect() + .await; + return Ok(Some(MixnetMessageOutcome::IpPackets(responses))); + } + IpPacketResponseData::Control(control_response) => match *control_response { + ControlResponse::Connect(_) => { + info!("Received connect response when already connected - ignoring"); + } + ControlResponse::Disconnect(_) => { + info!("Received disconnect response"); + return Ok(Some(MixnetMessageOutcome::Disconnect)); + } + ControlResponse::UnrequestedDisconnect(_) => { + info!("Received unrequested disconnect response, ignoring for now"); + } + ControlResponse::Pong(_) => { + info!("Received pong response, ignoring for now"); + } + ControlResponse::Health(_) => { + info!("Received health response, ignoring for now"); + } + ControlResponse::Info(info) => { + let msg = + format!("Received info response from the mixnet: {}", info.reply); + match info.level { + InfoLevel::Info => info!("{msg}"), + InfoLevel::Warn => warn!("{msg}"), + InfoLevel::Error => error!("{msg}"), + } + } + }, + } + } + Err(err) => { + // The exception to when we are not expecting a response, is when we + // are sending a ping to ourselves. + if let Ok(request) = IpPacketRequest::from_reconstructed_message(&message) { + if self.is_mix_ping(&request) { + return Ok(Some(MixnetMessageOutcome::MixnetSelfPing)); + } + } else { + warn!("Failed to deserialize reconstructed message: {err}"); + } + } + } + Ok(None) + } +} + +impl Default for IprListener { + fn default() -> Self { + Self::new() + } +} diff --git a/nym-network-monitor/Cargo.toml b/nym-network-monitor/Cargo.toml index a5862e0188..c693cb5b19 100644 --- a/nym-network-monitor/Cargo.toml +++ b/nym-network-monitor/Cargo.toml @@ -30,7 +30,7 @@ utoipa-swagger-ui = { workspace = true, features = ["axum"] } tokio-postgres = { workspace = true } # internal -nym-bin-common = { path = "../common/bin-common" } +nym-bin-common = { path = "../common/bin-common", features = ["basic_tracing"] } nym-client-core = { path = "../common/client-core" } nym-crypto = { path = "../common/crypto" } nym-network-defaults = { path = "../common/network-defaults" } diff --git a/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index df049b4ab6..caddfe1db6 100644 --- a/nym-network-monitor/src/accounting.rs +++ b/nym-network-monitor/src/accounting.rs @@ -5,15 +5,16 @@ use std::{ use anyhow::Result; use futures::{pin_mut, stream::FuturesUnordered, StreamExt}; -use log::{debug, info}; +use log::{debug, error, info}; use nym_sphinx::chunking::{monitoring, SentFragment}; use nym_topology::{NymRouteProvider, RoutingNode}; use nym_types::monitoring::{MonitorMessage, NodeResult}; -use nym_validator_client::nym_api::routes::{API_VERSION, STATUS, SUBMIT_GATEWAY, SUBMIT_NODE}; +use nym_validator_client::nym_api::routes::{STATUS, SUBMIT_GATEWAY, SUBMIT_NODE, V1_API_VERSION}; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; use serde::{Deserialize, Serialize}; -use tokio_postgres::{binary_copy::BinaryCopyInWriter, types::Type, Client}; +use tokio::task::JoinHandle; +use tokio_postgres::{binary_copy::BinaryCopyInWriter, types::Type, Client, NoTls}; use utoipa::ToSchema; use crate::{NYM_API_URL, PRIVATE_KEY, TOPOLOGY}; @@ -23,6 +24,39 @@ struct HydratedRoute { gateway_node: RoutingNode, } +#[derive(Serialize, Deserialize, Debug, ToSchema)] +struct AccountingRoute { + mix_nodes: (u32, u32, u32), + gateway_node: u32, + success: bool, +} + +impl AccountingRoute { + fn from_complete(route: &HydratedRoute) -> Self { + Self { + mix_nodes: ( + route.mix_nodes[0].node_id, + route.mix_nodes[1].node_id, + route.mix_nodes[2].node_id, + ), + gateway_node: route.gateway_node.node_id, + success: true, + } + } + + fn from_incomplete(route: &HydratedRoute) -> Self { + Self { + mix_nodes: ( + route.mix_nodes[0].node_id, + route.mix_nodes[1].node_id, + route.mix_nodes[2].node_id, + ), + gateway_node: route.gateway_node.node_id, + success: false, + } + } +} + #[derive(Serialize, Deserialize, Debug, Default, ToSchema)] struct GatewayStats(u32, u32); @@ -67,6 +101,8 @@ pub struct NetworkAccount { mix_details: HashMap, #[serde(skip)] gateway_details: HashMap, + #[serde(skip)] + accounting_routes: Vec, } impl NetworkAccount { @@ -190,14 +226,18 @@ impl NetworkAccount { .or_insert(GatewayStats::new(0, 0)); self.gateway_details.insert( route.gateway_node.identity_key.to_base58_string(), - route.gateway_node, + route.gateway_node.clone(), ); if self.complete_fragment_sets.contains(fragment_set_id) { self.complete_routes.push(mix_ids); gateway_stats_entry.incr_success(); + self.accounting_routes + .push(AccountingRoute::from_complete(&route)); } else { self.incomplete_routes.push(mix_ids); gateway_stats_entry.incr_failure(); + self.accounting_routes + .push(AccountingRoute::from_incomplete(&route)); } } } @@ -384,17 +424,70 @@ async fn submit_gateway_stats_to_db(client: Arc) -> anyhow::Result<()> { Ok(()) } -pub async fn submit_metrics_to_db(client: Arc) -> anyhow::Result<()> { - let client = Arc::clone(&client); - let client2 = Arc::clone(&client); - submit_node_stats_to_db(client).await?; - submit_gateway_stats_to_db(client2).await?; +async fn db_connection(database_url: Option<&String>) -> Result)>> { + if let Some(database_url) = database_url { + let (client, connection) = tokio_postgres::connect(database_url, NoTls).await?; + + let handle = tokio::spawn(async move { + if let Err(e) = connection.await { + error!("Postgres connection error: {e}"); + } + }); + + Ok(Some((client, handle))) + } else { + Ok(None) + } +} +pub async fn submit_metrics_to_db(database_url: Option<&String>) -> anyhow::Result<()> { + if let Some((client, handle)) = db_connection(database_url).await? { + let client = Arc::new(client); + let client2 = Arc::clone(&client); + let client3 = Arc::clone(&client); + submit_node_stats_to_db(client).await?; + submit_gateway_stats_to_db(client2).await?; + submit_accounting_routes_to_db(client3).await?; + handle.abort(); + } Ok(()) } -pub async fn submit_metrics(client: Option>) -> anyhow::Result<()> { - if let Some(client) = client { - submit_metrics_to_db(client).await?; +async fn submit_accounting_routes_to_db(client: Arc) -> anyhow::Result<()> { + let client = Arc::clone(&client); + let network_account = NetworkAccount::finalize()?; + let accounting_routes = network_account.accounting_routes; + + let sink = client + .copy_in("COPY routes (layer1, layer2, layer3, gw, success) FROM STDIN BINARY") + .await?; + + let writer = BinaryCopyInWriter::new( + sink, + &[Type::INT4, Type::INT4, Type::INT4, Type::INT4, Type::BOOL], + ); + pin_mut!(writer); + + for route in accounting_routes { + writer + .as_mut() + .write(&[ + &(route.mix_nodes.0 as i32), + &(route.mix_nodes.1 as i32), + &(route.mix_nodes.2 as i32), + &(route.gateway_node as i32), + &route.success, + ]) + .await?; + } + + writer.finish().await?; + + Ok(()) +} + +pub async fn submit_metrics(database_url: Option<&String>) -> anyhow::Result<()> { + if let Err(e) = submit_metrics_to_db(database_url).await { + error!("Error submitting metrics to db: {e}"); } if let Some(private_key) = PRIVATE_KEY.get() { @@ -404,9 +497,11 @@ pub async fn submit_metrics(client: Option>) -> anyhow::Result<()> { info!("Submitting metrics to {}", *NYM_API_URL); let client = reqwest::Client::new(); - let node_submit_url = format!("{}/{API_VERSION}/{STATUS}/{SUBMIT_NODE}", &*NYM_API_URL); - let gateway_submit_url = - format!("{}/{API_VERSION}/{STATUS}/{SUBMIT_GATEWAY}", &*NYM_API_URL); + let node_submit_url = format!("{}/{V1_API_VERSION}/{STATUS}/{SUBMIT_NODE}", &*NYM_API_URL); + let gateway_submit_url = format!( + "{}/{V1_API_VERSION}/{STATUS}/{SUBMIT_GATEWAY}", + &*NYM_API_URL + ); info!("Submitting {} mixnode measurements", node_stats.len()); diff --git a/nym-network-monitor/src/http.rs b/nym-network-monitor/src/http.rs index 7255656344..6608345242 100644 --- a/nym-network-monitor/src/http.rs +++ b/nym-network-monitor/src/http.rs @@ -84,7 +84,7 @@ impl HttpServer { axum::serve(listener, app).with_graceful_shutdown(self.cancel.cancelled_owned()); info!("##########################################################################################"); - info!("######################### HTTP server running, with {} clients ############################################", n_clients); + info!("######################### HTTP server running, with {n_clients} clients ############################################"); info!("##########################################################################################"); server_future.await?; diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index f285446a30..d0d04d62c6 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -2,7 +2,7 @@ use crate::http::HttpServer; use accounting::submit_metrics; use anyhow::Result; use clap::Parser; -use log::{error, info, warn}; +use log::{info, warn}; use nym_bin_common::bin_info; use nym_client_core::config::ForgetMe; use nym_crypto::asymmetric::ed25519::PrivateKey; @@ -10,6 +10,7 @@ use nym_network_defaults::setup_env; use nym_network_defaults::var_names::NYM_API; use nym_sdk::mixnet::{self, MixnetClient}; use nym_sphinx::chunking::monitoring; +use nym_topology::provider_trait::ToTopologyMetadata; use nym_topology::{HardcodedTopologyProvider, NymTopology}; use std::fs::File; use std::io::Write; @@ -23,11 +24,10 @@ use std::{ }; use tokio::sync::OnceCell; use tokio::{signal::ctrl_c, sync::RwLock}; -use tokio_postgres::NoTls; use tokio_util::sync::CancellationToken; static NYM_API_URL: LazyLock = LazyLock::new(|| { - std::env::var(NYM_API).unwrap_or_else(|_| panic!("{} env var not set", NYM_API)) + std::env::var(NYM_API).unwrap_or_else(|_| panic!("{NYM_API} env var not set")) }); static MIXNET_TIMEOUT: OnceCell = OnceCell::const_new(); @@ -49,10 +49,10 @@ async fn make_clients( ) { loop { let spawned_clients = clients.read().await.len(); - info!("Currently spawned clients: {}", spawned_clients); + info!("Currently spawned clients: {spawned_clients}"); // If we have enough clients, sleep for a minute and remove the oldest one if spawned_clients >= n_clients { - info!("New client will be spawned in {} seconds", lifetime); + info!("New client will be spawned in {lifetime} seconds"); tokio::time::sleep(tokio::time::Duration::from_secs(lifetime)).await; info!("Removing oldest client"); if let Some(dropped_client) = clients.write().await.pop_front() { @@ -75,7 +75,7 @@ async fn make_clients( let client = match make_client(topology.clone()).await { Ok(client) => client, Err(err) => { - warn!("{}, moving on", err); + warn!("{err}, moving on"); continue; } }; @@ -168,17 +168,19 @@ async fn nym_topology_from_env() -> anyhow::Result { let rewarded_set = client.get_current_rewarded_set().await?; // just get all nodes to make our lives easier because it's just one query for the whole duration of the monitor (?) - let nodes = client.get_all_basic_nodes().await?; + let nodes_response = client.get_all_basic_nodes_with_metadata().await?; + let nodes = nodes_response.nodes; + let metadata = nodes_response.metadata; - let mut topology = NymTopology::new_empty(rewarded_set); - topology.add_skimmed_nodes(&nodes); - - Ok(topology) + Ok( + NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) + .with_skimmed_nodes(&nodes), + ) } #[tokio::main] async fn main() -> Result<()> { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); let args = Args::parse(); @@ -229,31 +231,16 @@ async fn main() -> Result<()> { info!("Waiting for message (ctrl-c to exit)"); - let client = if let Some(database_url) = args.database_url { - let (client, connection) = tokio_postgres::connect(&database_url, NoTls).await?; - - tokio::spawn(async move { - if let Err(e) = connection.await { - error!("Postgres connection error: {}", e); - } - }); - - Some(Arc::new(client)) - } else { - None - }; - loop { - let client = client.as_ref().map(Arc::clone); match tokio::time::timeout(Duration::from_secs(600), ctrl_c()).await { Ok(_) => { info!("Received kill signal, shutting down, submitting final batch of metrics"); - submit_metrics(client).await?; + submit_metrics(args.database_url.as_ref()).await?; break; } Err(_) => { info!("Submitting metrics, cleaning metric buffers"); - submit_metrics(client).await?; + submit_metrics(args.database_url.as_ref()).await?; } }; } diff --git a/nym-node-status-api/README.md b/nym-node-status-api/README.md index 03da6ca3cb..1ef6706c1d 100644 --- a/nym-node-status-api/README.md +++ b/nym-node-status-api/README.md @@ -4,3 +4,61 @@ The Node Status API serves information about individual `nym-nodes` in the Mixne 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. You can find build and operation instructions in the [docs](https://nym.com/docs/apis/ns-api). + +## Database Support + +The Node Status API supports both SQLite and PostgreSQL databases through Cargo feature flags: + +- **SQLite** (default): Lightweight, file-based database suitable for development and small deployments +- **PostgreSQL**: Full-featured database recommended for production deployments + +### Building with Different Database Backends + +```bash +# Build with SQLite (default) +cargo build --features sqlite --no-default-features + +# Build with PostgreSQL +cargo build --features pg --no-default-features +``` + +### Running Tests + +```bash +# Test with SQLite +cargo test --features sqlite --no-default-features + +# Test with PostgreSQL +make test-db # This sets up a test PostgreSQL instance +``` + +### Development Commands + +The project includes a Makefile with helpful commands for both database backends: + +```bash +# Check code compilation +make check-sqlite # Check with SQLite +make check-pg # Check with PostgreSQL + +# Run clippy linter +make clippy-sqlite # Lint with SQLite +make clippy-pg # Lint with PostgreSQL +make clippy # Run both + +# PostgreSQL development +make dev-db # Start a PostgreSQL instance for development +make prepare-pg # Prepare SQLx offline cache for PostgreSQL +``` + +### Implementation Details + +The database abstraction is implemented using a query wrapper that automatically converts SQLite-style `?` placeholders to PostgreSQL-style `$1, $2, ...` placeholders at runtime. This allows writing queries once using SQLite syntax while maintaining compatibility with both databases. + +Key differences handled: +- Placeholder syntax (`?` vs `$1, $2, ...`) +- Type conversions (SQLite uses i64, PostgreSQL uses i32 for many fields) +- SQL dialect differences (e.g., `INSERT OR IGNORE` vs `ON CONFLICT DO NOTHING`) +- RETURNING clause behavior + +For more details on PostgreSQL setup, see [README_PG.md](nym-node-status-api/README_PG.md). diff --git a/nym-node-status-api/nym-node-status-agent/Cargo.toml b/nym-node-status-api/nym-node-status-agent/Cargo.toml index 5e01d55df7..6c6cab2ca9 100644 --- a/nym-node-status-api/nym-node-status-agent/Cargo.toml +++ b/nym-node-status-api/nym-node-status-agent/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-agent" -version = "1.0.0-rc.2" +version = "1.0.4" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -14,13 +14,20 @@ rust-version.workspace = true readme.workspace = true [dependencies] -anyhow = { workspace = true} +anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } -nym-bin-common = { path = "../../common/bin-common", features = ["models"]} -nym-node-status-client = { path = "../nym-node-status-client" } +futures = { workspace = true } +nym-bin-common = { path = "../../common/bin-common", features = ["models"] } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } + +nym-node-status-client = { path = "../nym-node-status-client" } rand = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } +tokio = { workspace = true, features = [ + "macros", + "rt-multi-thread", + "process", + "fs", +] } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/nym-node-status-api/nym-node-status-agent/Dockerfile b/nym-node-status-api/nym-node-status-agent/Dockerfile index fcab377e26..192af6678d 100644 --- a/nym-node-status-api/nym-node-status-agent/Dockerfile +++ b/nym-node-status-api/nym-node-status-agent/Dockerfile @@ -16,7 +16,7 @@ WORKDIR /usr/src/nym-vpn-client/nym-vpn-core RUN cargo build --release --package nym-gateway-probe COPY ./ /usr/src/nym -WORKDIR /usr/src/nym/nym-node-status-agent +WORKDIR /usr/src/nym/nym-node-status-api/nym-node-status-agent RUN cargo build --release #------------------------------------------------------------------- diff --git a/nym-node-status-api/nym-node-status-agent/build-push-node-status-agent.sh b/nym-node-status-api/nym-node-status-agent/build-push-node-status-agent.sh new file mode 100755 index 0000000000..2cf972a8f3 --- /dev/null +++ b/nym-node-status-api/nym-node-status-agent/build-push-node-status-agent.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Build and push Node Status Agent container to harbor.nymte.ch + +set -e + +# Configuration +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +WORKING_DIRECTORY="${SCRIPT_DIR}" +CONTAINER_NAME="node-status-agent" +REGISTRY="harbor.nymte.ch" +NAMESPACE="nym" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to display usage +usage() { + echo "Usage: $0 " + echo " gateway-probe-git-ref - Git reference (branch/tag/commit) for gateway probe" + echo "" + echo "Example: $0 main" + echo "Example: $0 release/2025.11-cheddar" + echo "Example: $0 v1.2.3" + exit 1 +} + +# Parse arguments +if [ $# -ne 1 ]; then + usage +fi + +GATEWAY_PROBE_GIT_REF="$1" + +# Get version from Cargo.toml +VERSION=$(grep "^version = " "${WORKING_DIRECTORY}/Cargo.toml" | sed -E 's/version = "(.*)"/\1/') +if [ -z "$VERSION" ]; then + echo -e "${RED}Error: Could not extract version from Cargo.toml${NC}" + exit 1 +fi + +# Clean up git ref for use in tag (replace / with -) +GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}" + +echo -e "${YELLOW}Building Node Status Agent${NC}" +echo -e "${YELLOW}Version: ${VERSION}${NC}" +echo -e "${YELLOW}Gateway Probe Git Ref: ${GATEWAY_PROBE_GIT_REF} (slug: ${GIT_REF_SLUG})${NC}" + +# Login to Harbor +echo -e "${GREEN}Logging into Harbor...${NC}" +docker login "${REGISTRY}" + +# Build the container +echo -e "${GREEN}Building container with gateway probe from ${GATEWAY_PROBE_GIT_REF}...${NC}" +# Build from repository root (two levels up from script location) +docker build \ + --build-arg GIT_REF="${GATEWAY_PROBE_GIT_REF}" \ + -f "${WORKING_DIRECTORY}/Dockerfile" \ + "${SCRIPT_DIR}/../.." \ + -t "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:${VERSION}-${GIT_REF_SLUG}" \ + -t "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:latest-${GIT_REF_SLUG}" + +# Push to Harbor +echo -e "${GREEN}Pushing container to Harbor...${NC}" +docker push "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:${VERSION}-${GIT_REF_SLUG}" +docker push "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:latest-${GIT_REF_SLUG}" + +echo -e "${GREEN}Successfully built and pushed ${CONTAINER_NAME}:${VERSION}-${GIT_REF_SLUG}${NC}" \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-agent/run.sh b/nym-node-status-api/nym-node-status-agent/run.sh index 544c6ca731..1061ad5f95 100755 --- a/nym-node-status-api/nym-node-status-agent/run.sh +++ b/nym-node-status-api/nym-node-status-agent/run.sh @@ -16,11 +16,17 @@ set -a source "${monorepo_root}/envs/${ENVIRONMENT}.env" set +a +if [ -z "$NYM_NODE_MNEMONICS" ]; then + echo "NYM_NODE_MNEMONICS is required to run an agent" + exit 1 +fi + export RUST_LOG="info" -export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" -export NODE_STATUS_AGENT_SERVER_PORT="8000" -export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" +NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +NODE_STATUS_AGENT_SERVER_PORT="8000" +SERVER="${NODE_STATUS_AGENT_SERVER_ADDRESS}|${NODE_STATUS_AGENT_SERVER_PORT}" export NODE_STATUS_AGENT_AUTH_KEY="BjyC9SsHAZUzPRkQR4sPTvVrp4GgaquTh5YfSJksvvWT" +export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1" workers=${1:-1} @@ -47,7 +53,7 @@ function swarm() { local workers=$1 for ((i = 1; i <= workers; i++)); do - ${monorepo_root}/target/release/nym-node-status-agent run-probe & + ${monorepo_root}/target/release/nym-node-status-agent run-probe --server ${SERVER} & done wait diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs b/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs index 75601548ed..bc5c055c76 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/mod.rs @@ -1,17 +1,45 @@ use crate::probe::GwProbe; use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; -use std::sync::OnceLock; +use nym_crypto::asymmetric::ed25519::PrivateKey; +use std::{env, sync::OnceLock}; pub(crate) mod generate_keypair; pub(crate) mod run_probe; +#[derive(Debug)] +pub(crate) struct ServerConfig { + pub(crate) address: String, + pub(crate) port: u16, + pub(crate) auth_key: PrivateKey, +} + // Helper for passing LONG_VERSION to clap fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) } +fn parse_server_config(s: &str) -> Result { + let parts: Vec<&str> = s.split('|').collect(); + if parts.len() != 2 { + return Err("Server config must be in format 'address|port'".to_string()); + } + + let address = parts[0].to_string(); + let port = parts[1] + .parse::() + .map_err(|_| "Invalid port number".to_string())?; + let auth_key = + PrivateKey::from_base58_string(env::var("NODE_STATUS_AGENT_AUTH_KEY").unwrap()).unwrap(); + + Ok(ServerConfig { + address, + port, + auth_key, + }) +} + #[derive(Parser, Debug)] #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Args { @@ -22,20 +50,19 @@ pub(crate) struct Args { #[derive(Subcommand, Debug)] pub(crate) enum Command { RunProbe { - #[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")] - server_address: String, - - #[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")] - server_port: u16, - - /// base58-encoded private key - #[arg(long, env = "NODE_STATUS_AGENT_AUTH_KEY")] - ns_api_auth_key: String, + /// Server configurations in format "address|port" + /// Can be specified multiple times for multiple servers + #[arg(short, long, required = true)] + server: Vec, /// path of binary to run #[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")] probe_path: String, + /// mnemonic for acquiring zk-nyms + #[arg(long, env = "NYM_NODE_MNEMONICS")] + mnemonic: String, + #[arg( long, env = "NODE_STATUS_AGENT_PROBE_EXTRA_ARGS", @@ -54,22 +81,29 @@ impl Args { pub(crate) async fn execute(&self) -> anyhow::Result<()> { match &self.command { Command::RunProbe { - server_address, - server_port, - ns_api_auth_key, + server, probe_path, + mnemonic, probe_extra_args, - } => run_probe::run_probe( - server_address, - server_port.to_owned(), - ns_api_auth_key, - probe_path, - probe_extra_args, - ) - .await - .inspect_err(|err| { - tracing::error!("{err}"); - })?, + } => { + // Parse server configs + let mut servers = Vec::new(); + for s in server { + match parse_server_config(s) { + Ok(config) => servers.push(config), + Err(e) => { + tracing::error!("Invalid server config '{}': {}", s, e); + anyhow::bail!("Invalid server config '{}': {}", s, e); + } + } + } + + run_probe::run_probe(&servers, probe_path, mnemonic, probe_extra_args) + .await + .inspect_err(|err| { + tracing::error!("{err}"); + })? + } Command::GenerateKeypair { path } => { let path = path .to_owned() diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs b/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs index 8cf779cfbc..07fa3b514f 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/run_probe.rs @@ -1,33 +1,143 @@ -use crate::cli::GwProbe; -use anyhow::Context; -use nym_crypto::asymmetric::ed25519::PrivateKey; +use crate::cli::{GwProbe, ServerConfig}; pub(crate) async fn run_probe( - server_ip: &str, - server_port: u16, - ns_api_auth_key: &str, + servers: &[ServerConfig], probe_path: &str, + mnemonic: &str, probe_extra_args: &Vec, ) -> anyhow::Result<()> { - let auth_key = PrivateKey::from_base58_string(ns_api_auth_key) - .context("Couldn't parse auth key, exiting")?; - - let ns_api_client = nym_node_status_client::NsApiClient::new(server_ip, server_port, auth_key); + if servers.is_empty() { + anyhow::bail!("No servers configured"); + } let probe = GwProbe::new(probe_path.to_string()); let version = probe.version().await; tracing::info!("Probe version:\n{}", version); - if let Some(testrun) = ns_api_client.request_testrun().await? { - let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key), probe_extra_args); + // Always use first server as primary + let primary_server = &servers[0]; + tracing::info!( + "Requesting testrun from primary server: {}:{}", + primary_server.address, + primary_server.port + ); - ns_api_client - .submit_results(testrun.testrun_id, log, testrun.assigned_at_utc) - .await?; - } else { - tracing::info!("No testruns available, exiting") + let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes( + &primary_server.auth_key.to_bytes(), + ) + .expect("Failed to clone auth key"); + let ns_api_client = nym_node_status_client::NsApiClient::new( + &primary_server.address, + primary_server.port, + auth_key, + ); + + match ns_api_client.request_testrun().await { + Ok(Some(testrun)) => { + tracing::info!( + "Received testrun {} for gateway {} from primary", + testrun.testrun_id, + testrun.gateway_identity_key + ); + + // Run the probe + let log = probe.run_and_get_log( + &Some(testrun.gateway_identity_key.clone()), + mnemonic, + probe_extra_args, + ); + + // Submit to ALL servers in parallel + let handles = servers + .iter() + .enumerate() + .map(|(idx, server)| { + let testrun = testrun.clone(); + let log = log.clone(); + + async move { + let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes( + &server.auth_key.to_bytes(), + ) + .expect("Failed to clone auth key"); + let client = nym_node_status_client::NsApiClient::new( + &server.address, + server.port, + auth_key, + ); + + let result = if idx == 0 { + // Primary server: submit regular results without context + client + .submit_results( + testrun.testrun_id as i64, + log, + testrun.assigned_at_utc, + ) + .await + } else { + // Other servers: submit results with context + client + .submit_results_with_context( + testrun.testrun_id, + log, + testrun.assigned_at_utc, + testrun.gateway_identity_key, + ) + .await + }; + + (idx, server.address.clone(), server.port, result) + } + }) + .collect::>(); + + let results = futures::future::join_all(handles).await; + + for result in results { + match result.3 { + Ok(()) => { + let method = if result.0 == 0 { + "regular" + } else { + "with context" + }; + tracing::info!( + "✅ Successfully submitted {} to server[{}] {}:{}", + method, + result.0, + result.1, + result.2 + ); + } + Err(e) => { + let method = if result.0 == 0 { + "regular" + } else { + "with context" + }; + tracing::warn!( + "❌ Failed to submit {} to server[{}] {}:{} - {}", + method, + result.0, + result.1, + result.2, + e + ); + } + } + } + + Ok(()) + } + Ok(None) => { + tracing::info!("No testruns available from primary server"); + Ok(()) + } + Err(e) => { + tracing::error!("Failed to contact primary server: {}", e); + Err(e) + } } - - Ok(()) } diff --git a/nym-node-status-api/nym-node-status-agent/src/main.rs b/nym-node-status-api/nym-node-status-agent/src/main.rs index d3078753fa..0415864d9f 100644 --- a/nym-node-status-api/nym-node-status-agent/src/main.rs +++ b/nym-node-status-api/nym-node-status-agent/src/main.rs @@ -51,7 +51,7 @@ pub(crate) fn setup_tracing() { "axum", ]; for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))); } filter = filter.add_directive(directive_checked("nym_bin_common=debug")); diff --git a/nym-node-status-api/nym-node-status-agent/src/probe.rs b/nym-node-status-api/nym-node-status-agent/src/probe.rs index 67e7e508c6..b9fcd5c040 100644 --- a/nym-node-status-api/nym-node-status-agent/src/probe.rs +++ b/nym-node-status-api/nym-node-status-agent/src/probe.rs @@ -30,7 +30,7 @@ impl GwProbe { } Err(e) => { error!("Failed to stat binary at {}: {}", &self.path, e); - return format!("Failed to access binary: {}", e); + return format!("Failed to access binary: {e}"); } } } @@ -55,17 +55,17 @@ impl GwProbe { output.status.code().unwrap_or(-1), stderr ); - format!("Command failed: {}", stderr) + format!("Command failed: {stderr}") } } Err(e) => { error!("Failed to get command output: {}", e); - format!("Failed to get command output: {}", e) + format!("Failed to get command output: {e}") } }, Err(e) => { error!("Failed to spawn process: {}", e); - format!("Failed to spawn process: {}", e) + format!("Failed to spawn process: {e}") } } } @@ -73,6 +73,7 @@ impl GwProbe { pub(crate) fn run_and_get_log( &self, gateway_key: &Option, + mnemonic: &str, probe_extra_args: &Vec, ) -> String { let mut command = std::process::Command::new(&self.path); @@ -81,6 +82,7 @@ impl GwProbe { if let Some(gateway_id) = gateway_key { command.arg("--gateway").arg(gateway_id); } + command.arg("--mnemonic").arg(mnemonic); tracing::info!("Extra args for the probe:"); for arg in probe_extra_args { diff --git a/nym-node-status-api/nym-node-status-api/.env.example b/nym-node-status-api/nym-node-status-api/.env.example new file mode 100644 index 0000000000..7625347a7c --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.env.example @@ -0,0 +1,32 @@ +# Example environment variables for nym-node-status-api + +# Database configuration +# For SQLite: +# DATABASE_URL=sqlite://nym-node-status-api.sqlite + +# For PostgreSQL: +# DATABASE_URL=postgres://testuser:testpass@localhost:5433/nym_node_status_api_test + +# Network configuration +NETWORK_NAME=sandbox +NYM_API=https://sandbox-nym-api1.nymtech.net/api +NYXD=https://rpc.sandbox.nymtech.net + +# API configuration +NYM_NODE_STATUS_API_HTTP_PORT=8000 +NYM_API_CLIENT_TIMEOUT=15 +SQLX_BUSY_TIMEOUT_S=5 + +# Monitoring intervals +NODE_STATUS_API_MONITOR_REFRESH_INTERVAL=300 +NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=300 +NODE_STATUS_API_GEODATA_TTL=86400 + +# Agent keys (comma-separated list) +NODE_STATUS_API_AGENT_KEY_LIST= + +# External service tokens +IPINFO_API_TOKEN=your_token_here + +# MixNodes (Optional) +DATA_PROVIDER_DELEGATION_LIST= \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-01ee4a30bc3104712e5bc371a45d614a89d88adf02358800433e06100c13c548.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-01ee4a30bc3104712e5bc371a45d614a89d88adf02358800433e06100c13c548.json deleted file mode 100644 index d59ea9da1a..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-01ee4a30bc3104712e5bc371a45d614a89d88adf02358800433e06100c13c548.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO mixnode_daily_stats (\n mix_id, date_utc,\n total_stake, packets_received,\n packets_sent, packets_dropped\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(mix_id, date_utc) DO UPDATE SET\n total_stake = excluded.total_stake,\n packets_received = mixnode_daily_stats.packets_received + excluded.packets_received,\n packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent,\n packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "01ee4a30bc3104712e5bc371a45d614a89d88adf02358800433e06100c13c548" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-021c6c65d1ed806d8430bef7883906b42a7e4b280c8efb32db15d7c6a51d7a27.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-021c6c65d1ed806d8430bef7883906b42a7e4b280c8efb32db15d7c6a51d7a27.json deleted file mode 100644 index 2ca61a8300..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-021c6c65d1ed806d8430bef7883906b42a7e4b280c8efb32db15d7c6a51d7a27.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT mix_id as node_id, host, http_api_port\n FROM mixnodes\n WHERE bonded = true\n ", - "describe": { - "columns": [ - { - "name": "node_id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "host", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "http_api_port", - "ordinal": 2, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "021c6c65d1ed806d8430bef7883906b42a7e4b280c8efb32db15d7c6a51d7a27" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json deleted file mode 100644 index ad57224826..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n gateway_identity_key\n FROM\n gateways\n WHERE\n id = ?", - "describe": { - "columns": [ - { - "name": "gateway_identity_key", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-0cf0e4d4f30e90caecffd6255ef85dab12730e538be194438f19ed7f198bd50e.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-0cf0e4d4f30e90caecffd6255ef85dab12730e538be194438f19ed7f198bd50e.json deleted file mode 100644 index fda05c400a..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-0cf0e4d4f30e90caecffd6255ef85dab12730e538be194438f19ed7f198bd50e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE\n testruns\n SET\n status = ?\n WHERE\n status = ?\n AND\n last_assigned_utc < ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "0cf0e4d4f30e90caecffd6255ef85dab12730e538be194438f19ed7f198bd50e" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07.json deleted file mode 100644 index 62823cff34..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n date_utc as \"date_utc!\",\n SUM(total_stake) as \"total_stake!: i64\",\n SUM(packets_received) as \"total_packets_received!: i64\",\n SUM(packets_sent) as \"total_packets_sent!: i64\",\n SUM(packets_dropped) as \"total_packets_dropped!: i64\"\n FROM (\n SELECT\n date_utc,\n n.total_stake,\n n.packets_received,\n n.packets_sent,\n n.packets_dropped\n FROM nym_node_daily_mixing_stats n\n UNION ALL\n SELECT\n m.date_utc,\n m.total_stake,\n m.packets_received,\n m.packets_sent,\n m.packets_dropped\n FROM mixnode_daily_stats m\n LEFT JOIN nym_node_daily_mixing_stats ON m.mix_id = nym_node_daily_mixing_stats.node_id\n WHERE nym_node_daily_mixing_stats.node_id IS NULL\n )\n GROUP BY date_utc\n ORDER BY date_utc ASC\n ", - "describe": { - "columns": [ - { - "name": "date_utc!", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "total_stake!: i64", - "ordinal": 1, - "type_info": "Int" - }, - { - "name": "total_packets_received!: i64", - "ordinal": 2, - "type_info": "Int" - }, - { - "name": "total_packets_sent!: i64", - "ordinal": 3, - "type_info": "Int" - }, - { - "name": "total_packets_dropped!: i64", - "ordinal": 4, - "type_info": "Int" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - true, - true, - true - ] - }, - "hash": "124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-12524c93213c29a4f231785ba25a5b483d9e1a395c65c536fdc4596b69b4584b.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-12524c93213c29a4f231785ba25a5b483d9e1a395c65c536fdc4596b69b4584b.json deleted file mode 100644 index d58822be6c..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-12524c93213c29a4f231785ba25a5b483d9e1a395c65c536fdc4596b69b4584b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM nym_nodes\n WHERE last_updated_utc < ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "12524c93213c29a4f231785ba25a5b483d9e1a395c65c536fdc4596b69b4584b" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json deleted file mode 100644 index 8b69daa6a3..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n key as \"key!\",\n value_json as \"value_json!\",\n last_updated_utc as \"last_updated_utc!\"\n FROM summary", - "describe": { - "columns": [ - { - "name": "key!", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "value_json!", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "last_updated_utc!", - "ordinal": 2, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - true, - true, - false - ] - }, - "hash": "1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-21e44766729777756f6eb04bf3b81df3e591008a1e3fd664ed83ca86ac51bd8c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-21e44766729777756f6eb04bf3b81df3e591008a1e3fd664ed83ca86ac51bd8c.json deleted file mode 100644 index 52929bc782..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-21e44766729777756f6eb04bf3b81df3e591008a1e3fd664ed83ca86ac51bd8c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO mixnode_packet_stats_raw (\n mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped\n ) VALUES (?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "21e44766729777756f6eb04bf3b81df3e591008a1e3fd664ed83ca86ac51bd8c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json deleted file mode 100644 index d9bf0dd6aa..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n id,\n gateway_identity_key\n FROM gateways\n WHERE id = ?\n LIMIT 1", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "gateway_identity_key", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false - ] - }, - "hash": "2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-227539374e7473f6f9642289c5b5d1bcd636315ab23537cb5f6d2f82a2bcb7bf.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-227539374e7473f6f9642289c5b5d1bcd636315ab23537cb5f6d2f82a2bcb7bf.json deleted file mode 100644 index 5a10f1d2c2..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-227539374e7473f6f9642289c5b5d1bcd636315ab23537cb5f6d2f82a2bcb7bf.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n node_id,\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n WHERE\n bond_info IS NOT NULL\n AND\n self_described IS NOT NULL\n ", - "describe": { - "columns": [ - { - "name": "node_id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "bond_info: serde_json::Value", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - true - ] - }, - "hash": "227539374e7473f6f9642289c5b5d1bcd636315ab23537cb5f6d2f82a2bcb7bf" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-25300e435780101fa207c8e26ef2f49ba5db84d63e89440bb494e8327fe73686.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-25300e435780101fa207c8e26ef2f49ba5db84d63e89440bb494e8327fe73686.json deleted file mode 100644 index fa5ef3a17f..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-25300e435780101fa207c8e26ef2f49ba5db84d63e89440bb494e8327fe73686.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT gateway_identity_key\n FROM gateways\n WHERE bonded = true\n ", - "describe": { - "columns": [ - { - "name": "gateway_identity_key", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "25300e435780101fa207c8e26ef2f49ba5db84d63e89440bb494e8327fe73686" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-283f49a65c7d70bf271702ff6a5c7ad6e68c81932d295ff18ed198c54706a57c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-283f49a65c7d70bf271702ff6a5c7ad6e68c81932d295ff18ed198c54706a57c.json deleted file mode 100644 index a20a89ee84..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-283f49a65c7d70bf271702ff6a5c7ad6e68c81932d295ff18ed198c54706a57c.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n WHERE\n self_described IS NOT NULL\n AND\n bond_info IS NOT NULL\n ", - "describe": { - "columns": [ - { - "name": "node_id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "ed25519_identity_pubkey", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "total_stake", - "ordinal": 2, - "type_info": "Int64" - }, - { - "name": "ip_addresses!: serde_json::Value", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "mix_port", - "ordinal": 4, - "type_info": "Int64" - }, - { - "name": "x25519_sphinx_pubkey", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "node_role: serde_json::Value", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "supported_roles: serde_json::Value", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "entry: serde_json::Value", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "performance", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "self_described: serde_json::Value", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "bond_info: serde_json::Value", - "ordinal": 11, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - true, - true - ] - }, - "hash": "283f49a65c7d70bf271702ff6a5c7ad6e68c81932d295ff18ed198c54706a57c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3243cf5646255a9430d1e6710970505d0dbcc62703f40e090e80ff48c77723c4.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3243cf5646255a9430d1e6710970505d0dbcc62703f40e090e80ff48c77723c4.json deleted file mode 100644 index 7aeb02d48b..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3243cf5646255a9430d1e6710970505d0dbcc62703f40e090e80ff48c77723c4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR IGNORE INTO gateway_session_stats\n (gateway_identity_key, node_id, day,\n unique_active_clients, session_started, users_hashes,\n vpn_sessions, mixnet_sessions, unknown_sessions)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "3243cf5646255a9430d1e6710970505d0dbcc62703f40e090e80ff48c77723c4" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3825020ab0ecbffe83270c57ec74fb7987a6807d3381a6068c79b127a668b190.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3825020ab0ecbffe83270c57ec74fb7987a6807d3381a6068c79b127a668b190.json deleted file mode 100644 index 3f34dd62c6..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3825020ab0ecbffe83270c57ec74fb7987a6807d3381a6068c79b127a668b190.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO mixnode_description (\n mix_id, moniker, website, security_contact, details, last_updated_utc\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (mix_id) DO UPDATE SET\n moniker = excluded.moniker,\n website = excluded.website,\n security_contact = excluded.security_contact,\n details = excluded.details,\n last_updated_utc = excluded.last_updated_utc\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "3825020ab0ecbffe83270c57ec74fb7987a6807d3381a6068c79b127a668b190" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3cd5cb4bfca4243925da4ddbccd811e842090e98982e1032670df77961870b32.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3cd5cb4bfca4243925da4ddbccd811e842090e98982e1032670df77961870b32.json deleted file mode 100644 index 60ecc30618..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3cd5cb4bfca4243925da4ddbccd811e842090e98982e1032670df77961870b32.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO mixnodes\n (mix_id, identity_key, bonded, total_stake,\n host, http_api_port, full_details,\n self_described, last_updated_utc, is_dp_delegatee)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(mix_id) DO UPDATE SET\n bonded=excluded.bonded,\n total_stake=excluded.total_stake, host=excluded.host,\n http_api_port=excluded.http_api_port,\n full_details=excluded.full_details,self_described=excluded.self_described,\n last_updated_utc=excluded.last_updated_utc,\n is_dp_delegatee = excluded.is_dp_delegatee;", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "3cd5cb4bfca4243925da4ddbccd811e842090e98982e1032670df77961870b32" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3e7e987780937873cdb393b157d7708c9f01047b0689eb0d4f7a973b328c609d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3e7e987780937873cdb393b157d7708c9f01047b0689eb0d4f7a973b328c609d.json deleted file mode 100644 index 380af2fd77..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3e7e987780937873cdb393b157d7708c9f01047b0689eb0d4f7a973b328c609d.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n id as \"id!\",\n gateway_identity_key as \"gateway_identity_key!\",\n self_described as \"self_described?\",\n explorer_pretty_bond as \"explorer_pretty_bond?\"\n FROM gateways\n WHERE gateway_identity_key = ?\n AND bonded = true\n ORDER BY gateway_identity_key\n LIMIT 1", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "gateway_identity_key!", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "self_described?", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "explorer_pretty_bond?", - "ordinal": 3, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true, - false, - false, - true - ] - }, - "hash": "3e7e987780937873cdb393b157d7708c9f01047b0689eb0d4f7a973b328c609d" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3eb1d8491bda3c1d6e071b6eb364b9a979f4bdb11ea81b2d0f022555bab51ecb.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3eb1d8491bda3c1d6e071b6eb364b9a979f4bdb11ea81b2d0f022555bab51ecb.json deleted file mode 100644 index 1560c0d4e8..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3eb1d8491bda3c1d6e071b6eb364b9a979f4bdb11ea81b2d0f022555bab51ecb.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n COALESCE(gd.moniker, \"NA\") as \"moniker!\",\n COALESCE(gd.website, \"NA\") as \"website!\",\n COALESCE(gd.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(gd.details, \"NA\") as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key", - "describe": { - "columns": [ - { - "name": "gateway_identity_key!", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "bonded: bool", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "performance!", - "ordinal": 2, - "type_info": "Int64" - }, - { - "name": "self_described?", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "explorer_pretty_bond?", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "last_probe_result?", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "last_probe_log?", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "last_testrun_utc?", - "ordinal": 7, - "type_info": "Int64" - }, - { - "name": "last_updated_utc!", - "ordinal": 8, - "type_info": "Int64" - }, - { - "name": "moniker!", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "website!", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "security_contact!", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "details!", - "ordinal": 12, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - true, - false, - false, - false, - false, - false - ] - }, - "hash": "3eb1d8491bda3c1d6e071b6eb364b9a979f4bdb11ea81b2d0f022555bab51ecb" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-3fc2baabf194b147b20be2a49401cc0c100a1d7a7c347393adde2410fa6f4dfe.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-3fc2baabf194b147b20be2a49401cc0c100a1d7a7c347393adde2410fa6f4dfe.json deleted file mode 100644 index b99c055794..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-3fc2baabf194b147b20be2a49401cc0c100a1d7a7c347393adde2410fa6f4dfe.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n total_stake\n FROM mixnodes\n WHERE mix_id = ?\n ", - "describe": { - "columns": [ - { - "name": "total_stake", - "ordinal": 0, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "3fc2baabf194b147b20be2a49401cc0c100a1d7a7c347393adde2410fa6f4dfe" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json deleted file mode 100644 index f9eb3657e7..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE testruns SET status = ? WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json deleted file mode 100644 index 6a9fd9d4eb..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE gateways SET last_probe_log = ? WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-4d865e873c9cb133883da94db72dcdebd4969e1f240def9fb1bf946f4a1f342f.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-4d865e873c9cb133883da94db72dcdebd4969e1f240def9fb1bf946f4a1f342f.json deleted file mode 100644 index 08b9e93fff..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-4d865e873c9cb133883da94db72dcdebd4969e1f240def9fb1bf946f4a1f342f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO testruns (gateway_id, status, ip_address, created_utc, log) VALUES (?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "4d865e873c9cb133883da94db72dcdebd4969e1f240def9fb1bf946f4a1f342f" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-4fca38abbb416d9457c34a8ba4faf481a837eda4f3e1bee1d430a4eb102a5b3d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-4fca38abbb416d9457c34a8ba4faf481a837eda4f3e1bee1d430a4eb102a5b3d.json new file mode 100644 index 0000000000..ee07d52cc2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-4fca38abbb416d9457c34a8ba4faf481a837eda4f3e1bee1d430a4eb102a5b3d.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO gateway_session_stats\n (gateway_identity_key, node_id, day,\n unique_active_clients, session_started, users_hashes,\n vpn_sessions, mixnet_sessions, unknown_sessions)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8", + "Date", + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "4fca38abbb416d9457c34a8ba4faf481a837eda4f3e1bee1d430a4eb102a5b3d" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-5912ea335a957d217f5e2b3a63a25b31715c2098310fe7a9db688bc2fd36aad4.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-5912ea335a957d217f5e2b3a63a25b31715c2098310fe7a9db688bc2fd36aad4.json deleted file mode 100644 index 08bc84273c..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-5912ea335a957d217f5e2b3a63a25b31715c2098310fe7a9db688bc2fd36aad4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO nym_node_daily_mixing_stats (\n node_id, date_utc,\n total_stake, packets_received,\n packets_sent, packets_dropped\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(node_id, date_utc) DO UPDATE SET\n total_stake = excluded.total_stake,\n packets_received = nym_node_daily_mixing_stats.packets_received + excluded.packets_received,\n packets_sent = nym_node_daily_mixing_stats.packets_sent + excluded.packets_sent,\n packets_dropped = nym_node_daily_mixing_stats.packets_dropped + excluded.packets_dropped\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "5912ea335a957d217f5e2b3a63a25b31715c2098310fe7a9db688bc2fd36aad4" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-5e9cbb39f5fb53774803270f422989e199aac4d4a71913c7074359b4bd676b02.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-5e9cbb39f5fb53774803270f422989e199aac4d4a71913c7074359b4bd676b02.json deleted file mode 100644 index e665d23924..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-5e9cbb39f5fb53774803270f422989e199aac4d4a71913c7074359b4bd676b02.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE testruns\n SET\n status = ?,\n last_assigned_utc = ?\n WHERE rowid =\n (\n SELECT rowid\n FROM testruns\n WHERE status = ?\n ORDER BY created_utc asc\n LIMIT 1\n )\n RETURNING\n id as \"id!\",\n gateway_id\n ", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "gateway_id", - "ordinal": 1, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 3 - }, - "nullable": [ - true, - false - ] - }, - "hash": "5e9cbb39f5fb53774803270f422989e199aac4d4a71913c7074359b4bd676b02" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-664e059ac2c58e1115fe214376a6b326b31c93298f20019772cce2e277a194f8.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-664e059ac2c58e1115fe214376a6b326b31c93298f20019772cce2e277a194f8.json deleted file mode 100644 index 2ba3ec13fa..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-664e059ac2c58e1115fe214376a6b326b31c93298f20019772cce2e277a194f8.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO nym_nodes\n (node_id, ed25519_identity_pubkey,\n total_stake,\n ip_addresses, mix_port,\n x25519_sphinx_pubkey, node_role,\n supported_roles, entry,\n self_described,\n bond_info,\n performance, last_updated_utc\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(node_id) DO UPDATE SET\n ed25519_identity_pubkey=excluded.ed25519_identity_pubkey,\n ip_addresses=excluded.ip_addresses,\n mix_port=excluded.mix_port,\n x25519_sphinx_pubkey=excluded.x25519_sphinx_pubkey,\n node_role=excluded.node_role,\n supported_roles=excluded.supported_roles,\n entry=excluded.entry,\n self_described=excluded.self_described,\n bond_info=excluded.bond_info,\n performance=excluded.performance,\n last_updated_utc=excluded.last_updated_utc\n ;", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "664e059ac2c58e1115fe214376a6b326b31c93298f20019772cce2e277a194f8" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json index 38fe641a3a..5e60c5a545 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json @@ -1,19 +1,19 @@ { - "db_name": "SQLite", + "db_name": "PostgreSQL", "query": "SELECT count(id) FROM mixnodes", "describe": { "columns": [ { - "name": "count(id)", "ordinal": 0, - "type_info": "Int" + "name": "count", + "type_info": "Int8" } ], "parameters": { - "Right": 0 + "Left": [] }, "nullable": [ - false + null ] }, "hash": "670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b" diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-6de15de62c0caa545910a17877a3ac5ebe44a398b199ab0120207a5569f54d0b.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-6de15de62c0caa545910a17877a3ac5ebe44a398b199ab0120207a5569f54d0b.json new file mode 100644 index 0000000000..b9690c68ee --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-6de15de62c0caa545910a17877a3ac5ebe44a398b199ab0120207a5569f54d0b.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO testruns (gateway_id, status, ip_address, created_utc, log) VALUES ($1, $2, $3, $4, $5) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4", + "Int4", + "Varchar", + "Int8", + "Varchar" + ] + }, + "nullable": [ + false + ] + }, + "hash": "6de15de62c0caa545910a17877a3ac5ebe44a398b199ab0120207a5569f54d0b" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json deleted file mode 100644 index 9d93b219f9..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE gateways SET last_probe_result = ? WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f75af377da33db1455c6e0f612e0fa9583888f343b8b59faf37fc6799b244379.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd.json similarity index 61% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-f75af377da33db1455c6e0f612e0fa9583888f343b8b59faf37fc6799b244379.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd.json index 484facb672..7fa6221256 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f75af377da33db1455c6e0f612e0fa9583888f343b8b59faf37fc6799b244379.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd.json @@ -1,66 +1,66 @@ { - "db_name": "SQLite", - "query": "SELECT\n mn.mix_id as \"mix_id!\",\n mn.bonded as \"bonded: bool\",\n mn.is_dp_delegatee as \"is_dp_delegatee: bool\",\n mn.total_stake as \"total_stake!\",\n mn.full_details as \"full_details!\",\n mn.self_described as \"self_described\",\n mn.last_updated_utc as \"last_updated_utc!\",\n COALESCE(md.moniker, \"NA\") as \"moniker!\",\n COALESCE(md.website, \"NA\") as \"website!\",\n COALESCE(md.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(md.details, \"NA\") as \"details!\"\n FROM mixnodes mn\n LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id\n ORDER BY mn.mix_id", + "db_name": "PostgreSQL", + "query": "SELECT\n mn.mix_id as \"mix_id!\",\n mn.bonded as \"bonded: bool\",\n mn.is_dp_delegatee as \"is_dp_delegatee: bool\",\n mn.total_stake as \"total_stake!\",\n mn.full_details as \"full_details!\",\n mn.self_described as \"self_described\",\n mn.last_updated_utc as \"last_updated_utc!\",\n md.moniker as \"moniker!\",\n md.website as \"website!\",\n md.security_contact as \"security_contact!\",\n md.details as \"details!\"\n FROM mixnodes mn\n LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id\n ORDER BY mn.mix_id", "describe": { "columns": [ { - "name": "mix_id!", "ordinal": 0, - "type_info": "Int64" + "name": "mix_id!", + "type_info": "Int8" }, { - "name": "bonded: bool", "ordinal": 1, - "type_info": "Int64" + "name": "bonded: bool", + "type_info": "Bool" }, { - "name": "is_dp_delegatee: bool", "ordinal": 2, - "type_info": "Int64" + "name": "is_dp_delegatee: bool", + "type_info": "Bool" }, { - "name": "total_stake!", "ordinal": 3, - "type_info": "Int64" + "name": "total_stake!", + "type_info": "Int8" }, { - "name": "full_details!", "ordinal": 4, - "type_info": "Text" + "name": "full_details!", + "type_info": "Varchar" }, { - "name": "self_described", "ordinal": 5, - "type_info": "Text" + "name": "self_described", + "type_info": "Varchar" }, { - "name": "last_updated_utc!", "ordinal": 6, - "type_info": "Int64" + "name": "last_updated_utc!", + "type_info": "Int8" }, { - "name": "moniker!", "ordinal": 7, - "type_info": "Text" + "name": "moniker!", + "type_info": "Varchar" }, { - "name": "website!", "ordinal": 8, - "type_info": "Text" + "name": "website!", + "type_info": "Varchar" }, { - "name": "security_contact!", "ordinal": 9, - "type_info": "Text" + "name": "security_contact!", + "type_info": "Varchar" }, { - "name": "details!", "ordinal": 10, - "type_info": "Text" + "name": "details!", + "type_info": "Varchar" } ], "parameters": { - "Right": 0 + "Left": [] }, "nullable": [ false, @@ -70,11 +70,11 @@ true, true, false, - false, - false, - false, - false + true, + true, + true, + true ] }, - "hash": "f75af377da33db1455c6e0f612e0fa9583888f343b8b59faf37fc6799b244379" + "hash": "74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd" } diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-7526390c3d17622564067f153867295a279179315650f6fb363d5f3177bf70e3.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-7526390c3d17622564067f153867295a279179315650f6fb363d5f3177bf70e3.json deleted file mode 100644 index c339cc96bd..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-7526390c3d17622564067f153867295a279179315650f6fb363d5f3177bf70e3.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n node_id,\n self_described as \"self_described: serde_json::Value\"\n FROM\n nym_nodes\n WHERE\n self_described IS NOT NULL\n ", - "describe": { - "columns": [ - { - "name": "node_id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "self_described: serde_json::Value", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - true - ] - }, - "hash": "7526390c3d17622564067f153867295a279179315650f6fb363d5f3177bf70e3" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json deleted file mode 100644 index 9cba203bdb..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n id as \"id!\",\n date as \"date!\",\n timestamp_utc as \"timestamp_utc!\",\n value_json as \"value_json!\"\n FROM summary_history\n ORDER BY date DESC\n LIMIT 30", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "date!", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "timestamp_utc!", - "ordinal": 2, - "type_info": "Int64" - }, - { - "name": "value_json!", - "ordinal": 3, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - true, - false, - false, - true - ] - }, - "hash": "7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json deleted file mode 100644 index 5252ccf2e7..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO summary_history\n (date, timestamp_utc, value_json)\n VALUES (?, ?, ?)\n ON CONFLICT(date) DO UPDATE SET\n timestamp_utc=excluded.timestamp_utc,\n value_json=excluded.value_json;", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json index 60583ed8b2..3d922cb7f0 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json @@ -1,19 +1,19 @@ { - "db_name": "SQLite", + "db_name": "PostgreSQL", "query": "SELECT count(id) FROM gateways", "describe": { "columns": [ { - "name": "count(id)", "ordinal": 0, - "type_info": "Int" + "name": "count", + "type_info": "Int8" } ], "parameters": { - "Right": 0 + "Left": [] }, "nullable": [ - false + null ] }, "hash": "86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5" diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-9104e50524ad5a103bd199a0531d73b74876e9aecda2117227e2e180258d91a1.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-9104e50524ad5a103bd199a0531d73b74876e9aecda2117227e2e180258d91a1.json deleted file mode 100644 index e3e26ae088..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-9104e50524ad5a103bd199a0531d73b74876e9aecda2117227e2e180258d91a1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO nym_node_descriptions (\n node_id, moniker, website, security_contact, details, last_updated_utc\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (node_id) DO UPDATE SET\n moniker = excluded.moniker,\n website = excluded.website,\n security_contact = excluded.security_contact,\n details = excluded.details,\n last_updated_utc = excluded.last_updated_utc\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "9104e50524ad5a103bd199a0531d73b74876e9aecda2117227e2e180258d91a1" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json index 6c2a194e80..d87f4c21ca 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json @@ -1,21 +1,21 @@ { - "db_name": "SQLite", + "db_name": "PostgreSQL", "query": "SELECT\n gateway_identity_key as \"gateway_identity_key!\",\n bonded as \"bonded: bool\"\n FROM gateways\n ORDER BY last_testrun_utc", "describe": { "columns": [ { - "name": "gateway_identity_key!", "ordinal": 0, - "type_info": "Text" + "name": "gateway_identity_key!", + "type_info": "Varchar" }, { - "name": "bonded: bool", "ordinal": 1, - "type_info": "Int64" + "name": "bonded: bool", + "type_info": "Bool" } ], "parameters": { - "Right": 0 + "Left": [] }, "nullable": [ false, diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-9334f0c91252fcd7ec72558a271222615bb282e5334665700709ae475a5daea2.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-9334f0c91252fcd7ec72558a271222615bb282e5334665700709ae475a5daea2.json deleted file mode 100644 index 5f5dcdcf44..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-9334f0c91252fcd7ec72558a271222615bb282e5334665700709ae475a5daea2.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n ", - "describe": { - "columns": [ - { - "name": "node_id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "ed25519_identity_pubkey", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "total_stake", - "ordinal": 2, - "type_info": "Int64" - }, - { - "name": "ip_addresses!: serde_json::Value", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "mix_port", - "ordinal": 4, - "type_info": "Int64" - }, - { - "name": "x25519_sphinx_pubkey", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "node_role: serde_json::Value", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "supported_roles: serde_json::Value", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "entry: serde_json::Value", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "performance", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "self_described: serde_json::Value", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "bond_info: serde_json::Value", - "ordinal": 11, - "type_info": "Text" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - true, - false, - true, - true - ] - }, - "hash": "9334f0c91252fcd7ec72558a271222615bb282e5334665700709ae475a5daea2" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-9796d354ae075eab4cbd3438839c39da94025494395ec7b093aefef696f2d0c5.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-9796d354ae075eab4cbd3438839c39da94025494395ec7b093aefef696f2d0c5.json deleted file mode 100644 index 967a2c648c..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-9796d354ae075eab4cbd3438839c39da94025494395ec7b093aefef696f2d0c5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO gateways\n (gateway_identity_key, bonded,\n self_described, explorer_pretty_bond,\n last_updated_utc, performance)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(gateway_identity_key) DO UPDATE SET\n bonded=excluded.bonded,\n self_described=excluded.self_described,\n explorer_pretty_bond=excluded.explorer_pretty_bond,\n last_updated_utc=excluded.last_updated_utc,\n performance = excluded.performance;", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "9796d354ae075eab4cbd3438839c39da94025494395ec7b093aefef696f2d0c5" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-97f4b340bb0f6c9c78d77b24d4f9d3d13ab61cbaac6f8c3cfc06c95e0f13252f.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-97f4b340bb0f6c9c78d77b24d4f9d3d13ab61cbaac6f8c3cfc06c95e0f13252f.json deleted file mode 100644 index 53ad65993f..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-97f4b340bb0f6c9c78d77b24d4f9d3d13ab61cbaac6f8c3cfc06c95e0f13252f.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n COALESCE(packets_received, 0) as packets_received,\n COALESCE(packets_sent, 0) as packets_sent,\n COALESCE(packets_dropped, 0) as packets_dropped\n FROM nym_nodes_packet_stats_raw\n WHERE node_id = ?\n ORDER BY timestamp_utc DESC\n LIMIT 1 OFFSET 1\n ", - "describe": { - "columns": [ - { - "name": "packets_received", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "packets_sent", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "packets_dropped", - "ordinal": 2, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "97f4b340bb0f6c9c78d77b24d4f9d3d13ab61cbaac6f8c3cfc06c95e0f13252f" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-a19da2073bc691fe5cd2119a9527c16d6a8806ba50dcb759ac705b1a19288ca9.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-a19da2073bc691fe5cd2119a9527c16d6a8806ba50dcb759ac705b1a19288ca9.json new file mode 100644 index 0000000000..afd4055f0a --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-a19da2073bc691fe5cd2119a9527c16d6a8806ba50dcb759ac705b1a19288ca9.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n date_utc as \"date_utc!\",\n SUM(total_stake)::BIGINT as \"total_stake!: i64\",\n SUM(packets_received)::BIGINT as \"total_packets_received!: i64\",\n SUM(packets_sent)::BIGINT as \"total_packets_sent!: i64\",\n SUM(packets_dropped)::BIGINT as \"total_packets_dropped!: i64\"\n FROM (\n SELECT\n date_utc,\n n.total_stake,\n n.packets_received,\n n.packets_sent,\n n.packets_dropped\n FROM nym_node_daily_mixing_stats n\n UNION ALL\n SELECT\n m.date_utc,\n m.total_stake,\n m.packets_received,\n m.packets_sent,\n m.packets_dropped\n FROM mixnode_daily_stats m\n LEFT JOIN nym_node_daily_mixing_stats ON m.mix_id = nym_node_daily_mixing_stats.node_id\n WHERE nym_node_daily_mixing_stats.node_id IS NULL\n )\n GROUP BY date_utc\n ORDER BY date_utc ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "date_utc!", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "total_stake!: i64", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "total_packets_received!: i64", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "total_packets_sent!: i64", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "total_packets_dropped!: i64", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + null, + null, + null, + null + ] + }, + "hash": "a19da2073bc691fe5cd2119a9527c16d6a8806ba50dcb759ac705b1a19288ca9" +} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-a1d9eb816acd1a91ed0975c801c9295c01a78861a2a0597ad28b1579a14bf008.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-a1d9eb816acd1a91ed0975c801c9295c01a78861a2a0597ad28b1579a14bf008.json deleted file mode 100644 index b5cc3e88fa..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-a1d9eb816acd1a91ed0975c801c9295c01a78861a2a0597ad28b1579a14bf008.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n created_utc as \"created_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\",\n last_assigned_utc\n FROM testruns\n WHERE gateway_id = ? AND status != 2\n ORDER BY id DESC\n LIMIT 1", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "gateway_id!", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "status!", - "ordinal": 2, - "type_info": "Int64" - }, - { - "name": "created_utc!", - "ordinal": 3, - "type_info": "Int64" - }, - { - "name": "ip_address!", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "log!", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "last_assigned_utc", - "ordinal": 6, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "a1d9eb816acd1a91ed0975c801c9295c01a78861a2a0597ad28b1579a14bf008" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-a79a61b87325f3f1d9c5a4fb386ccd585be0641e5878acb6283b879f22ed2b4c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-a79a61b87325f3f1d9c5a4fb386ccd585be0641e5878acb6283b879f22ed2b4c.json deleted file mode 100644 index 37dd4ac669..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-a79a61b87325f3f1d9c5a4fb386ccd585be0641e5878acb6283b879f22ed2b4c.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n COUNT(id) as \"count: i64\"\n FROM testruns\n WHERE\n status = ?\n ", - "describe": { - "columns": [ - { - "name": "count: i64", - "ordinal": 0, - "type_info": "Int" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "a79a61b87325f3f1d9c5a4fb386ccd585be0641e5878acb6283b879f22ed2b4c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-b68796d1d8d2384b30f1aace06269682c4ae96f774261f5c298264d3c12e5b67.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-b68796d1d8d2384b30f1aace06269682c4ae96f774261f5c298264d3c12e5b67.json deleted file mode 100644 index 383c3681f0..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-b68796d1d8d2384b30f1aace06269682c4ae96f774261f5c298264d3c12e5b67.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE nym_nodes\n SET\n self_described = NULL,\n bond_info = NULL", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "b68796d1d8d2384b30f1aace06269682c4ae96f774261f5c298264d3c12e5b67" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json deleted file mode 100644 index 8cc95e72de..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-c42917c9542c1d720d92035863064741aefc9f7a7d1630f6b863ebd8174b6684.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-c42917c9542c1d720d92035863064741aefc9f7a7d1630f6b863ebd8174b6684.json deleted file mode 100644 index 86b8a642bd..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-c42917c9542c1d720d92035863064741aefc9f7a7d1630f6b863ebd8174b6684.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE\n mixnodes\n SET\n bonded = false\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "c42917c9542c1d720d92035863064741aefc9f7a7d1630f6b863ebd8174b6684" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-c910788edefe64bbb34379702bcbde9ec6159c9fa03b13652e1f620dcd92125e.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-c910788edefe64bbb34379702bcbde9ec6159c9fa03b13652e1f620dcd92125e.json deleted file mode 100644 index 5c06c0e053..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-c910788edefe64bbb34379702bcbde9ec6159c9fa03b13652e1f620dcd92125e.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT mix_id\n FROM mixnodes\n WHERE bonded = true\n ", - "describe": { - "columns": [ - { - "name": "mix_id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - }, - "hash": "c910788edefe64bbb34379702bcbde9ec6159c9fa03b13652e1f620dcd92125e" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-c9e61180ec35dfab8623333fafa3b216b93440d0fddc0a37dd1b6c1813741f6a.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-c9e61180ec35dfab8623333fafa3b216b93440d0fddc0a37dd1b6c1813741f6a.json deleted file mode 100644 index 0d313098b6..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-c9e61180ec35dfab8623333fafa3b216b93440d0fddc0a37dd1b6c1813741f6a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM gateway_session_stats WHERE day <= ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "c9e61180ec35dfab8623333fafa3b216b93440d0fddc0a37dd1b6c1813741f6a" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-d2e07d44594ca5b44a6100482ff432c39d761f2a0ac1d6515cf73416f2eb6c61.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-d2e07d44594ca5b44a6100482ff432c39d761f2a0ac1d6515cf73416f2eb6c61.json deleted file mode 100644 index abcb28773f..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-d2e07d44594ca5b44a6100482ff432c39d761f2a0ac1d6515cf73416f2eb6c61.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n total_stake\n FROM nym_nodes\n WHERE node_id = ?\n ", - "describe": { - "columns": [ - { - "name": "total_stake", - "ordinal": 0, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "d2e07d44594ca5b44a6100482ff432c39d761f2a0ac1d6515cf73416f2eb6c61" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json deleted file mode 100644 index 5f7443bd50..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO summary\n (key, value_json, last_updated_utc)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value_json=excluded.value_json,\n last_updated_utc=excluded.last_updated_utc;", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f0c64794cbaed87a1d3166251d8e6720c9a9fc929144188460849be85d915004.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-f0c64794cbaed87a1d3166251d8e6720c9a9fc929144188460849be85d915004.json deleted file mode 100644 index cf1df0017b..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f0c64794cbaed87a1d3166251d8e6720c9a9fc929144188460849be85d915004.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n created_utc as \"created_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\",\n last_assigned_utc\n FROM testruns\n WHERE\n id = ?\n AND\n status = ?\n ORDER BY created_utc\n LIMIT 1", - "describe": { - "columns": [ - { - "name": "id!", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "gateway_id!", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "status!", - "ordinal": 2, - "type_info": "Int64" - }, - { - "name": "created_utc!", - "ordinal": 3, - "type_info": "Int64" - }, - { - "name": "ip_address!", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "log!", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "last_assigned_utc", - "ordinal": 6, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "f0c64794cbaed87a1d3166251d8e6720c9a9fc929144188460849be85d915004" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f1f47a4490c3e1330885ef3cf3cda054f2cf760520a46a94db22a02a9cb53dba.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-f1f47a4490c3e1330885ef3cf3cda054f2cf760520a46a94db22a02a9cb53dba.json deleted file mode 100644 index 3f55754cc9..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f1f47a4490c3e1330885ef3cf3cda054f2cf760520a46a94db22a02a9cb53dba.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT\n COALESCE(packets_received, 0) as packets_received,\n COALESCE(packets_sent, 0) as packets_sent,\n COALESCE(packets_dropped, 0) as packets_dropped\n FROM mixnode_packet_stats_raw\n WHERE mix_id = ?\n ORDER BY timestamp_utc DESC\n LIMIT 1 OFFSET 1\n ", - "describe": { - "columns": [ - { - "name": "packets_received", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "packets_sent", - "ordinal": 1, - "type_info": "Int64" - }, - { - "name": "packets_dropped", - "ordinal": 2, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "f1f47a4490c3e1330885ef3cf3cda054f2cf760520a46a94db22a02a9cb53dba" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f2a7e7229b414a6283685558d1d0731d21b1a92eb39481cf342b87c3b5b0f759.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-f2a7e7229b414a6283685558d1d0731d21b1a92eb39481cf342b87c3b5b0f759.json deleted file mode 100644 index dd5529e9e7..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f2a7e7229b414a6283685558d1d0731d21b1a92eb39481cf342b87c3b5b0f759.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO gateway_description (\n gateway_identity_key,\n moniker,\n website,\n security_contact,\n details,\n last_updated_utc\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (gateway_identity_key) DO UPDATE SET\n moniker = excluded.moniker,\n website = excluded.website,\n security_contact = excluded.security_contact,\n details = excluded.details,\n last_updated_utc = excluded.last_updated_utc\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "f2a7e7229b414a6283685558d1d0731d21b1a92eb39481cf342b87c3b5b0f759" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-f7e3fa31d68c028bf39cc95389f29f8758ec922dd2e7ea064a1e537e580c9ee5.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-f7e3fa31d68c028bf39cc95389f29f8758ec922dd2e7ea064a1e537e580c9ee5.json deleted file mode 100644 index da95053996..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-f7e3fa31d68c028bf39cc95389f29f8758ec922dd2e7ea064a1e537e580c9ee5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE\n gateways\n SET\n bonded = false\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - }, - "hash": "f7e3fa31d68c028bf39cc95389f29f8758ec922dd2e7ea064a1e537e580c9ee5" -} diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-fcb1698d9e0e3a14524c92e7c99a811588c2bbc50d4975487a0464321a1b18c9.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-fcb1698d9e0e3a14524c92e7c99a811588c2bbc50d4975487a0464321a1b18c9.json deleted file mode 100644 index 296c6389d2..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-fcb1698d9e0e3a14524c92e7c99a811588c2bbc50d4975487a0464321a1b18c9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO nym_nodes_packet_stats_raw (\n node_id, timestamp_utc, packets_received, packets_sent, packets_dropped\n ) VALUES (?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "fcb1698d9e0e3a14524c92e7c99a811588c2bbc50d4975487a0464321a1b18c9" -} diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 936f86cf0a..841ffd1e7d 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "2.1.0" +version = "3.3.2" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -17,36 +17,41 @@ ammonia = { workspace = true } anyhow = { workspace = true } axum = { workspace = true, features = ["tokio", "macros"] } bip39 = { workspace = true } -chrono = { workspace = true } +celes = { workspace = true } clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } cosmwasm-std = { workspace = true } -envy = { workspace = true } futures-util = { workspace = true } +humantime = { workspace = true } itertools = { workspace = true } moka = { workspace = true, features = ["future"] } + nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } nym-bin-common = { path = "../../common/bin-common", features = ["models"] } nym-node-status-client = { path = "../nym-node-status-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] } nym-http-api-client = { path = "../../common/http-api-client" } +nym-http-api-common = { path = "../../common/http-api-common", features = ["middleware"] } nym-network-defaults = { path = "../../common/network-defaults" } nym-serde-helpers = { path = "../../common/serde-helpers" } nym-statistics-common = { path = "../../common/statistics" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-task = { path = "../../common/task" } nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] } + rand = { workspace = true } rand_chacha = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } +semver = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_json_path = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "time"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "time"] } thiserror = { workspace = true } -time = { workspace = true } +time = { workspace = true, features = ["formatting"] } tokio = { workspace = true, features = ["rt-multi-thread"] } tokio-util = { workspace = true } tracing = { workspace = true } @@ -59,13 +64,20 @@ utoipauto = { workspace = true } nym-node-metrics = { path = "../../nym-node/nym-node-metrics" } +[features] +default = ["pg"] +sqlite = ["sqlx/sqlite"] +pg = ["sqlx/postgres"] [build-dependencies] anyhow = { workspace = true } tokio = { workspace = true, features = ["macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", - "sqlite", "macros", "migrate", ] } + +[dev-dependencies] +axum-test = "17.3.0" +time = { workspace = true, features = ["macros"] } diff --git a/nym-node-status-api/nym-node-status-api/Dockerfile b/nym-node-status-api/nym-node-status-api/Dockerfile-pg similarity index 90% rename from nym-node-status-api/nym-node-status-api/Dockerfile rename to nym-node-status-api/nym-node-status-api/Dockerfile-pg index 6eeaf8c6ee..4a6c7b24c5 100644 --- a/nym-node-status-api/nym-node-status-api/Dockerfile +++ b/nym-node-status-api/nym-node-status-api/Dockerfile-pg @@ -2,9 +2,9 @@ FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym -WORKDIR /usr/src/nym/nym-node-status-api +WORKDIR /usr/src/nym/nym-node-status-api/nym-node-status-api/ -RUN cargo build --release +RUN cargo build --release --features pg #------------------------------------------------------------------- diff --git a/nym-node-status-api/nym-node-status-api/Dockerfile-sqlite b/nym-node-status-api/nym-node-status-api/Dockerfile-sqlite new file mode 100644 index 0000000000..c37afedb08 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/Dockerfile-sqlite @@ -0,0 +1,37 @@ +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-api/nym-node-status-api/ + +RUN cargo build --release --features sqlite --no-default-features + + +#------------------------------------------------------------------- +# The following environment variables are required at runtime: +# +# EXPLORER_API +# NYXD +# NYM_API +# DATABASE_URL +# +# And optionally: +# +# NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL +# NYM_NODE_STATUS_API_HTTP_PORT +# NYM_API_CLIENT_TIMEOUT +# EXPLORER_CLIENT_TIMEOUT +# NODE_STATUS_API_MONITOR_REFRESH_INTERVAL +# NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL +# +# see https://github.com/nymtech/nym/blob/develop/nym-node-status-api/src/cli.rs for details +#------------------------------------------------------------------- + +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/nym-node-status-api/Makefile b/nym-node-status-api/nym-node-status-api/Makefile new file mode 100644 index 0000000000..5f2881c1b4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/Makefile @@ -0,0 +1,117 @@ +# Makefile for nym-node-status-api database management + +# --- Configuration --- +TEST_DATABASE_URL := postgres://testuser:testpass@localhost:5433/nym_node_status_api_test + +# Docker compose service names +DB_SERVICE_NAME := postgres-test +DB_CONTAINER_NAME := nym_node_status_api_postgres_test + +# Default target +.PHONY: default +default: help + +# --- Main Targets --- +.PHONY: prepare-pg +prepare-pg: test-db-up test-db-wait test-db-migrate test-db-prepare test-db-down ## Setup PostgreSQL and prepare SQLx offline cache + +.PHONY: test-db +test-db: test-db-up test-db-wait test-db-migrate test-db-run test-db-down ## Run tests with PostgreSQL database + +.PHONY: dev-db +dev-db: test-db-up test-db-wait test-db-migrate ## Start PostgreSQL for development (keeps running) + @echo "PostgreSQL is running on port 5433" + @echo "Connection string: $(TEST_DATABASE_URL)" + +# --- Docker Compose Targets --- +.PHONY: test-db-up +test-db-up: ## Start the PostgreSQL test database in the background + @echo "Starting PostgreSQL test database..." + docker compose up -d $(DB_SERVICE_NAME) + +.PHONY: test-db-wait +test-db-wait: ## Wait for the PostgreSQL database to be healthy + @echo "Waiting for PostgreSQL database..." + @while ! docker inspect --format='{{.State.Health.Status}}' $(DB_CONTAINER_NAME) 2>/dev/null | grep -q 'healthy'; do \ + echo -n "."; \ + sleep 1; \ + done; \ + echo " Database is healthy!" + +.PHONY: test-db-down +test-db-down: ## Stop and remove the test database + @echo "Stopping PostgreSQL test database..." + docker compose down + +# --- SQLx Targets --- +.PHONY: test-db-migrate +test-db-migrate: ## Run database migrations against PostgreSQL + @echo "Running PostgreSQL migrations..." + DATABASE_URL="$(TEST_DATABASE_URL)" sqlx migrate run --source migrations_pg + +.PHONY: test-db-prepare +test-db-prepare: ## Run sqlx prepare for compile-time query verification + @echo "Running sqlx prepare for PostgreSQL..." + DATABASE_URL="$(TEST_DATABASE_URL)" cargo sqlx prepare -- --features pg + +# --- Build and Test Targets --- +.PHONY: test-db-run +test-db-run: ## Run tests with PostgreSQL feature + @echo "Running tests with PostgreSQL..." + DATABASE_URL="$(TEST_DATABASE_URL)" cargo test --features pg --no-default-features + +.PHONY: build-pg +build-pg: ## Build with PostgreSQL feature + @echo "Building with PostgreSQL feature..." + cargo build --features pg --no-default-features + +.PHONY: build-sqlite +build-sqlite: ## Build with SQLite feature (default) + @echo "Building with SQLite feature..." + cargo build --features sqlite --no-default-features + +.PHONY: check-pg +check-pg: ## Check code with PostgreSQL feature + @echo "Checking code with PostgreSQL feature..." + cargo check --features pg --no-default-features + +.PHONY: check-sqlite +check-sqlite: ## Check code with SQLite feature + @echo "Checking code with SQLite feature..." + cargo check --features sqlite --no-default-features + +.PHONY: clippy +clippy: clippy-pg clippy-sqlite + +.PHONY: clippy-pg +clippy-pg: ## Run clippy with PostgreSQL feature + @echo "Running clippy with PostgreSQL feature..." + cargo clippy --features pg --no-default-features -- -D warnings + +.PHONY: clippy-sqlite +clippy-sqlite: ## Run clippy with SQLite feature (default) + @echo "Running clippy with SQLite feature..." + cargo clippy --features sqlite --no-default-features -- -D warnings + +# --- Cleanup Targets --- +.PHONY: clean +clean: ## Clean build artifacts and SQLx cache + cargo clean + rm -rf .sqlx + +.PHONY: clean-db +clean-db: test-db-down ## Stop database and clean volumes + docker volume rm -f nym-node-status-api_postgres_test_data 2>/dev/null || true + +# --- Utility Targets --- +.PHONY: sqlx-cli +sqlx-cli: ## Install sqlx-cli if not already installed + @command -v sqlx >/dev/null 2>&1 || cargo install sqlx-cli --features postgres,sqlite + +.PHONY: psql +psql: ## Connect to the running PostgreSQL database with psql + @docker exec -it $(DB_CONTAINER_NAME) psql -U testuser -d nym_node_status_api_test + +.PHONY: help +help: ## Show help for Makefile targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' diff --git a/nym-node-status-api/nym-node-status-api/README_PG.md b/nym-node-status-api/nym-node-status-api/README_PG.md new file mode 100644 index 0000000000..8a6d2c3f0b --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/README_PG.md @@ -0,0 +1,104 @@ +# PostgreSQL Support for nym-node-status-api + +This project now supports both SQLite (default) and PostgreSQL databases. + +## Quick Start with PostgreSQL + +### 1. Install Prerequisites + +```bash +# Install sqlx-cli if not already installed +make sqlx-cli +``` + +### 2. Prepare PostgreSQL for Development + +```bash +# This will: +# - Start PostgreSQL in Docker +# - Run migrations +# - Generate SQLx offline query cache +# - Stop the database +make prepare-pg +``` + +### 3. Build with PostgreSQL + +```bash +# Build with PostgreSQL feature +make build-pg + +# Or manually: +cargo build --features pg --no-default-features +``` + +### 4. Run with PostgreSQL + +```bash +# Start PostgreSQL for development (keeps running) +make dev-db + +# In another terminal, run the application +DATABASE_URL=postgres://testuser:testpass@localhost:5433/nym_node_status_api_test \ +cargo run --features pg --no-default-features +``` + +## Database Features + +- `sqlite` (default): Uses SQLite database +- `pg`: Uses PostgreSQL database + +Only one database feature can be active at a time. + +## Migration Differences + +SQLite migrations are in `migrations/`, PostgreSQL migrations are in `migrations_pg/`. + +Key differences: +- **AUTOINCREMENT** → **SERIAL** +- **INTEGER CHECK (0,1)** → **BOOLEAN** +- **REAL** → **DOUBLE PRECISION** +- No table recreation needed for constraint changes in PostgreSQL + +## Makefile Targets + +```bash +make help # Show all available targets +make prepare-pg # Setup PostgreSQL and prepare SQLx cache +make dev-db # Start PostgreSQL for development +make test-db # Run tests with PostgreSQL +make build-pg # Build with PostgreSQL +make build-sqlite # Build with SQLite +make psql # Connect to running PostgreSQL +make clean # Clean build artifacts +make clean-db # Stop database and clean volumes +``` + +## Environment Variables + +See `.env.example` for all configuration options. Key variable: + +```bash +# For PostgreSQL: +DATABASE_URL=postgres://testuser:testpass@localhost:5433/nym_node_status_api_test + +# For SQLite: +DATABASE_URL=sqlite://nym-node-status-api.sqlite +``` + +## Troubleshooting + +### SQLx Offline Mode + +If you see "no cached data for this query" errors: + +1. Ensure PostgreSQL is running: `make dev-db` +2. Run: `make test-db-prepare` + +### Connection Refused + +If you see "Connection refused" errors: + +1. Check Docker is running: `docker ps` +2. Check PostgreSQL container: `docker ps | grep nym_node_status_api_postgres_test` +3. Restart database: `make test-db-down && make dev-db` \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/build-push-node-status-api.sh b/nym-node-status-api/nym-node-status-api/build-push-node-status-api.sh new file mode 100755 index 0000000000..8aaa823ba6 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/build-push-node-status-api.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Build and push Node Status API container to harbor.nymte.ch + +set -e + +# Configuration +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +WORKING_DIRECTORY="${SCRIPT_DIR}" +CONTAINER_NAME="node-status-api" +REGISTRY="harbor.nymte.ch" +NAMESPACE="nym" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to display usage +usage() { + echo "Usage: $0 [pg|sqlite|both]" + echo " pg - Build and push PostgreSQL version" + echo " sqlite - Build and push SQLite version" + echo " both - Build and push both versions (default)" + exit 1 +} + +# Parse arguments +DB_TYPE="${1:-both}" + +if [[ ! "$DB_TYPE" =~ ^(pg|sqlite|both)$ ]]; then + usage +fi + +# Get version from Cargo.toml +VERSION=$(grep "^version = " "${WORKING_DIRECTORY}/Cargo.toml" | sed -E 's/version = "(.*)"/\1/') +if [ -z "$VERSION" ]; then + echo -e "${RED}Error: Could not extract version from Cargo.toml${NC}" + exit 1 +fi +echo -e "${YELLOW}Version: ${VERSION}${NC}" + +# Login to Harbor +echo -e "${GREEN}Logging into Harbor...${NC}" +docker login "${REGISTRY}" + +# Function to build and push +build_and_push() { + local db_type=$1 + local dockerfile="Dockerfile-${db_type}" + + echo -e "${GREEN}Building ${db_type} container...${NC}" + # Build from repository root (two levels up from script location) + docker build -f "${WORKING_DIRECTORY}/${dockerfile}" "${SCRIPT_DIR}/../.." \ + -t "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:${VERSION}-${db_type}" \ + -t "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:latest-${db_type}" + + echo -e "${GREEN}Pushing ${db_type} container to Harbor...${NC}" + docker push "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:${VERSION}-${db_type}" + docker push "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:latest-${db_type}" + + echo -e "${GREEN}Successfully built and pushed ${CONTAINER_NAME}:${VERSION}-${db_type}${NC}" +} + +# Build based on selection +case "$DB_TYPE" in + pg) + build_and_push "pg" + ;; + sqlite) + build_and_push "sqlite" + ;; + both) + build_and_push "pg" + echo "" + build_and_push "sqlite" + ;; +esac + +echo -e "${GREEN}All builds completed successfully!${NC}" \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/build.rs b/nym-node-status-api/nym-node-status-api/build.rs index 1032fcb691..f2fe512707 100644 --- a/nym-node-status-api/nym-node-status-api/build.rs +++ b/nym-node-status-api/nym-node-status-api/build.rs @@ -1,58 +1,13 @@ -use anyhow::{anyhow, Result}; -use sqlx::{Connection, SqliteConnection}; -#[cfg(target_family = "unix")] -use std::fs::Permissions; -#[cfg(target_family = "unix")] -use std::os::unix::fs::PermissionsExt; -use tokio::{fs::File, io::AsyncWriteExt}; - -const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; +use anyhow::Result; /// If you need to re-run migrations or reset the db, just run /// cargo clean -p nym-node-status-api #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { - let out_dir = read_env_var("OUT_DIR")?; - let database_path = format!("{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); - - write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; - let mut conn = SqliteConnection::connect(&database_path).await?; - sqlx::migrate!("./migrations").run(&mut conn).await?; - - #[cfg(target_family = "unix")] - println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path); - - #[cfg(target_family = "windows")] - // for some strange reason we need to add a leading `/` to the windows path even though it's - // not a valid windows path... but hey, it works... - println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path); - - Ok(()) -} - -fn read_env_var(var: &str) -> Result { - std::env::var(var).map_err(|_| anyhow!("You need to set {} env var", var)) -} - -/// use `./enter_db.sh` to inspect DB -async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { - let mut file = File::create("settings.sql").await?; - let settings = ".mode columns -.headers on"; - file.write_all(settings.as_bytes()).await?; - - let mut file = File::create("enter_db.sh").await?; - let contents = format!( - "#!/bin/bash\n\ - sqlite3 -init settings.sql {}/{}", - out_dir, db_filename, - ); - file.write_all(contents.as_bytes()).await?; - - #[cfg(target_family = "unix")] - file.set_permissions(Permissions::from_mode(0o755)) - .await - .map_err(anyhow::Error::from)?; + #[cfg(feature = "pg")] + if let Ok(database_url) = std::env::var("DATABASE_URL") { + println!("cargo::rustc-env=DATABASE_URL={database_url}"); + } Ok(()) } diff --git a/nym-node-status-api/nym-node-status-api/docker-compose.yml b/nym-node-status-api/nym-node-status-api/docker-compose.yml new file mode 100644 index 0000000000..12dbe51031 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/docker-compose.yml @@ -0,0 +1,21 @@ +services: + postgres-test: + image: postgres:16-alpine + container_name: nym_node_status_api_postgres_test + environment: + POSTGRES_DB: nym_node_status_api_test + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + ports: + - '5433:5432' # Map to 5433 to avoid conflicts with default PostgreSQL + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U testuser -d nym_node_status_api_test'] + interval: 5s + timeout: 5s + retries: 5 + # Optional: Add volume for persistent data during development + # volumes: + # - postgres_test_data:/var/lib/postgresql/data + +# volumes: +# postgres_test_data: \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh index bd21bee53d..b5409280e5 100755 --- a/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh @@ -23,7 +23,7 @@ function run_bare() { echo "RUST_LOG=${RUST_LOG}" # --conection-url is provided in build.rs - cargo run --package nym-node-status-api + cargo run --package nym-node-status-api --features pg --no-default-features } function run_docker() { diff --git a/nym-node-status-api/nym-node-status-api/migrations/007_date_fix.sql b/nym-node-status-api/nym-node-status-api/migrations/007_date_fix.sql new file mode 100644 index 0000000000..4db114e9af --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations/007_date_fix.sql @@ -0,0 +1,113 @@ +-- for a couple of days after migrating chrono -> time, we stored dates as +-- 2025-June-DD instead of 2025-06-DD. This migration fixes those entries. +-- +-- Because of a UNIQUE constraint on (node_id, date_utc), we can't just rename in-place. +-- - merge (add) node stats back to the original table where conflict (node_id, date_utc) would exist +-- - delete invalid records from original table (those stats were merged into correct rows above) +-- - insert rows that did not have a conflicting (node_Id, date_utc) combo +-- Conflicts affect only the date which has both kinds of entries, +-- e.g. 2025-06-05 and 2025-June-05 (date when this change was deployed) +-- +-- This applies to both affected tables. + +-- ---------------------------------------- +-- mixnode_daily_stats +-- ---------------------------------------- + +-- First, copy over rows with invalid date to a temp table (in the correct date format) +CREATE TEMP TABLE tmp_mix AS +SELECT + mix_id, + REPLACE(date_utc,'June','06') AS new_date, + SUM(total_stake) AS total_stake_sum, + SUM(packets_received) AS packets_received_sum, + SUM(packets_sent) AS packets_sent_sum, + SUM(packets_dropped) AS packets_dropped_sum +FROM mixnode_daily_stats +WHERE date_utc LIKE '%June%' +GROUP BY mix_id, new_date; + +UPDATE mixnode_daily_stats AS m +SET + total_stake = m.total_stake, + packets_received = m.packets_received + (SELECT packets_received_sum FROM tmp_mix WHERE mix_id = m.mix_id AND new_date = m.date_utc), + packets_sent = m.packets_sent + (SELECT packets_sent_sum FROM tmp_mix WHERE mix_id = m.mix_id AND new_date = m.date_utc), + packets_dropped = m.packets_dropped + (SELECT packets_dropped_sum FROM tmp_mix WHERE mix_id = m.mix_id AND new_date = m.date_utc) +WHERE EXISTS ( + SELECT 1 FROM tmp_mix + WHERE mix_id = m.mix_id + AND new_date = m.date_utc +); + +DELETE FROM mixnode_daily_stats + WHERE date_utc LIKE '%June%'; + +INSERT INTO mixnode_daily_stats + (mix_id, date_utc, total_stake, packets_received, packets_sent, packets_dropped) +SELECT + mix_id, + new_date, + total_stake_sum, + packets_received_sum, + packets_sent_sum, + packets_dropped_sum +FROM tmp_mix AS t +-- only those whose new_date did _not_ already exist +WHERE NOT EXISTS ( + SELECT 1 FROM mixnode_daily_stats AS m + WHERE m.mix_id = t.mix_id + AND m.date_utc = t.new_date +); + +DROP TABLE tmp_mix; + + +-- ---------------------------------------- +-- nym_node_daily_mixing_stats +-- ---------------------------------------- + +CREATE TEMP TABLE tmp_nym_node_stats AS +SELECT + node_id, + REPLACE(date_utc,'June','06') AS new_date, + SUM(total_stake) AS total_stake_sum, + SUM(packets_received) AS packets_received_sum, + SUM(packets_sent) AS packets_sent_sum, + SUM(packets_dropped) AS packets_dropped_sum +FROM nym_node_daily_mixing_stats +WHERE date_utc LIKE '%June%' +GROUP BY node_id, new_date; + +UPDATE nym_node_daily_mixing_stats AS m +SET + total_stake = m.total_stake, + packets_received = m.packets_received + (SELECT packets_received_sum FROM tmp_nym_node_stats WHERE node_id = m.node_id AND new_date = m.date_utc), + packets_sent = m.packets_sent + (SELECT packets_sent_sum FROM tmp_nym_node_stats WHERE node_id = m.node_id AND new_date = m.date_utc), + packets_dropped = m.packets_dropped + (SELECT packets_dropped_sum FROM tmp_nym_node_stats WHERE node_id = m.node_id AND new_date = m.date_utc) +WHERE EXISTS ( + SELECT 1 FROM tmp_nym_node_stats + WHERE node_id = m.node_id + AND new_date = m.date_utc +); + +DELETE FROM nym_node_daily_mixing_stats + WHERE date_utc LIKE '%June%'; + +INSERT INTO nym_node_daily_mixing_stats + (node_id, date_utc, total_stake, packets_received, packets_sent, packets_dropped) +SELECT + node_id, + new_date, + total_stake_sum, + packets_received_sum, + packets_sent_sum, + packets_dropped_sum +FROM tmp_nym_node_stats AS t +WHERE NOT EXISTS ( + SELECT 1 FROM nym_node_daily_mixing_stats AS m + WHERE m.node_id = t.node_id + AND m.date_utc = t.new_date +); + +DROP TABLE tmp_nym_node_stats; + diff --git a/nym-node-status-api/nym-node-status-api/migrations/008_performance_indexes.sql b/nym-node-status-api/nym-node-status-api/migrations/008_performance_indexes.sql new file mode 100644 index 0000000000..235b50e171 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations/008_performance_indexes.sql @@ -0,0 +1,16 @@ +-- Add partial indexes for NOT NULL filtering to improve performance of /explorer/v3/nodes endpoint + +-- Index for queries filtering on self_described IS NOT NULL +CREATE INDEX IF NOT EXISTS idx_nym_nodes_self_described_not_null +ON nym_nodes(node_id) +WHERE self_described IS NOT NULL; + +-- Index for queries filtering on bond_info IS NOT NULL +CREATE INDEX IF NOT EXISTS idx_nym_nodes_bond_info_not_null +ON nym_nodes(node_id) +WHERE bond_info IS NOT NULL; + +-- Composite index for queries filtering on both bond_info AND self_described +CREATE INDEX IF NOT EXISTS idx_nym_nodes_bond_self_described +ON nym_nodes(node_id) +WHERE bond_info IS NOT NULL AND self_described IS NOT NULL; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000000_init.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000000_init.sql new file mode 100644 index 0000000000..23f84e8667 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000000_init.sql @@ -0,0 +1,113 @@ +CREATE TABLE gateways +( + id SERIAL PRIMARY KEY, + gateway_identity_key VARCHAR NOT NULL UNIQUE, + self_described VARCHAR NOT NULL, + explorer_pretty_bond VARCHAR, + last_probe_result VARCHAR, + last_probe_log VARCHAR, + config_score INTEGER NOT NULL DEFAULT 0, + config_score_successes DOUBLE PRECISION NOT NULL DEFAULT 0, + config_score_samples DOUBLE PRECISION NOT NULL DEFAULT 0, + routing_score INTEGER NOT NULL DEFAULT 0, + routing_score_successes DOUBLE PRECISION NOT NULL DEFAULT 0, + routing_score_samples DOUBLE PRECISION NOT NULL DEFAULT 0, + test_run_samples DOUBLE PRECISION NOT NULL DEFAULT 0, + last_testrun_utc BIGINT, + last_updated_utc BIGINT NOT NULL, + bonded BOOLEAN NOT NULL DEFAULT FALSE, + blacklisted BOOLEAN NOT NULL DEFAULT FALSE, + performance INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_gateway_description_gateway_identity_key ON gateways (gateway_identity_key); + + +CREATE TABLE mixnodes ( + id SERIAL PRIMARY KEY, + identity_key VARCHAR NOT NULL UNIQUE, + mix_id BIGINT NOT NULL UNIQUE, + bonded BOOLEAN NOT NULL DEFAULT FALSE, + total_stake BIGINT NOT NULL, + host VARCHAR NOT NULL, + http_api_port BIGINT NOT NULL, + blacklisted BOOLEAN NOT NULL DEFAULT FALSE, + full_details VARCHAR, + self_described VARCHAR, + last_updated_utc BIGINT NOT NULL, + is_dp_delegatee BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id); +CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key); + +CREATE TABLE mixnode_description ( + id SERIAL PRIMARY KEY, + mix_id BIGINT UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc BIGINT NOT NULL, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id) +); + +-- Indexes for description table +CREATE INDEX idx_mixnode_description_mix_id ON mixnode_description (mix_id); + + +CREATE TABLE summary +( + key VARCHAR PRIMARY KEY, + value_json VARCHAR, + last_updated_utc BIGINT NOT NULL +); + + +CREATE TABLE summary_history +( + id SERIAL PRIMARY KEY, + date VARCHAR UNIQUE NOT NULL, + timestamp_utc BIGINT NOT NULL, + value_json VARCHAR +); + +CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc); +CREATE INDEX idx_summary_history_date ON summary_history (date); + + +CREATE TABLE gateway_description ( + id SERIAL PRIMARY KEY, + gateway_identity_key VARCHAR UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc BIGINT NOT NULL, + FOREIGN KEY (gateway_identity_key) REFERENCES gateways (gateway_identity_key) +); + + +CREATE TABLE mixnode_daily_stats ( + id SERIAL PRIMARY KEY, + mix_id BIGINT NOT NULL, + total_stake BIGINT NOT NULL, + date_utc VARCHAR NOT NULL, + packets_received INTEGER DEFAULT 0, + packets_sent INTEGER DEFAULT 0, + packets_dropped INTEGER DEFAULT 0, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), + UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index +); + + +CREATE TABLE testruns +( + id SERIAL PRIMARY KEY, + gateway_id INTEGER NOT NULL, + status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete + timestamp_utc BIGINT NOT NULL, + ip_address VARCHAR NOT NULL, + log VARCHAR NOT NULL, + FOREIGN KEY (gateway_id) REFERENCES gateways (id) +); \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000001_last_assigned_utc.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000001_last_assigned_utc.sql new file mode 100644 index 0000000000..f5929f0034 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000001_last_assigned_utc.sql @@ -0,0 +1,5 @@ +ALTER TABLE testruns +RENAME COLUMN timestamp_utc TO created_utc; + +ALTER TABLE testruns +ADD COLUMN last_assigned_utc BIGINT; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000002_session_stats.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000002_session_stats.sql new file mode 100644 index 0000000000..e2d8e8440c --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000002_session_stats.sql @@ -0,0 +1,16 @@ +CREATE TABLE gateway_session_stats ( + id SERIAL PRIMARY KEY, + gateway_identity_key VARCHAR NOT NULL, + node_id BIGINT NOT NULL, + day DATE NOT NULL, + unique_active_clients BIGINT NOT NULL, + session_started BIGINT NOT NULL, + users_hashes VARCHAR, + vpn_sessions VARCHAR, + mixnet_sessions VARCHAR, + unknown_sessions VARCHAR, + UNIQUE (node_id, day) -- This constraint automatically creates an index +); + +CREATE INDEX idx_gateway_session_stats_identity_key ON gateway_session_stats (gateway_identity_key); +CREATE INDEX idx_gateway_session_stats_day ON gateway_session_stats (day); \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000003_scraper_tables.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000003_scraper_tables.sql new file mode 100644 index 0000000000..ba591789d5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000003_scraper_tables.sql @@ -0,0 +1,11 @@ +CREATE TABLE mixnode_packet_stats_raw ( + id SERIAL PRIMARY KEY, + mix_id BIGINT NOT NULL, + timestamp_utc BIGINT NOT NULL, + packets_received INTEGER, + packets_sent INTEGER, + packets_dropped INTEGER, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id) +); + +CREATE INDEX idx_mixnode_packet_stats_raw_mix_id_timestamp_utc ON mixnode_packet_stats_raw (mix_id, timestamp_utc); \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000004_obsolete_fields.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000004_obsolete_fields.sql new file mode 100644 index 0000000000..a473ab1190 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000004_obsolete_fields.sql @@ -0,0 +1,54 @@ +ALTER TABLE mixnodes DROP COLUMN blacklisted; +ALTER TABLE gateways DROP COLUMN blacklisted; + +CREATE TABLE nym_nodes ( + node_id INTEGER PRIMARY KEY, + ed25519_identity_pubkey VARCHAR NOT NULL UNIQUE, + total_stake BIGINT NOT NULL, + ip_addresses TEXT NOT NULL, + mix_port INTEGER NOT NULL, + x25519_sphinx_pubkey VARCHAR NOT NULL UNIQUE, + node_role TEXT NOT NULL, + supported_roles TEXT NOT NULL, + performance VARCHAR NOT NULL, + entry TEXT, + last_updated_utc INTEGER NOT NULL +); + +CREATE INDEX idx_nym_nodes_node_id ON nym_nodes (node_id); +CREATE INDEX idx_nym_nodes_ed25519_identity_pubkey ON nym_nodes (ed25519_identity_pubkey); + +CREATE TABLE nym_node_descriptions ( + id SERIAL PRIMARY KEY, + node_id INTEGER UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id) +); + +CREATE TABLE nym_nodes_packet_stats_raw ( + id SERIAL PRIMARY KEY, + node_id INTEGER NOT NULL, + timestamp_utc INTEGER NOT NULL, + packets_received INTEGER, + packets_sent INTEGER, + packets_dropped INTEGER, + FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id) +); + +CREATE INDEX idx_nym_nodes_packet_stats_raw_node_id_timestamp_utc ON nym_nodes_packet_stats_raw (node_id, timestamp_utc); + +CREATE TABLE nym_node_daily_mixing_stats ( + id SERIAL PRIMARY KEY, + node_id INTEGER NOT NULL, + total_stake BIGINT NOT NULL, + date_utc VARCHAR NOT NULL, + packets_received INTEGER DEFAULT 0, + packets_sent INTEGER DEFAULT 0, + packets_dropped INTEGER DEFAULT 0, + FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id), + UNIQUE (node_id, date_utc) -- This constraint automatically creates an index +); \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000005_node_self_described.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000005_node_self_described.sql new file mode 100644 index 0000000000..8e7bc7ef10 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000005_node_self_described.sql @@ -0,0 +1,23 @@ +ALTER TABLE nym_nodes ADD COLUMN self_described TEXT; +ALTER TABLE nym_nodes ADD COLUMN bond_info TEXT; + +-- PostgreSQL doesn't need table recreation for adding ON DELETE CASCADE +-- We can drop and recreate the foreign key constraints directly + +-- Drop existing foreign key constraints +ALTER TABLE nym_node_descriptions DROP CONSTRAINT IF EXISTS nym_node_descriptions_node_id_fkey; +ALTER TABLE nym_nodes_packet_stats_raw DROP CONSTRAINT IF EXISTS nym_nodes_packet_stats_raw_node_id_fkey; +ALTER TABLE nym_node_daily_mixing_stats DROP CONSTRAINT IF EXISTS nym_node_daily_mixing_stats_node_id_fkey; + +-- Add foreign key constraints with ON DELETE CASCADE +ALTER TABLE nym_node_descriptions + ADD CONSTRAINT nym_node_descriptions_node_id_fkey + FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id) ON DELETE CASCADE; + +ALTER TABLE nym_nodes_packet_stats_raw + ADD CONSTRAINT nym_nodes_packet_stats_raw_node_id_fkey + FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id) ON DELETE CASCADE; + +ALTER TABLE nym_node_daily_mixing_stats + ADD CONSTRAINT nym_node_daily_mixing_stats_node_id_fkey + FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id) ON DELETE CASCADE; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000006_remove_unique_constraint.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000006_remove_unique_constraint.sql new file mode 100644 index 0000000000..98a7b75ca7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000006_remove_unique_constraint.sql @@ -0,0 +1,9 @@ +-- Removing UNIQUE constraints on nym_nodes +-- In PostgreSQL, we can drop constraints directly without recreating the table + +-- Drop the unique constraints +ALTER TABLE nym_nodes DROP CONSTRAINT IF EXISTS nym_nodes_ed25519_identity_pubkey_key; +ALTER TABLE nym_nodes DROP CONSTRAINT IF EXISTS nym_nodes_x25519_sphinx_pubkey_key; + +-- The columns and indexes remain, only the unique constraints are removed +-- The existing indexes idx_nym_nodes_node_id and idx_nym_nodes_ed25519_identity_pubkey remain unchanged \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000007_date_fix.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000007_date_fix.sql new file mode 100644 index 0000000000..64fb3064f7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000007_date_fix.sql @@ -0,0 +1,112 @@ +-- for a couple of days after migrating chrono -> time, we stored dates as +-- 2025-June-DD instead of 2025-06-DD. This migration fixes those entries. +-- +-- Because of a UNIQUE constraint on (node_id, date_utc), we can't just rename in-place. +-- - merge (add) node stats back to the original table where conflict (node_id, date_utc) would exist +-- - delete invalid records from original table (those stats were merged into correct rows above) +-- - insert rows that did not have a conflicting (node_Id, date_utc) combo +-- Conflicts affect only the date which has both kinds of entries, +-- e.g. 2025-06-05 and 2025-June-05 (date when this change was deployed) +-- +-- This applies to both affected tables. + +-- ---------------------------------------- +-- mixnode_daily_stats +-- ---------------------------------------- + +-- First, copy over rows with invalid date to a temp table (in the correct date format) +CREATE TEMP TABLE tmp_mix AS +SELECT + mix_id, + REPLACE(date_utc,'June','06') AS new_date, + SUM(total_stake) AS total_stake_sum, + SUM(packets_received) AS packets_received_sum, + SUM(packets_sent) AS packets_sent_sum, + SUM(packets_dropped) AS packets_dropped_sum +FROM mixnode_daily_stats +WHERE date_utc LIKE '%June%' +GROUP BY mix_id, new_date; + +UPDATE mixnode_daily_stats AS m +SET + total_stake = m.total_stake, + packets_received = m.packets_received + (SELECT packets_received_sum FROM tmp_mix WHERE mix_id = m.mix_id AND new_date = m.date_utc), + packets_sent = m.packets_sent + (SELECT packets_sent_sum FROM tmp_mix WHERE mix_id = m.mix_id AND new_date = m.date_utc), + packets_dropped = m.packets_dropped + (SELECT packets_dropped_sum FROM tmp_mix WHERE mix_id = m.mix_id AND new_date = m.date_utc) +WHERE EXISTS ( + SELECT 1 FROM tmp_mix + WHERE mix_id = m.mix_id + AND new_date = m.date_utc +); + +DELETE FROM mixnode_daily_stats + WHERE date_utc LIKE '%June%'; + +INSERT INTO mixnode_daily_stats + (mix_id, date_utc, total_stake, packets_received, packets_sent, packets_dropped) +SELECT + mix_id, + new_date, + total_stake_sum, + packets_received_sum, + packets_sent_sum, + packets_dropped_sum +FROM tmp_mix AS t +-- only those whose new_date did _not_ already exist +WHERE NOT EXISTS ( + SELECT 1 FROM mixnode_daily_stats AS m + WHERE m.mix_id = t.mix_id + AND m.date_utc = t.new_date +); + +DROP TABLE tmp_mix; + + +-- ---------------------------------------- +-- nym_node_daily_mixing_stats +-- ---------------------------------------- + +CREATE TEMP TABLE tmp_nym_node_stats AS +SELECT + node_id, + REPLACE(date_utc,'June','06') AS new_date, + SUM(total_stake) AS total_stake_sum, + SUM(packets_received) AS packets_received_sum, + SUM(packets_sent) AS packets_sent_sum, + SUM(packets_dropped) AS packets_dropped_sum +FROM nym_node_daily_mixing_stats +WHERE date_utc LIKE '%June%' +GROUP BY node_id, new_date; + +UPDATE nym_node_daily_mixing_stats AS m +SET + total_stake = m.total_stake, + packets_received = m.packets_received + (SELECT packets_received_sum FROM tmp_nym_node_stats WHERE node_id = m.node_id AND new_date = m.date_utc), + packets_sent = m.packets_sent + (SELECT packets_sent_sum FROM tmp_nym_node_stats WHERE node_id = m.node_id AND new_date = m.date_utc), + packets_dropped = m.packets_dropped + (SELECT packets_dropped_sum FROM tmp_nym_node_stats WHERE node_id = m.node_id AND new_date = m.date_utc) +WHERE EXISTS ( + SELECT 1 FROM tmp_nym_node_stats + WHERE node_id = m.node_id + AND new_date = m.date_utc +); + +DELETE FROM nym_node_daily_mixing_stats + WHERE date_utc LIKE '%June%'; + +INSERT INTO nym_node_daily_mixing_stats + (node_id, date_utc, total_stake, packets_received, packets_sent, packets_dropped) +SELECT + node_id, + new_date, + total_stake_sum, + packets_received_sum, + packets_sent_sum, + packets_dropped_sum +FROM tmp_nym_node_stats AS t +WHERE NOT EXISTS ( + SELECT 1 FROM nym_node_daily_mixing_stats AS m + WHERE m.node_id = t.node_id + AND m.date_utc = t.new_date +); + +DROP TABLE tmp_nym_node_stats; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000008_jsonb_columns.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000008_jsonb_columns.sql new file mode 100644 index 0000000000..4ebfec1595 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000008_jsonb_columns.sql @@ -0,0 +1,3 @@ +-- Convert ip_addresses column from TEXT to JSONB for better type safety +ALTER TABLE nym_nodes + ALTER COLUMN ip_addresses TYPE JSONB USING ip_addresses::JSONB; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000009_more_jsonb_columns.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000009_more_jsonb_columns.sql new file mode 100644 index 0000000000..1dcec55fea --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000009_more_jsonb_columns.sql @@ -0,0 +1,7 @@ +-- Convert remaining TEXT columns that store JSON to JSONB +ALTER TABLE nym_nodes + ALTER COLUMN node_role TYPE JSONB USING node_role::JSONB, + ALTER COLUMN supported_roles TYPE JSONB USING supported_roles::JSONB, + ALTER COLUMN entry TYPE JSONB USING entry::JSONB, + ALTER COLUMN self_described TYPE JSONB USING self_described::JSONB, + ALTER COLUMN bond_info TYPE JSONB USING bond_info::JSONB; \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000010_performance_indexes_postgres.sql b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000010_performance_indexes_postgres.sql new file mode 100644 index 0000000000..15bee4ac30 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations_pg/20240101000010_performance_indexes_postgres.sql @@ -0,0 +1,17 @@ +-- Add partial indexes for NOT NULL filtering to improve performance of /explorer/v3/nodes endpoint +-- PostgreSQL version + +-- Index for queries filtering on self_described IS NOT NULL +CREATE INDEX IF NOT EXISTS idx_nym_nodes_self_described_not_null +ON nym_nodes(node_id) +WHERE self_described IS NOT NULL; + +-- Index for queries filtering on bond_info IS NOT NULL +CREATE INDEX IF NOT EXISTS idx_nym_nodes_bond_info_not_null +ON nym_nodes(node_id) +WHERE bond_info IS NOT NULL; + +-- Composite index for queries filtering on both bond_info AND self_described +CREATE INDEX IF NOT EXISTS idx_nym_nodes_bond_self_described +ON nym_nodes(node_id) +WHERE bond_info IS NOT NULL AND self_described IS NOT NULL; diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index e9d76a1f75..d3cb0f9c15 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -16,10 +16,6 @@ pub(crate) struct Cli { #[clap(long, env = "NETWORK_NAME")] pub(crate) network_name: String, - /// Explorer api url. - #[clap(short, long, env = "EXPLORER_API")] - pub(crate) explorer_api: String, - /// Nym api url. #[clap(short, long, env = "NYM_API")] pub(crate) nym_api: String, @@ -42,19 +38,23 @@ pub(crate) struct Cli { /// Nym api client timeout. #[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")] - #[arg(value_parser = parse_duration)] + #[arg(value_parser = parse_duration_std)] pub(crate) nym_api_client_timeout: Duration, /// Connection url for the database. #[clap(long, env = "DATABASE_URL")] pub(crate) database_url: String, + #[clap(long, default_value = "5", env = "SQLX_BUSY_TIMEOUT_S")] + #[arg(value_parser = parse_duration_std)] + pub(crate) sqlx_busy_timeout_s: Duration, + #[clap( long, default_value = "300", env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL" )] - #[arg(value_parser = parse_duration)] + #[arg(value_parser = parse_duration_std)] pub(crate) monitor_refresh_interval: Duration, #[clap( @@ -62,17 +62,28 @@ pub(crate) struct Cli { default_value = "300", env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL" )] - #[arg(value_parser = parse_duration)] + #[arg(value_parser = parse_duration_std)] pub(crate) testruns_refresh_interval: Duration, #[clap(long, default_value = "86400", env = "NODE_STATUS_API_GEODATA_TTL")] - #[arg(value_parser = parse_duration)] + #[arg(value_parser = parse_duration_std)] pub(crate) geodata_ttl: Duration, #[clap(env = "NODE_STATUS_API_AGENT_KEY_LIST")] #[arg(value_delimiter = ',')] pub(crate) agent_key_list: Vec, + #[clap(long, default_value = "120s", env = "AGENT_REQUEST_FRESHNESS")] + #[arg(value_parser = parse_duration_humantime)] + pub(crate) agent_request_freshness: time::Duration, + + #[clap( + long, + default_value_t = 10, + env = "NYM_NODE_STATUS_API_PACKET_STATS_MAX_CONCURRENT_TASKS" + )] + pub(crate) packet_stats_max_concurrent_tasks: usize, + /// https://github.com/ipinfo/rust #[clap(long, env = "IPINFO_API_TOKEN")] pub(crate) ipinfo_api_token: String, @@ -85,7 +96,40 @@ pub(crate) struct Cli { pub(crate) max_agent_count: i64, } -fn parse_duration(arg: &str) -> Result { +fn parse_duration_humantime(arg: &str) -> Result { + let std_duration = match humantime::parse_duration(arg) { + Ok(duration) => duration, + // assume old format (seconds) as a fallback + Err(_) => parse_duration_std(arg)?, + }; + + Ok(time::Duration::seconds(std_duration.as_secs() as i64)) +} + +fn parse_duration_std(arg: &str) -> Result { let seconds = arg.parse()?; Ok(std::time::Duration::from_secs(seconds)) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn humantime_should_work() { + let should_parse = [("120s", 120), ("120", 120), ("0s", 0), ("0", 0)]; + + for (raw, expected) in should_parse { + if let Ok(parsed) = parse_duration_humantime(raw) { + assert_eq!(parsed.whole_seconds(), expected); + } else { + panic!("Failed to parse {raw}") + } + } + + let should_not_parse = ["0.1s", "-15s"]; + for raw in should_not_parse { + assert!(parse_duration_humantime(raw).is_err()); + } + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/mod.rs index cc3398964c..964193455e 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/mod.rs @@ -1,26 +1,56 @@ use anyhow::{anyhow, Result}; +use std::{str::FromStr, time::Duration}; + +pub(crate) mod models; +pub(crate) mod queries; +pub(crate) mod query_wrapper; + +#[cfg(test)] +mod tests; + +// Re-export the query wrapper functions for easier access +pub(crate) use query_wrapper::query; +#[allow(unused_imports)] +pub(crate) use query_wrapper::query_as; + +#[cfg(feature = "sqlite")] use sqlx::{ migrate::Migrator, sqlite::{SqliteAutoVacuum, SqliteConnectOptions, SqliteSynchronous}, ConnectOptions, SqlitePool, }; -use std::str::FromStr; -pub(crate) mod models; -pub(crate) mod queries; +#[cfg(feature = "pg")] +use sqlx::{migrate::Migrator, postgres::PgConnectOptions, ConnectOptions, PgPool}; +#[cfg(feature = "sqlite")] static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); +#[cfg(feature = "pg")] +static MIGRATOR: Migrator = sqlx::migrate!("./migrations_pg"); + +#[cfg(feature = "sqlite")] pub(crate) type DbPool = SqlitePool; +#[cfg(feature = "pg")] +pub(crate) type DbPool = PgPool; + +#[cfg(feature = "sqlite")] +pub(crate) type DbConnection = sqlx::pool::PoolConnection; + +#[cfg(feature = "pg")] +pub(crate) type DbConnection = sqlx::pool::PoolConnection; + pub(crate) struct Storage { pool: DbPool, } impl Storage { - pub async fn init(connection_url: String) -> Result { + #[cfg(feature = "sqlite")] + pub async fn init(connection_url: String, busy_timeout: Duration) -> Result { let connect_options = SqliteConnectOptions::from_str(&connection_url)? .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .busy_timeout(busy_timeout) .synchronous(SqliteSynchronous::Normal) .auto_vacuum(SqliteAutoVacuum::Incremental) .foreign_keys(true) @@ -33,6 +63,31 @@ impl Storage { MIGRATOR.run(&pool).await?; + // aftering setting pragma, check whether it was set successfully + Self::assert_busy_timeout(pool.clone(), busy_timeout.as_secs() as i64).await?; + + Ok(Storage { pool }) + } + + #[cfg(feature = "pg")] + pub async fn init(connection_url: String, _busy_timeout: Duration) -> Result { + use std::env; + let mut connect_options = + PgConnectOptions::from_str(&connection_url)?.disable_statement_logging(); + + let ssl_cert_path = env::var("PG_CERT").ok(); + + if let Some(ssl_cert) = ssl_cert_path { + connect_options = connect_options + .ssl_mode(sqlx::postgres::PgSslMode::Require) + .ssl_root_cert(ssl_cert); + } + let pool = sqlx::PgPool::connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + MIGRATOR.run(&pool).await?; + Ok(Storage { pool }) } @@ -40,4 +95,28 @@ impl Storage { pub fn pool_owned(&self) -> DbPool { self.pool.clone() } + + #[cfg(feature = "sqlite")] + async fn assert_busy_timeout(pool: DbPool, expected_busy_timeout_s: i64) -> Result<()> { + let mut conn = pool.acquire().await?; + // Sqlite stores this value as miliseconds + // https://www.sqlite.org/pragma.html#pragma_busy_timeout + let busy_timeout_db = sqlx::query!("PRAGMA busy_timeout;") + .fetch_one(conn.as_mut()) + .await?; + + let actual_busy_timeout_ms = busy_timeout_db.timeout.unwrap_or(0); + tracing::info!("PRAGMA busy_timeout={}ms", actual_busy_timeout_ms); + let expected_busy_timeout_ms = expected_busy_timeout_s * 1000; + + if expected_busy_timeout_ms != actual_busy_timeout_ms { + anyhow::bail!( + "PRAGMA busy_timeout expected: {}ms, actual: {}ms", + expected_busy_timeout_ms, + actual_busy_timeout_ms + ); + } + + Ok(()) + } } diff --git a/nym-node-status-api/nym-node-status-api/src/db/models.rs b/nym-node-status-api/nym-node-status-api/src/db/models.rs index 062399bca5..dbf98b2ac2 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/models.rs @@ -1,21 +1,23 @@ -use std::str::FromStr; - use crate::{ http::{self, models::SummaryHistory}, - utils::{decimal_to_i64, NumericalCheckedCast}, + utils::{decimal_to_i64, unix_timestamp_to_utc_rfc3339, NumericalCheckedCast}, }; use anyhow::Context; use nym_contracts_common::Percent; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_node_requests::api::v1::node::models::NodeDescription; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeDescription}; use nym_validator_client::{ client::NymNodeDetails, models::NymNodeDescription, nym_api::SkimmedNode, }; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use std::net::IpAddr; +use std::str::FromStr; use strum_macros::{EnumString, FromRepr}; -use time::{Date, OffsetDateTime}; +use time::{Date, OffsetDateTime, UtcDateTime}; use utoipa::ToSchema; macro_rules! serialize_opt_to_value { @@ -39,11 +41,11 @@ pub(crate) struct GatewayInsertRecord { pub(crate) performance: u8, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromRow)] pub(crate) struct GatewayDto { pub(crate) gateway_identity_key: String, pub(crate) bonded: bool, - pub(crate) performance: i64, + pub(crate) performance: i32, pub(crate) self_described: Option, pub(crate) explorer_pretty_bond: Option, pub(crate) last_probe_result: Option, @@ -64,12 +66,8 @@ impl TryFrom for http::models::Gateway { // number of successful testruns in the last 24h. let routing_score = 0f32; let config_score = 0u32; - let last_updated_utc = - timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); - let last_testrun_utc = value - .last_testrun_utc - .and_then(|i| i.cast_checked().ok()) - .map(|t| timestamp_as_utc(t).to_rfc3339()); + let last_updated_utc = unix_timestamp_to_utc_rfc3339(value.last_updated_utc); + let last_testrun_utc = value.last_testrun_utc.map(unix_timestamp_to_utc_rfc3339); let self_described = value.self_described.clone().unwrap_or("null".to_string()); let explorer_pretty_bond = value @@ -113,11 +111,6 @@ impl TryFrom for http::models::Gateway { } } -fn timestamp_as_utc(unix_timestamp: u64) -> chrono::DateTime { - let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); - d.into() -} - pub(crate) struct MixnodeRecord { pub(crate) mix_id: u32, pub(crate) identity_key: String, @@ -131,7 +124,7 @@ pub(crate) struct MixnodeRecord { pub(crate) is_dp_delegatee: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromRow)] pub(crate) struct MixnodeDto { pub(crate) mix_id: i64, pub(crate) bonded: bool, @@ -159,8 +152,7 @@ impl TryFrom for http::models::Mixnode { .clone() .map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null)); - let last_updated_utc = - timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let last_updated_utc = unix_timestamp_to_utc_rfc3339(value.last_updated_utc); let is_dp_delegatee = value.is_dp_delegatee; let moniker = value.moniker.clone(); let website = value.website.clone(); @@ -194,14 +186,14 @@ pub(crate) struct BondedStatusDto { } #[allow(unused)] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, FromRow)] pub(crate) struct SummaryDto { pub(crate) key: String, pub(crate) value_json: String, pub(crate) last_updated_utc: i64, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, FromRow)] pub(crate) struct SummaryHistoryDto { #[allow(dead_code)] pub id: i64, @@ -218,7 +210,7 @@ impl TryFrom for SummaryHistory { Ok(SummaryHistory { value_json, date: value.date.clone(), - timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(), + timestamp_utc: unix_timestamp_to_utc_rfc3339(value.timestamp_utc), }) } } @@ -240,6 +232,13 @@ pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; // have to import it use gateway::GatewaySummary; use mixnode::MixnodeSummary; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_mixnet_contract_common::NodeId; +use nym_validator_client::models::{ + AuthenticatorDetails, DeclaredRoles, DescribedNodeType, HostInformation, HostKeys, + IpPacketRouterDetails, NetworkRequesterDetails, NymNodeData, OffsetDateTimeJsonSchemaWrapper, + SphinxKey, VersionedNoiseKey, WebSockets, WireguardDetails, +}; #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct NetworkSummary { @@ -298,11 +297,11 @@ pub(crate) mod gateway { } #[allow(dead_code)] // not dead code, this is SQL data model -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromRow)] pub struct TestRunDto { - pub id: i64, - pub gateway_id: i64, - pub status: i64, + pub id: i32, + pub gateway_id: i32, + pub status: i32, pub created_utc: i64, pub ip_address: String, pub log: String, @@ -324,9 +323,9 @@ pub struct GatewayIdentityDto { } #[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros -#[derive(Debug, Clone)] +#[derive(Debug, Clone, FromRow)] pub struct GatewayInfoDto { - pub id: i64, + pub id: i32, pub gateway_identity_key: String, pub self_described: Option, pub explorer_pretty_bond: Option, @@ -373,7 +372,7 @@ impl TryFrom for http::models::SessionStats { } } -#[derive(strum_macros::Display)] +#[derive(strum_macros::Display, Clone)] pub(crate) enum ScrapeNodeKind { LegacyMixnode { mix_id: i64 }, MixingNymNode { node_id: i64 }, @@ -390,6 +389,7 @@ impl ScrapeNodeKind { } } +#[derive(Clone)] pub(crate) struct ScraperNodeInfo { pub node_kind: ScrapeNodeKind, pub hosts: Vec, @@ -421,13 +421,13 @@ impl ScraperNodeInfo { } #[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros -#[derive(sqlx::Decode, Debug)] +#[derive(FromRow, Debug)] pub(crate) struct NymNodeDto { - pub node_id: i64, + pub node_id: i32, pub ed25519_identity_pubkey: String, pub total_stake: i64, pub ip_addresses: serde_json::Value, - pub mix_port: i64, + pub mix_port: i32, pub x25519_sphinx_pubkey: String, pub node_role: serde_json::Value, pub supported_roles: serde_json::Value, @@ -440,11 +440,11 @@ pub(crate) struct NymNodeDto { #[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros #[derive(Debug)] pub(crate) struct NymNodeInsertRecord { - pub node_id: i64, + pub node_id: i32, pub ed25519_identity_pubkey: String, pub total_stake: i64, pub ip_addresses: serde_json::Value, - pub mix_port: i64, + pub mix_port: i32, pub x25519_sphinx_pubkey: String, pub node_role: serde_json::Value, pub supported_roles: serde_json::Value, @@ -452,7 +452,7 @@ pub(crate) struct NymNodeInsertRecord { pub entry: Option, pub self_described: Option, pub bond_info: Option, - pub last_updated_utc: String, + pub last_updated_utc: i64, } impl NymNodeInsertRecord { @@ -461,7 +461,7 @@ impl NymNodeInsertRecord { bond_info: Option<&NymNodeDetails>, self_described: Option<&NymNodeDescription>, ) -> anyhow::Result { - let now = OffsetDateTime::now_utc().to_string(); + let now = OffsetDateTime::now_utc().unix_timestamp(); // if bond info is missing, set stake to 0 let total_stake = bond_info @@ -472,11 +472,11 @@ impl NymNodeInsertRecord { let self_described = serialize_opt_to_value!(self_described)?; let record = Self { - node_id: skimmed_node.node_id.into(), + node_id: skimmed_node.node_id as i32, ed25519_identity_pubkey: skimmed_node.ed25519_identity_pubkey.to_base58_string(), total_stake, ip_addresses: serde_json::to_value(&skimmed_node.ip_addresses)?, - mix_port: skimmed_node.mix_port as i64, + mix_port: skimmed_node.mix_port as i32, x25519_sphinx_pubkey: skimmed_node.x25519_sphinx_pubkey.to_base58_string(), node_role: serde_json::to_value(&skimmed_node.role)?, supported_roles: serde_json::to_value(skimmed_node.supported_roles)?, @@ -525,9 +525,132 @@ impl TryFrom for SkimmedNode { } } -#[derive(Debug, Serialize, Deserialize, sqlx::Decode)] +#[derive(Debug, Serialize, Deserialize, sqlx::Decode, FromRow)] pub struct NodeStats { - pub packets_received: i64, - pub packets_sent: i64, - pub packets_dropped: i64, + pub packets_received: i32, + pub packets_sent: i32, + pub packets_dropped: i32, +} + +pub struct InsertStatsRecord { + pub node_kind: ScrapeNodeKind, + pub timestamp_utc: UtcDateTime, + pub unix_timestamp: i64, + pub stats: NodeStats, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct NymNodeDescriptionDeHelper { + pub node_id: NodeId, + pub contract_node_type: DescribedNodeType, + pub description: NymNodeDataDeHelper, +} + +#[allow(deprecated)] +impl From for NymNodeDescription { + fn from(helper: NymNodeDescriptionDeHelper) -> Self { + let current_x25519_sphinx_key = helper + .description + .host_information + .keys + .current_x25519_sphinx_key + .unwrap_or(SphinxKey { + // indicate the legacy case + rotation_id: u32::MAX, + public_key: helper.description.host_information.keys.x25519, + }); + + NymNodeDescription { + node_id: helper.node_id, + contract_node_type: helper.contract_node_type, + description: NymNodeData { + last_polled: helper.description.last_polled, + host_information: HostInformation { + ip_address: helper.description.host_information.ip_address, + hostname: helper.description.host_information.hostname, + keys: HostKeys { + ed25519: helper.description.host_information.keys.ed25519, + x25519: helper.description.host_information.keys.x25519, + current_x25519_sphinx_key, + pre_announced_x25519_sphinx_key: helper + .description + .host_information + .keys + .pre_announced_x25519_sphinx_key, + x25519_versioned_noise: helper + .description + .host_information + .keys + .x25519_versioned_noise, + }, + }, + declared_role: helper.description.declared_role, + auxiliary_details: helper.description.auxiliary_details, + build_information: helper.description.build_information, + network_requester: helper.description.network_requester, + ip_packet_router: helper.description.ip_packet_router, + authenticator: helper.description.authenticator, + wireguard: helper.description.wireguard, + mixnet_websockets: helper.description.mixnet_websockets, + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct NymNodeDataDeHelper { + #[serde(default)] + pub last_polled: OffsetDateTimeJsonSchemaWrapper, + + pub host_information: HostInformationDeHelper, + + #[serde(default)] + pub declared_role: DeclaredRoles, + + #[serde(default)] + pub auxiliary_details: AuxiliaryDetails, + + // TODO: do we really care about ALL build info or just the version? + pub build_information: BinaryBuildInformationOwned, + + #[serde(default)] + pub network_requester: Option, + + #[serde(default)] + pub ip_packet_router: Option, + + #[serde(default)] + pub authenticator: Option, + + #[serde(default)] + pub wireguard: Option, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSockets, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct HostInformationDeHelper { + pub ip_address: Vec, + pub hostname: Option, + pub keys: HostKeysDeHelper, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct HostKeysDeHelper { + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519: ed25519::PublicKey, + + #[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25519: x25519::PublicKey, + + // legacy data will NOT have this information + pub current_x25519_sphinx_key: Option, + + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option, + + #[serde(default)] + pub x25519_versioned_noise: Option, } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs index 92e9d7d849..4b0dc2c0f9 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways.rs @@ -3,31 +3,32 @@ use std::collections::HashSet; use crate::{ db::{ models::{GatewayDto, GatewayInsertRecord}, - DbPool, + DbConnection, DbPool, }, http::models::Gateway, + node_scraper::helpers::NodeDescriptionResponse, }; use futures_util::TryStreamExt; -use sqlx::{pool::PoolConnection, Sqlite}; +use sqlx::Row; use tracing::error; pub(crate) async fn select_gateway_identity( - conn: &mut PoolConnection, - gateway_pk: i64, + conn: &mut DbConnection, + gateway_pk: i32, ) -> anyhow::Result { - let record = sqlx::query!( + let record = crate::db::query( r#"SELECT gateway_identity_key FROM gateways WHERE id = ?"#, - gateway_pk ) + .bind(gateway_pk) .fetch_one(conn.as_mut()) .await?; - Ok(record.gateway_identity_key) + Ok(record.try_get("gateway_identity_key")?) } pub(crate) async fn update_bonded_gateways( @@ -36,7 +37,7 @@ pub(crate) async fn update_bonded_gateways( ) -> anyhow::Result<()> { let mut tx = pool.begin().await?; - sqlx::query!( + crate::db::query( r#"UPDATE gateways SET @@ -47,7 +48,7 @@ pub(crate) async fn update_bonded_gateways( .await?; for record in gateways { - sqlx::query!( + crate::db::query( "INSERT INTO gateways (gateway_identity_key, bonded, self_described, explorer_pretty_bond, @@ -59,13 +60,13 @@ pub(crate) async fn update_bonded_gateways( explorer_pretty_bond=excluded.explorer_pretty_bond, last_updated_utc=excluded.last_updated_utc, performance = excluded.performance;", - record.identity_key, - record.bonded, - record.self_described, - record.explorer_pretty_bond, - record.last_updated_utc, - record.performance ) + .bind(record.identity_key) + .bind(record.bonded) + .bind(record.self_described) + .bind(record.explorer_pretty_bond) + .bind(record.last_updated_utc) + .bind(record.performance as i32) .execute(&mut *tx) .await?; } @@ -77,22 +78,21 @@ pub(crate) async fn update_bonded_gateways( pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result> { let mut conn = pool.acquire().await?; - let items = sqlx::query_as!( - GatewayDto, + let items = crate::db::query_as::( r#"SELECT - gw.gateway_identity_key as "gateway_identity_key!", - gw.bonded as "bonded: bool", - gw.performance as "performance!", - gw.self_described as "self_described?", - gw.explorer_pretty_bond as "explorer_pretty_bond?", - gw.last_probe_result as "last_probe_result?", - gw.last_probe_log as "last_probe_log?", - gw.last_testrun_utc as "last_testrun_utc?", - gw.last_updated_utc as "last_updated_utc!", - COALESCE(gd.moniker, "NA") as "moniker!", - COALESCE(gd.website, "NA") as "website!", - COALESCE(gd.security_contact, "NA") as "security_contact!", - COALESCE(gd.details, "NA") as "details!" + gw.gateway_identity_key, + gw.bonded, + gw.performance, + gw.self_described, + gw.explorer_pretty_bond, + gw.last_probe_result, + gw.last_probe_log, + gw.last_testrun_utc, + gw.last_updated_utc, + COALESCE(gd.moniker, 'NA') as moniker, + COALESCE(gd.website, 'NA') as website, + COALESCE(gd.security_contact, 'NA') as security_contact, + COALESCE(gd.details, 'NA') as details FROM gateways gw LEFT JOIN gateway_description gd ON gw.gateway_identity_key = gd.gateway_identity_key @@ -106,28 +106,100 @@ pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result>>() - .map_err(|e| { - error!("Conversion from DTO failed: {e}. Invalidly stored data?"); - e - })?; + .inspect_err(|e| error!("Conversion from DTO failed: {e}. Invalidly stored data?"))?; tracing::trace!("Fetched {} gateways from DB", items.len()); Ok(items) } pub(crate) async fn get_bonded_gateway_id_keys(pool: &DbPool) -> anyhow::Result> { let mut conn = pool.acquire().await?; - let items = sqlx::query!( + let items = crate::db::query( r#" SELECT gateway_identity_key FROM gateways WHERE bonded = true - "# + "#, ) .fetch_all(&mut *conn) .await? .into_iter() - .map(|record| record.gateway_identity_key) + .map(|record| record.try_get::("gateway_identity_key").unwrap()) .collect::>(); Ok(items) } + +pub(crate) async fn insert_gateway_description( + conn: &mut DbConnection, + identity_key: String, + description: NodeDescriptionResponse, + timestamp: i64, +) -> anyhow::Result<()> { + crate::db::query( + r#" + INSERT INTO gateway_description ( + gateway_identity_key, + moniker, + website, + security_contact, + details, + last_updated_utc + ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (gateway_identity_key) DO UPDATE SET + moniker = excluded.moniker, + website = excluded.website, + security_contact = excluded.security_contact, + details = excluded.details, + last_updated_utc = excluded.last_updated_utc + "#, + ) + .bind(identity_key) + .bind(description.moniker) + .bind(description.website) + .bind(description.security_contact) + .bind(description.details) + .bind(timestamp) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn get_or_create_gateway( + conn: &mut DbConnection, + gateway_identity_key: &str, +) -> anyhow::Result { + // Try to find existing gateway + let existing = crate::db::query("SELECT id FROM gateways WHERE gateway_identity_key = ?") + .bind(gateway_identity_key.to_string()) + .fetch_optional(conn.as_mut()) + .await?; + + if let Some(row) = existing { + return Ok(row.try_get("id")?); + } + + // Create new gateway + tracing::info!("Creating new gateway record for {}", gateway_identity_key); + let now = crate::utils::now_utc().unix_timestamp(); + + let result = crate::db::query( + r#"INSERT INTO gateways ( + gateway_identity_key, + bonded, + performance, + self_described, + last_updated_utc + ) VALUES (?, ?, ?, ?, ?) + RETURNING id"#, + ) + .bind(gateway_identity_key.to_string()) + .bind(true) // Assume bonded since being tested + .bind(0) // Initial performance + .bind("null") + .bind(now) + .fetch_one(conn.as_mut()) + .await?; + + Ok(result.try_get("id")?) +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs index 8ba813ee1f..bf90bff06b 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs @@ -6,6 +6,7 @@ use futures_util::TryStreamExt; use time::Date; use tracing::error; +#[cfg(feature = "sqlite")] pub(crate) async fn insert_session_records( pool: &DbPool, records: Vec, @@ -36,6 +37,38 @@ pub(crate) async fn insert_session_records( Ok(()) } +#[cfg(feature = "pg")] +pub(crate) async fn insert_session_records( + pool: &DbPool, + records: Vec, +) -> anyhow::Result<()> { + let mut tx = pool.begin().await?; + for record in records { + sqlx::query!( + "INSERT INTO gateway_session_stats + (gateway_identity_key, node_id, day, + unique_active_clients, session_started, users_hashes, + vpn_sessions, mixnet_sessions, unknown_sessions) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT DO NOTHING", + record.gateway_identity_key, + record.node_id, + record.day, + record.unique_active_clients, + record.session_started, + record.users_hashes, + record.vpn_sessions, + record.mixnet_sessions, + record.unknown_sessions, + ) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + + Ok(()) +} + pub(crate) async fn get_sessions_stats(pool: &DbPool) -> anyhow::Result> { let mut conn = pool.acquire().await?; let items = sqlx::query_as( @@ -68,7 +101,8 @@ pub(crate) async fn get_sessions_stats(pool: &DbPool) -> anyhow::Result anyhow::Result<()> { let mut conn = pool.acquire().await?; - sqlx::query!("DELETE FROM gateway_session_stats WHERE day <= ?", cut_off) + crate::db::query("DELETE FROM gateway_session_stats WHERE day <= ?") + .bind(cut_off) .execute(&mut *conn) .await?; Ok(()) diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/misc.rs index 4243c9d2e7..248b556c1d 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/misc.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/misc.rs @@ -1,13 +1,14 @@ +use time::UtcDateTime; + use crate::db::{models::NetworkSummary, DbPool}; -use chrono::{DateTime, Utc}; /// take `last_updated` instead of calculating it so that `summary` matches /// `daily_summary` pub(crate) async fn insert_summaries( pool: &DbPool, - summaries: &[(&str, usize)], - summary: &NetworkSummary, - last_updated: DateTime, + summaries: Vec<(String, usize)>, + summary: NetworkSummary, + last_updated: UtcDateTime, ) -> anyhow::Result<()> { insert_summary(pool, summaries, last_updated).await?; @@ -18,25 +19,25 @@ pub(crate) async fn insert_summaries( async fn insert_summary( pool: &DbPool, - summaries: &[(&str, usize)], - last_updated: DateTime, + summaries: Vec<(String, usize)>, + last_updated: UtcDateTime, ) -> anyhow::Result<()> { - let timestamp = last_updated.timestamp(); + let timestamp = last_updated.unix_timestamp(); let mut tx = pool.begin().await?; for (kind, value) in summaries { let value = value.to_string(); - sqlx::query!( + crate::db::query( "INSERT INTO summary (key, value_json, last_updated_utc) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value_json=excluded.value_json, last_updated_utc=excluded.last_updated_utc;", - kind, - value, - timestamp ) + .bind(kind.clone()) + .bind(value) + .bind(timestamp) .execute(&mut *tx) .await .map_err(|err| { @@ -59,30 +60,50 @@ async fn insert_summary( /// This is not aggregate data, it's a set of latest data points async fn insert_summary_history( pool: &DbPool, - summary: &NetworkSummary, - last_updated: DateTime, + summary: NetworkSummary, + last_updated: UtcDateTime, ) -> anyhow::Result<()> { let mut conn = pool.acquire().await?; let value_json = serde_json::to_string(&summary)?; - let timestamp = last_updated.timestamp(); - let now_rfc3339 = last_updated.to_rfc3339(); - // YYYY-MM-DD, without time - let date = &now_rfc3339[..10]; + let timestamp = last_updated.unix_timestamp(); - sqlx::query!( + let date = datetime_to_only_date_str(last_updated); + + crate::db::query( "INSERT INTO summary_history (date, timestamp_utc, value_json) VALUES (?, ?, ?) ON CONFLICT(date) DO UPDATE SET timestamp_utc=excluded.timestamp_utc, value_json=excluded.value_json;", - date, - timestamp, - value_json ) + .bind(date) + .bind(timestamp) + .bind(value_json) .execute(&mut *conn) .await?; Ok(()) } + +/// YYYY-MM-DD, without time +fn datetime_to_only_date_str(datetime: UtcDateTime) -> String { + datetime.date().to_string() +} + +#[cfg(test)] +mod test { + use time::macros::utc_datetime; + + use super::*; + + #[test] + fn date_is_expected_format() { + // to store records daily, we rely on a date in a specific format + // if the behaviour changes, you should adjust code to return the expected form + let example_date = utc_datetime!(2025-05-01 01:23); + let expected = "2025-05-01"; + assert_eq!(expected, datetime_to_only_date_str(example_date)); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs index 0b5f6e8abd..eeb0a4e592 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs @@ -1,14 +1,16 @@ use std::collections::HashSet; use futures_util::TryStreamExt; +use sqlx::Row; use tracing::error; use crate::{ db::{ models::{MixnodeDto, MixnodeRecord}, - DbPool, + DbConnection, DbPool, }, http::models::{DailyStats, Mixnode}, + node_scraper::helpers::NodeDescriptionResponse, }; pub(crate) async fn update_mixnodes( @@ -18,7 +20,7 @@ pub(crate) async fn update_mixnodes( let mut tx = pool.begin().await?; // mark all as unbonded - sqlx::query!( + crate::db::query( r#"UPDATE mixnodes SET @@ -29,9 +31,9 @@ pub(crate) async fn update_mixnodes( .await?; // existing nodes get updated on insert - for record in mixnodes.iter() { + for record in mixnodes.into_iter() { // https://www.sqlite.org/lang_upsert.html - sqlx::query!( + crate::db::query( "INSERT INTO mixnodes (mix_id, identity_key, bonded, total_stake, host, http_api_port, full_details, @@ -44,17 +46,17 @@ pub(crate) async fn update_mixnodes( full_details=excluded.full_details,self_described=excluded.self_described, last_updated_utc=excluded.last_updated_utc, is_dp_delegatee = excluded.is_dp_delegatee;", - record.mix_id, - record.identity_key, - record.bonded, - record.total_stake, - record.host, - record.http_port, - record.full_details, - record.self_described, - record.last_updated_utc, - record.is_dp_delegatee ) + .bind(record.mix_id as i64) + .bind(record.identity_key) + .bind(record.bonded) + .bind(record.total_stake) + .bind(record.host) + .bind(record.http_port as i32) + .bind(record.full_details) + .bind(record.self_described) + .bind(record.last_updated_utc) + .bind(record.is_dp_delegatee) .execute(&mut *tx) .await?; } @@ -76,10 +78,10 @@ pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result> { let mut conn = pool.acquire().await?; - let items = sqlx::query!( + let items = crate::db::query( r#" SELECT mix_id FROM mixnodes WHERE bonded = true - "# + "#, ) .fetch_all(&mut *conn) .await? .into_iter() - .map(|record| record.mix_id) + .map(|record| record.try_get::("mix_id").unwrap()) .collect::>(); Ok(items) } + +pub(crate) async fn insert_mixnode_description( + conn: &mut DbConnection, + mix_id: i64, + description: NodeDescriptionResponse, + timestamp: i64, +) -> anyhow::Result<()> { + crate::db::query( + r#" + INSERT INTO mixnode_description ( + mix_id, moniker, website, security_contact, details, last_updated_utc + ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (mix_id) DO UPDATE SET + moniker = excluded.moniker, + website = excluded.website, + security_contact = excluded.security_contact, + details = excluded.details, + last_updated_utc = excluded.last_updated_utc + "#, + ) + .bind(mix_id) + .bind(description.moniker) + .bind(description.website) + .bind(description.security_contact) + .bind(description.details) + .bind(timestamp) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs index ad57bd467b..012d32ae24 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs @@ -9,7 +9,8 @@ mod summary; pub(crate) mod testruns; pub(crate) use gateways::{ - get_all_gateways, get_bonded_gateway_id_keys, select_gateway_identity, update_bonded_gateways, + get_all_gateways, get_bonded_gateway_id_keys, get_or_create_gateway, select_gateway_identity, + update_bonded_gateways, }; pub(crate) use gateways_stats::{delete_old_records, get_sessions_stats, insert_session_records}; pub(crate) use misc::insert_summaries; @@ -19,7 +20,7 @@ pub(crate) use nym_nodes::{ get_described_node_bond_info, get_node_self_description, update_nym_nodes, }; pub(crate) use packet_stats::{ - get_raw_node_stats, insert_daily_node_stats, insert_node_packet_stats, + batch_store_packet_stats, get_raw_node_stats, insert_daily_node_stats_uncommitted, }; pub(crate) use scraper::{get_nodes_for_scraping, insert_scraped_node_description}; pub(crate) use summary::{get_summary, get_summary_history}; diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs index d7d85c28f9..93cca9e429 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs @@ -4,34 +4,40 @@ use nym_validator_client::{ client::{NodeId, NymNodeDetails}, models::NymNodeDescription, }; +use sqlx::Row; use std::collections::HashMap; -use tracing::instrument; +use tracing::{instrument, warn}; -use crate::db::{ - models::{NymNodeDto, NymNodeInsertRecord}, - DbPool, +use crate::db::models::NymNodeDescriptionDeHelper; +use crate::{ + db::{ + models::{NymNodeDto, NymNodeInsertRecord}, + DbConnection, DbPool, + }, + node_scraper::helpers::NodeDescriptionResponse, }; pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result> { let mut conn = pool.acquire().await?; - sqlx::query_as!( - NymNodeDto, + crate::db::query_as::( r#"SELECT node_id, ed25519_identity_pubkey, total_stake, - ip_addresses as "ip_addresses!: serde_json::Value", + ip_addresses, mix_port, x25519_sphinx_pubkey, - node_role as "node_role: serde_json::Value", - supported_roles as "supported_roles: serde_json::Value", - entry as "entry: serde_json::Value", + node_role, + supported_roles, + entry, performance, - self_described as "self_described: serde_json::Value", - bond_info as "bond_info: serde_json::Value" + self_described, + bond_info FROM nym_nodes + ORDER BY + node_id "#, ) .fetch(&mut *conn) @@ -50,21 +56,20 @@ pub(crate) async fn get_described_bonded_nym_nodes( ) -> anyhow::Result> { let mut conn = pool.acquire().await?; - sqlx::query_as!( - NymNodeDto, + crate::db::query_as::( r#"SELECT node_id, ed25519_identity_pubkey, total_stake, - ip_addresses as "ip_addresses!: serde_json::Value", + ip_addresses, mix_port, x25519_sphinx_pubkey, - node_role as "node_role: serde_json::Value", - supported_roles as "supported_roles: serde_json::Value", - entry as "entry: serde_json::Value", + node_role, + supported_roles, + entry, performance, - self_described as "self_described: serde_json::Value", - bond_info as "bond_info: serde_json::Value" + self_described, + bond_info FROM nym_nodes WHERE @@ -86,7 +91,7 @@ pub(crate) async fn update_nym_nodes( ) -> anyhow::Result { let mut tx = pool.begin().await?; - sqlx::query!( + crate::db::query( "UPDATE nym_nodes SET self_described = NULL, @@ -98,7 +103,7 @@ pub(crate) async fn update_nym_nodes( let inserted = node_records.len(); for record in node_records { // https://www.sqlite.org/lang_upsert.html - sqlx::query!( + crate::db::query( "INSERT INTO nym_nodes (node_id, ed25519_identity_pubkey, total_stake, @@ -123,20 +128,20 @@ pub(crate) async fn update_nym_nodes( performance=excluded.performance, last_updated_utc=excluded.last_updated_utc ;", - record.node_id, - record.ed25519_identity_pubkey, - record.total_stake, - record.ip_addresses, - record.mix_port, - record.x25519_sphinx_pubkey, - record.node_role, - record.supported_roles, - record.entry, - record.self_described, - record.bond_info, - record.performance, - record.last_updated_utc, ) + .bind(record.node_id) + .bind(record.ed25519_identity_pubkey) + .bind(record.total_stake) + .bind(record.ip_addresses) + .bind(record.mix_port) + .bind(record.x25519_sphinx_pubkey) + .bind(record.node_role) + .bind(record.supported_roles) + .bind(record.entry) + .bind(record.self_described) + .bind(record.bond_info) + .bind(record.performance) + .bind(record.last_updated_utc) .execute(&mut *tx) .await .map_err(|e| anyhow::anyhow!("Failed to INSERT node_id={}: {}", record.node_id, e))?; @@ -144,6 +149,10 @@ pub(crate) async fn update_nym_nodes( tx.commit().await?; + tracing::debug!( + "Successfully inserted/updated {} nym_nodes records", + inserted + ); Ok(inserted) } @@ -152,10 +161,10 @@ pub(crate) async fn get_described_node_bond_info( ) -> anyhow::Result> { let mut conn = pool.acquire().await?; - sqlx::query!( + crate::db::query( r#"SELECT node_id, - bond_info as "bond_info: serde_json::Value" + bond_info FROM nym_nodes WHERE @@ -170,11 +179,11 @@ pub(crate) async fn get_described_node_bond_info( records .into_iter() .filter_map(|record| { - record - .bond_info - // only return details for nodes which have details stored - .and_then(|bond_info| serde_json::from_value::(bond_info).ok()) - .map(|res| (record.node_id as NodeId, res)) + let node_id: i32 = record.try_get("node_id").ok()?; + let bond_info: serde_json::Value = record.try_get("bond_info").ok()?; + serde_json::from_value::(bond_info) + .ok() + .map(|res| (node_id as i64 as NodeId, res)) }) .collect::>() }) @@ -186,14 +195,16 @@ pub(crate) async fn get_node_self_description( ) -> anyhow::Result> { let mut conn = pool.acquire().await?; - sqlx::query!( + crate::db::query( r#"SELECT node_id, - self_described as "self_described: serde_json::Value" + self_described FROM nym_nodes WHERE self_described IS NOT NULL + ORDER BY + node_id "#, ) .fetch_all(&mut *conn) @@ -202,13 +213,14 @@ pub(crate) async fn get_node_self_description( records .into_iter() .filter_map(|record| { - record - .self_described - // only return details for nodes which have details stored - .and_then(|description| { - serde_json::from_value::(description).ok() - }) - .map(|res| (record.node_id as NodeId, res)) + let node_id: i32 = record.try_get("node_id").ok()?; + let self_described: serde_json::Value = record.try_get("self_described").ok()?; + + let val = serde_json::from_value::(self_described) + .inspect_err(|err| { + warn!("malformed description data for node {node_id}: {err}") + }); + val.ok().map(|res| (node_id as NodeId, res.into())) }) .collect::>() }) @@ -220,7 +232,7 @@ pub(crate) async fn get_bonded_node_description( ) -> anyhow::Result> { let mut conn = pool.acquire().await?; - sqlx::query!( + crate::db::query( r#"SELECT nd.node_id, moniker, @@ -230,7 +242,7 @@ pub(crate) async fn get_bonded_node_description( FROM nym_node_descriptions nd INNER JOIN - nym_nodes + nym_nodes nn on nd.node_id = nn.node_id WHERE bond_info IS NOT NULL "#, @@ -241,14 +253,15 @@ pub(crate) async fn get_bonded_node_description( records .into_iter() .map(|elem| { - let node_id: NodeId = elem.node_id.try_into().unwrap_or_default(); + let node_id: i32 = elem.try_get("node_id").unwrap_or(0); + let node_id: NodeId = node_id.try_into().unwrap_or_default(); ( node_id, NodeDescription { - moniker: elem.moniker.unwrap_or_default(), - website: elem.website.unwrap_or_default(), - security_contact: elem.security_contact.unwrap_or_default(), - details: elem.details.unwrap_or_default(), + moniker: elem.try_get("moniker").unwrap_or_default(), + website: elem.try_get("website").unwrap_or_default(), + security_contact: elem.try_get("security_contact").unwrap_or_default(), + details: elem.try_get("details").unwrap_or_default(), }, ) }) @@ -256,3 +269,34 @@ pub(crate) async fn get_bonded_node_description( }) .map_err(From::from) } + +pub(crate) async fn insert_nym_node_description( + conn: &mut DbConnection, + node_id: i64, + description: NodeDescriptionResponse, + timestamp: i64, +) -> anyhow::Result<()> { + crate::db::query( + r#" + INSERT INTO nym_node_descriptions ( + node_id, moniker, website, security_contact, details, last_updated_utc + ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (node_id) DO UPDATE SET + moniker = excluded.moniker, + website = excluded.website, + security_contact = excluded.security_contact, + details = excluded.details, + last_updated_utc = excluded.last_updated_utc + "#, + ) + .bind(node_id) + .bind(description.moniker) + .bind(description.website) + .bind(description.security_contact) + .bind(description.details) + .bind(timestamp) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs index 6041103096..fed7f8dc49 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs @@ -1,49 +1,103 @@ -use crate::db::{ - models::{NodeStats, ScrapeNodeKind, ScraperNodeInfo}, - DbPool, +use crate::{ + db::{ + models::{InsertStatsRecord, NodeStats, ScrapeNodeKind}, + DbPool, + }, + node_scraper::helpers::update_daily_stats_uncommitted, + utils::now_utc, }; use anyhow::Result; +use sqlx::Transaction; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{info, instrument}; -pub(crate) async fn insert_node_packet_stats( +#[instrument(level = "info", skip_all)] +pub(crate) async fn batch_store_packet_stats( pool: &DbPool, + results: Arc>>, +) -> anyhow::Result<()> { + let results_iter = results.lock().await; + info!( + "📊 ⏳ Storing {} packet stats into the DB", + results_iter.len() + ); + let started_at = now_utc(); + + let mut tx = pool + .begin() + .await + .map_err(|err| anyhow::anyhow!("Failed to begin transaction: {err}"))?; + + for stats_record in &(*results_iter) { + insert_node_packet_stats_uncommitted( + &mut tx, + &stats_record.node_kind, + &stats_record.stats, + stats_record.unix_timestamp, + ) + .await?; + + update_daily_stats_uncommitted( + &mut tx, + &stats_record.node_kind, + stats_record.timestamp_utc, + &stats_record.stats, + ) + .await?; + } + + tx.commit() + .await + .inspect(|_| { + let elapsed = now_utc() - started_at; + info!( + "📊 ☑️ Packet stats successfully committed to DB (took {}s)", + elapsed.as_seconds_f32() + ); + }) + .map_err(|err| anyhow::anyhow!("Failed to commit: {err}")) +} + +#[cfg(feature = "sqlite")] +pub(crate) async fn insert_node_packet_stats_uncommitted( + tx: &mut Transaction<'static, sqlx::Sqlite>, node_kind: &ScrapeNodeKind, stats: &NodeStats, timestamp_utc: i64, ) -> Result<()> { - let mut conn = pool.acquire().await?; - match node_kind { ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query!( + sqlx::query( r#" INSERT INTO mixnode_packet_stats_raw ( mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped ) VALUES (?, ?, ?, ?, ?) "#, - mix_id, - timestamp_utc, - stats.packets_received, - stats.packets_sent, - stats.packets_dropped, ) - .execute(&mut *conn) + .bind(mix_id) + .bind(timestamp_utc) + .bind(stats.packets_received) + .bind(stats.packets_sent) + .bind(stats.packets_dropped) + .execute(tx.as_mut()) .await?; } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { - sqlx::query!( + sqlx::query( r#" INSERT INTO nym_nodes_packet_stats_raw ( node_id, timestamp_utc, packets_received, packets_sent, packets_dropped ) VALUES (?, ?, ?, ?, ?) "#, - node_id, - timestamp_utc, - stats.packets_received, - stats.packets_sent, - stats.packets_dropped, ) - .execute(&mut *conn) + .bind(node_id) + .bind(timestamp_utc) + .bind(stats.packets_received) + .bind(stats.packets_sent) + .bind(stats.packets_dropped) + .execute(tx.as_mut()) .await?; } } @@ -51,18 +105,62 @@ pub(crate) async fn insert_node_packet_stats( Ok(()) } -pub(crate) async fn get_raw_node_stats( - pool: &DbPool, - node: &ScraperNodeInfo, -) -> Result> { - let mut conn = pool.acquire().await?; +#[cfg(feature = "pg")] +pub(crate) async fn insert_node_packet_stats_uncommitted( + tx: &mut Transaction<'static, sqlx::Postgres>, + node_kind: &ScrapeNodeKind, + stats: &NodeStats, + timestamp_utc: i64, +) -> Result<()> { + match node_kind { + ScrapeNodeKind::LegacyMixnode { mix_id } => { + sqlx::query( + r#" + INSERT INTO mixnode_packet_stats_raw ( + mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped + ) VALUES ($1, $2, $3, $4, $5) + "#, + ) + .bind(mix_id) + .bind(timestamp_utc) + .bind(stats.packets_received) + .bind(stats.packets_sent) + .bind(stats.packets_dropped) + .execute(tx.as_mut()) + .await?; + } + ScrapeNodeKind::MixingNymNode { node_id } + | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { + sqlx::query( + r#" + INSERT INTO nym_nodes_packet_stats_raw ( + node_id, timestamp_utc, packets_received, packets_sent, packets_dropped + ) VALUES ($1, $2, $3, $4, $5) + "#, + ) + .bind(node_id) + .bind(timestamp_utc) + .bind(stats.packets_received) + .bind(stats.packets_sent) + .bind(stats.packets_dropped) + .execute(tx.as_mut()) + .await?; + } + } - let packets = match node.node_kind { + Ok(()) +} + +#[cfg(feature = "sqlite")] +pub(crate) async fn get_raw_node_stats( + tx: &mut Transaction<'static, sqlx::Sqlite>, + node_kind: &ScrapeNodeKind, +) -> Result> { + let packets = match node_kind { // if no packets are found, it's fine to assume 0 because that's also // SQL default value if none provided ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query_as!( - NodeStats, + sqlx::query_as::<_, NodeStats>( r#" SELECT COALESCE(packets_received, 0) as packets_received, @@ -73,15 +171,14 @@ pub(crate) async fn get_raw_node_stats( ORDER BY timestamp_utc DESC LIMIT 1 OFFSET 1 "#, - mix_id ) - .fetch_optional(&mut *conn) + .bind(mix_id) + .fetch_optional(tx.as_mut()) .await? } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { - sqlx::query_as!( - NodeStats, + sqlx::query_as::<_, NodeStats>( r#" SELECT COALESCE(packets_received, 0) as packets_received, @@ -92,9 +189,9 @@ pub(crate) async fn get_raw_node_stats( ORDER BY timestamp_utc DESC LIMIT 1 OFFSET 1 "#, - node_id ) - .fetch_optional(&mut *conn) + .bind(node_id) + .fetch_optional(tx.as_mut()) .await? } }; @@ -102,29 +199,76 @@ pub(crate) async fn get_raw_node_stats( Ok(packets) } -pub(crate) async fn insert_daily_node_stats( - pool: &DbPool, - node: &ScraperNodeInfo, +#[cfg(feature = "pg")] +pub(crate) async fn get_raw_node_stats( + tx: &mut Transaction<'static, sqlx::Postgres>, + node_kind: &ScrapeNodeKind, +) -> Result> { + let packets = match node_kind { + // if no packets are found, it's fine to assume 0 because that's also + // SQL default value if none provided + ScrapeNodeKind::LegacyMixnode { mix_id } => { + sqlx::query_as::<_, NodeStats>( + r#" + SELECT + COALESCE(packets_received, 0) as packets_received, + COALESCE(packets_sent, 0) as packets_sent, + COALESCE(packets_dropped, 0) as packets_dropped + FROM mixnode_packet_stats_raw + WHERE mix_id = $1 + ORDER BY timestamp_utc DESC + LIMIT 1 OFFSET 1 + "#, + ) + .bind(mix_id) + .fetch_optional(tx.as_mut()) + .await? + } + ScrapeNodeKind::MixingNymNode { node_id } + | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { + sqlx::query_as::<_, NodeStats>( + r#" + SELECT + COALESCE(packets_received, 0) as packets_received, + COALESCE(packets_sent, 0) as packets_sent, + COALESCE(packets_dropped, 0) as packets_dropped + FROM nym_nodes_packet_stats_raw + WHERE node_id = $1 + ORDER BY timestamp_utc DESC + LIMIT 1 OFFSET 1 + "#, + ) + .bind(node_id) + .fetch_optional(tx.as_mut()) + .await? + } + }; + + Ok(packets) +} + +#[cfg(feature = "sqlite")] +pub(crate) async fn insert_daily_node_stats_uncommitted( + tx: &mut Transaction<'static, sqlx::Sqlite>, + node_kind: &ScrapeNodeKind, date_utc: &str, packets: NodeStats, ) -> Result<()> { - let mut conn = pool.acquire().await?; - - match node.node_kind { + match node_kind { ScrapeNodeKind::LegacyMixnode { mix_id } => { - let total_stake = sqlx::query_scalar!( + let total_stake = sqlx::query_scalar::<_, i64>( r#" SELECT total_stake FROM mixnodes WHERE mix_id = ? "#, - mix_id ) - .fetch_one(&mut *conn) + .bind(mix_id) + .fetch_one(tx.as_mut()) .await?; - sqlx::query!( + sqlx::query( r#" INSERT INTO mixnode_daily_stats ( mix_id, date_utc, @@ -137,31 +281,31 @@ pub(crate) async fn insert_daily_node_stats( packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent, packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped "#, - mix_id, - date_utc, - total_stake, - packets.packets_received, - packets.packets_sent, - packets.packets_dropped, ) - .execute(&mut *conn) + .bind(mix_id) + .bind(date_utc) + .bind(total_stake) + .bind(packets.packets_received) + .bind(packets.packets_sent) + .bind(packets.packets_dropped) + .execute(tx.as_mut()) .await?; } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { - let total_stake = sqlx::query_scalar!( + let total_stake = sqlx::query_scalar::<_, i64>( r#" SELECT total_stake FROM nym_nodes WHERE node_id = ? "#, - node_id ) - .fetch_one(&mut *conn) + .bind(node_id) + .fetch_one(tx.as_mut()) .await?; - sqlx::query!( + sqlx::query( r#" INSERT INTO nym_node_daily_mixing_stats ( node_id, date_utc, @@ -174,14 +318,100 @@ pub(crate) async fn insert_daily_node_stats( packets_sent = nym_node_daily_mixing_stats.packets_sent + excluded.packets_sent, packets_dropped = nym_node_daily_mixing_stats.packets_dropped + excluded.packets_dropped "#, - node_id, - date_utc, - total_stake, - packets.packets_received, - packets.packets_sent, - packets.packets_dropped, ) - .execute(&mut *conn) + .bind(node_id) + .bind(date_utc) + .bind(total_stake) + .bind(packets.packets_received) + .bind(packets.packets_sent) + .bind(packets.packets_dropped) + .execute(tx.as_mut()) + .await?; + } + } + + Ok(()) +} + +#[cfg(feature = "pg")] +pub(crate) async fn insert_daily_node_stats_uncommitted( + tx: &mut Transaction<'static, sqlx::Postgres>, + node_kind: &ScrapeNodeKind, + date_utc: &str, + packets: NodeStats, +) -> Result<()> { + match node_kind { + ScrapeNodeKind::LegacyMixnode { mix_id } => { + let total_stake = sqlx::query_scalar::<_, i64>( + r#" + SELECT + total_stake + FROM mixnodes + WHERE mix_id = $1 + "#, + ) + .bind(mix_id) + .fetch_one(tx.as_mut()) + .await?; + + sqlx::query( + r#" + INSERT INTO mixnode_daily_stats ( + mix_id, date_utc, + total_stake, packets_received, + packets_sent, packets_dropped + ) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT(mix_id, date_utc) DO UPDATE SET + total_stake = excluded.total_stake, + packets_received = mixnode_daily_stats.packets_received + excluded.packets_received, + packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent, + packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped + "#, + ) + .bind(mix_id) + .bind(date_utc) + .bind(total_stake) + .bind(packets.packets_received) + .bind(packets.packets_sent) + .bind(packets.packets_dropped) + .execute(tx.as_mut()) + .await?; + } + ScrapeNodeKind::MixingNymNode { node_id } + | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { + let total_stake = sqlx::query_scalar::<_, i64>( + r#" + SELECT + total_stake + FROM nym_nodes + WHERE node_id = $1 + "#, + ) + .bind(node_id) + .fetch_one(tx.as_mut()) + .await?; + + sqlx::query( + r#" + INSERT INTO nym_node_daily_mixing_stats ( + node_id, date_utc, + total_stake, packets_received, + packets_sent, packets_dropped + ) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT(node_id, date_utc) DO UPDATE SET + total_stake = excluded.total_stake, + packets_received = nym_node_daily_mixing_stats.packets_received + excluded.packets_received, + packets_sent = nym_node_daily_mixing_stats.packets_sent + excluded.packets_sent, + packets_dropped = nym_node_daily_mixing_stats.packets_dropped + excluded.packets_dropped + "#, + ) + .bind(node_id) + .bind(date_utc) + .bind(total_stake) + .bind(packets.packets_received) + .bind(packets.packets_sent) + .bind(packets.packets_dropped) + .execute(tx.as_mut()) .await?; } } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs index aff700595d..efecd3c83b 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs @@ -1,13 +1,18 @@ use crate::{ db::{ models::{ScrapeNodeKind, ScraperNodeInfo}, - queries, DbPool, + queries::{ + self, gateways::insert_gateway_description, mixnodes::insert_mixnode_description, + nym_nodes::insert_nym_node_description, + }, + DbPool, }, - mixnet_scraper::helpers::NodeDescriptionResponse, + node_scraper::helpers::NodeDescriptionResponse, + utils::now_utc, }; use anyhow::Result; -use chrono::Utc; use nym_validator_client::nym_api::SkimmedNode; +use sqlx::Row; pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result> { let mut nodes_to_scrape = Vec::new(); @@ -64,12 +69,12 @@ pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result Result Result Result<()> { - let timestamp = Utc::now().timestamp(); + let timestamp = now_utc().unix_timestamp(); let mut conn = pool.acquire().await?; match node_kind { ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query!( - r#" - INSERT INTO mixnode_description ( - mix_id, moniker, website, security_contact, details, last_updated_utc - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT (mix_id) DO UPDATE SET - moniker = excluded.moniker, - website = excluded.website, - security_contact = excluded.security_contact, - details = excluded.details, - last_updated_utc = excluded.last_updated_utc - "#, - mix_id, - description.moniker, - description.website, - description.security_contact, - description.details, - timestamp, - ) - .execute(&mut *conn) - .await?; + insert_mixnode_description(&mut conn, mix_id, description, timestamp).await?; } ScrapeNodeKind::MixingNymNode { node_id } => { - sqlx::query!( - r#" - INSERT INTO nym_node_descriptions ( - node_id, moniker, website, security_contact, details, last_updated_utc - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT (node_id) DO UPDATE SET - moniker = excluded.moniker, - website = excluded.website, - security_contact = excluded.security_contact, - details = excluded.details, - last_updated_utc = excluded.last_updated_utc - "#, - node_id, - description.moniker, - description.website, - description.security_contact, - description.details, - timestamp, - ) - .execute(&mut *conn) - .await?; + insert_nym_node_description(&mut conn, node_id, description, timestamp).await?; } - ScrapeNodeKind::EntryExitNymNode { identity_key, .. } => { - sqlx::query!( - r#" - INSERT INTO gateway_description ( - gateway_identity_key, - moniker, - website, - security_contact, - details, - last_updated_utc - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT (gateway_identity_key) DO UPDATE SET - moniker = excluded.moniker, - website = excluded.website, - security_contact = excluded.security_contact, - details = excluded.details, - last_updated_utc = excluded.last_updated_utc - "#, - identity_key, - description.moniker, - description.website, - description.security_contact, - description.details, - timestamp, - ) - .execute(&mut *conn) - .await?; + ScrapeNodeKind::EntryExitNymNode { + node_id, + identity_key, + } => { + insert_nym_node_description(&mut conn, node_id, description.clone(), timestamp).await?; + // for historic reasons (/gateways API), store this info into gateways table as well + insert_gateway_description(&mut conn, identity_key, description, timestamp).await?; } } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs index ec23ad1500..3695c4481b 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Utc}; use futures_util::TryStreamExt; use std::collections::HashMap; use tracing::error; @@ -19,17 +18,17 @@ use crate::{ error::{HttpError, HttpResult}, models::SummaryHistory, }, + utils::unix_timestamp_to_utc_rfc3339, }; pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result> { let mut conn = pool.acquire().await?; - let items = sqlx::query_as!( - SummaryHistoryDto, + let items = crate::db::query_as::( r#"SELECT - id as "id!", - date as "date!", - timestamp_utc as "timestamp_utc!", - value_json as "value_json!" + id, + date, + timestamp_utc, + value_json FROM summary_history ORDER BY date DESC LIMIT 30"#, @@ -51,13 +50,12 @@ pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result anyhow::Result> { let mut conn = pool.acquire().await?; - Ok(sqlx::query_as!( - SummaryDto, + Ok(crate::db::query_as::( r#"SELECT - key as "key!", - value_json as "value_json!", - last_updated_utc as "last_updated_utc!" - FROM summary"# + key, + value_json, + last_updated_utc + FROM summary"#, ) .fetch(&mut *conn) .try_collect::>() @@ -162,10 +160,5 @@ fn to_count_i32(value: &SummaryDto) -> i32 { } fn to_timestamp(value: &SummaryDto) -> String { - timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339() -} - -fn timestamp_as_utc(unix_timestamp: u64) -> DateTime { - let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); - d.into() + unix_timestamp_to_utc_rfc3339(value.last_updated_utc) } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs index dc1e20ee2e..80dafc070d 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/testruns.rs @@ -1,42 +1,38 @@ +use crate::db::models::{TestRunDto, TestRunStatus}; +use crate::db::DbConnection; use crate::db::DbPool; use crate::http::models::TestrunAssignment; -use crate::{ - db::models::{TestRunDto, TestRunStatus}, - testruns::now_utc, -}; -use chrono::Duration; -use sqlx::{pool::PoolConnection, Sqlite}; +use crate::utils::now_utc; +use sqlx::Row; +use time::Duration; -pub(crate) async fn count_testruns_in_progress( - conn: &mut PoolConnection, -) -> anyhow::Result { - sqlx::query_scalar!( - r#"SELECT - COUNT(id) as "count: i64" - FROM testruns - WHERE - status = ? - "#, - TestRunStatus::InProgress as i64, - ) - .fetch_one(conn.as_mut()) - .await - .map_err(anyhow::Error::from) +pub(crate) async fn count_testruns_in_progress(conn: &mut DbConnection) -> anyhow::Result { + #[cfg(feature = "sqlite")] + let sql = "SELECT COUNT(id) FROM testruns WHERE status = ?"; + + #[cfg(feature = "pg")] + let sql = "SELECT COUNT(id) FROM testruns WHERE status = $1"; + + let count: i64 = sqlx::query_scalar(sql) + .bind(TestRunStatus::InProgress as i32) + .fetch_one(conn.as_mut()) + .await?; + + Ok(count) } pub(crate) async fn get_in_progress_testrun_by_id( - conn: &mut PoolConnection, - testrun_id: i64, + conn: &mut DbConnection, + testrun_id: i32, ) -> anyhow::Result { - sqlx::query_as!( - TestRunDto, + crate::db::query_as::( r#"SELECT - id as "id!", - gateway_id as "gateway_id!", - status as "status!", - created_utc as "created_utc!", - ip_address as "ip_address!", - log as "log!", + id, + gateway_id, + status, + created_utc, + ip_address, + log, last_assigned_utc FROM testruns WHERE @@ -45,12 +41,12 @@ pub(crate) async fn get_in_progress_testrun_by_id( status = ? ORDER BY created_utc LIMIT 1"#, - testrun_id, - TestRunStatus::InProgress as i64, ) + .bind(testrun_id) + .bind(TestRunStatus::InProgress as i32) .fetch_one(conn.as_mut()) .await - .map_err(|e| anyhow::anyhow!("Couldn't retrieve testrun {testrun_id}: {e}")) + .map_err(|e| anyhow::anyhow!("Failed to retrieve in-progress testrun {testrun_id}: {e}")) } pub(crate) async fn update_testruns_assigned_before( @@ -59,9 +55,9 @@ pub(crate) async fn update_testruns_assigned_before( ) -> anyhow::Result { let mut conn = db.acquire().await?; let previous_run = now_utc() - max_age; - let cutoff_timestamp = previous_run.timestamp(); + let cutoff_timestamp = previous_run.unix_timestamp(); - let res = sqlx::query!( + let res = crate::db::query( r#"UPDATE testruns SET @@ -71,10 +67,10 @@ pub(crate) async fn update_testruns_assigned_before( AND last_assigned_utc < ? "#, - TestRunStatus::Queued as i64, - TestRunStatus::InProgress as i64, - cutoff_timestamp ) + .bind(TestRunStatus::Queued as i32) + .bind(TestRunStatus::InProgress as i32) + .bind(cutoff_timestamp) .execute(conn.as_mut()) .await?; @@ -91,36 +87,36 @@ pub(crate) async fn update_testruns_assigned_before( } pub(crate) async fn assign_oldest_testrun( - conn: &mut PoolConnection, + conn: &mut DbConnection, ) -> anyhow::Result> { - let now = now_utc().timestamp(); + let now = now_utc().unix_timestamp(); // find & mark as "In progress" in the same transaction to avoid race conditions - let returning = sqlx::query!( + let returning = crate::db::query( r#"UPDATE testruns SET status = ?, last_assigned_utc = ? - WHERE rowid = + WHERE id = ( - SELECT rowid + SELECT id FROM testruns WHERE status = ? ORDER BY created_utc asc LIMIT 1 ) RETURNING - id as "id!", + id, gateway_id "#, - TestRunStatus::InProgress as i64, - now, - TestRunStatus::Queued as i64, ) + .bind(TestRunStatus::InProgress as i32) + .bind(now) + .bind(TestRunStatus::Queued as i32) .fetch_optional(conn.as_mut()) .await?; if let Some(testrun) = returning { - let gw_identity = sqlx::query!( + let gw_identity = crate::db::query( r#" SELECT id, @@ -128,14 +124,14 @@ pub(crate) async fn assign_oldest_testrun( FROM gateways WHERE id = ? LIMIT 1"#, - testrun.gateway_id ) + .bind(testrun.try_get::("gateway_id")?) .fetch_one(conn.as_mut()) .await?; Ok(Some(TestrunAssignment { - testrun_id: testrun.id, - gateway_identity_key: gw_identity.gateway_identity_key, + testrun_id: testrun.try_get("id")?, + gateway_identity_key: gw_identity.try_get("gateway_identity_key")?, assigned_at_utc: now, })) } else { @@ -144,67 +140,146 @@ pub(crate) async fn assign_oldest_testrun( } pub(crate) async fn update_testrun_status( - conn: &mut PoolConnection, - testrun_id: i64, + conn: &mut DbConnection, + testrun_id: i32, status: TestRunStatus, ) -> anyhow::Result<()> { - let status = status as u32; - sqlx::query!( - "UPDATE testruns SET status = ? WHERE id = ?", - status, - testrun_id - ) - .execute(conn.as_mut()) - .await?; + let status = status as i32; + crate::db::query("UPDATE testruns SET status = ? WHERE id = ?") + .bind(status) + .bind(testrun_id) + .execute(conn.as_mut()) + .await?; Ok(()) } pub(crate) async fn update_gateway_last_probe_log( - conn: &mut PoolConnection, - gateway_pk: i64, - log: &str, + conn: &mut DbConnection, + gateway_pk: i32, + log: String, ) -> anyhow::Result<()> { - sqlx::query!( - "UPDATE gateways SET last_probe_log = ? WHERE id = ?", - log, - gateway_pk - ) - .execute(conn.as_mut()) - .await - .map(drop) - .map_err(From::from) + crate::db::query("UPDATE gateways SET last_probe_log = ? WHERE id = ?") + .bind(log) + .bind(gateway_pk) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(|e| { + anyhow::anyhow!( + "Failed to update probe log for gateway {}: {}", + gateway_pk, + e + ) + }) } pub(crate) async fn update_gateway_last_probe_result( - conn: &mut PoolConnection, - gateway_pk: i64, - result: &str, + conn: &mut DbConnection, + gateway_pk: i32, + result: String, ) -> anyhow::Result<()> { - sqlx::query!( - "UPDATE gateways SET last_probe_result = ? WHERE id = ?", - result, - gateway_pk - ) - .execute(conn.as_mut()) - .await - .map(drop) - .map_err(From::from) + crate::db::query("UPDATE gateways SET last_probe_result = ? WHERE id = ?") + .bind(result) + .bind(gateway_pk) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(|e| { + anyhow::anyhow!( + "Failed to update probe result for gateway {}: {}", + gateway_pk, + e + ) + }) } pub(crate) async fn update_gateway_score( - conn: &mut PoolConnection, - gateway_pk: i64, + conn: &mut DbConnection, + gateway_pk: i32, ) -> anyhow::Result<()> { - let now = now_utc().timestamp(); - sqlx::query!( - "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", - now, - now, - gateway_pk - ) - .execute(conn.as_mut()) - .await - .map(drop) - .map_err(From::from) + let now = now_utc().unix_timestamp(); + crate::db::query("UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?") + .bind(now) + .bind(now) + .bind(gateway_pk) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn get_testrun_by_id( + conn: &mut DbConnection, + testrun_id: i32, +) -> anyhow::Result { + crate::db::query_as::( + r#"SELECT + id, + gateway_id, + status, + created_utc, + ip_address, + log, + last_assigned_utc + FROM testruns + WHERE id = ?"#, + ) + .bind(testrun_id) + .fetch_one(conn.as_mut()) + .await + .map_err(|e| anyhow::anyhow!("Testrun {} not found: {}", testrun_id, e)) +} + +pub(crate) async fn insert_external_testrun( + conn: &mut DbConnection, + testrun_id: i32, + gateway_id: i32, + assigned_at_utc: i64, +) -> anyhow::Result<()> { + let now = crate::utils::now_utc().unix_timestamp(); + + crate::db::query( + r#"INSERT INTO testruns ( + id, + gateway_id, + status, + created_utc, + last_assigned_utc, + ip_address, + log + ) VALUES (?, ?, ?, ?, ?, ?, ?)"#, + ) + .bind(testrun_id) + .bind(gateway_id) + .bind(TestRunStatus::InProgress as i32) + .bind(now) + .bind(assigned_at_utc) + .bind("external") // Marker for external origin + .bind("") // Empty initial log + .execute(conn.as_mut()) + .await?; + + tracing::debug!( + "Created external testrun {} for gateway {}", + testrun_id, + gateway_id + ); + Ok(()) +} + +pub(crate) async fn update_testrun_status_by_gateway( + conn: &mut DbConnection, + gateway_id: i32, + status: TestRunStatus, +) -> anyhow::Result<()> { + let status = status as i32; + crate::db::query("UPDATE testruns SET status = ? WHERE gateway_id = ? AND status = ?") + .bind(status) + .bind(gateway_id) + .bind(TestRunStatus::InProgress as i32) + .execute(conn.as_mut()) + .await?; + + Ok(()) } diff --git a/nym-node-status-api/nym-node-status-api/src/db/query_wrapper.rs b/nym-node-status-api/nym-node-status-api/src/db/query_wrapper.rs new file mode 100644 index 0000000000..e061d654a1 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/db/query_wrapper.rs @@ -0,0 +1,251 @@ +use sqlx::Database; + +/// Converts SQLite-style ? placeholders to PostgreSQL $N format +#[cfg(feature = "pg")] +fn convert_placeholders(query: &str) -> String { + let mut result = String::with_capacity(query.len() + 10); + let mut placeholder_count = 0; + let mut chars = query.chars(); + let mut in_string: Option = None; + let mut escape_next = false; + + #[allow(clippy::while_let_on_iterator)] + while let Some(ch) = chars.next() { + if escape_next { + result.push(ch); + escape_next = false; + continue; + } + + if let Some(quote_char) = in_string { + result.push(ch); + if ch == quote_char { + in_string = None; + } else if ch == '\\' { + escape_next = true; + } + continue; + } + + match ch { + '\\' => { + result.push(ch); + escape_next = true; + } + '\'' | '"' => { + result.push(ch); + in_string = Some(ch); + } + '?' => { + placeholder_count += 1; + result.push_str(&format!("${placeholder_count}")); + } + _ => { + result.push(ch); + } + } + } + + result +} + +/// Creates a query that automatically handles placeholder conversion +#[cfg(feature = "sqlite")] +pub fn query( + sql: &str, +) -> sqlx::query::Query<'_, sqlx::Sqlite, ::Arguments<'_>> { + sqlx::query(sql) +} + +#[cfg(feature = "pg")] +pub fn query( + sql: &str, +) -> sqlx::query::Query<'static, sqlx::Postgres, ::Arguments<'static>> { + let converted = convert_placeholders(sql); + sqlx::query(Box::leak(converted.into_boxed_str())) +} + +/// Creates a query_as that automatically handles placeholder conversion +#[cfg(feature = "sqlite")] +pub fn query_as( + sql: &str, +) -> sqlx::query::QueryAs<'_, sqlx::Sqlite, O, ::Arguments<'_>> +where + O: for<'r> sqlx::FromRow<'r, ::Row>, +{ + sqlx::query_as(sql) +} + +#[cfg(feature = "pg")] +pub fn query_as( + sql: &str, +) -> sqlx::query::QueryAs< + 'static, + sqlx::Postgres, + O, + ::Arguments<'static>, +> +where + O: for<'r> sqlx::FromRow<'r, ::Row>, +{ + let converted = convert_placeholders(sql); + sqlx::query_as(Box::leak(converted.into_boxed_str())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(feature = "pg")] + fn test_convert_placeholders() { + // Basic conversion + assert_eq!( + convert_placeholders(r"SELECT * FROM table WHERE id = ?"), + r"SELECT * FROM table WHERE id = $1" + ); + + // Multiple placeholders + assert_eq!( + convert_placeholders(r"INSERT INTO table (a, b, c) VALUES (?, ?, ?)"), + r"INSERT INTO table (a, b, c) VALUES ($1, $2, $3)" + ); + + // Placeholder inside string literal should be ignored + assert_eq!( + convert_placeholders(r"SELECT * FROM table WHERE name = 'test?' AND id = ?"), + r"SELECT * FROM table WHERE name = 'test?' AND id = $1" + ); + + // Update statement + assert_eq!( + convert_placeholders(r"UPDATE table SET a = ?, b = ? WHERE id = ?"), + r"UPDATE table SET a = $1, b = $2 WHERE id = $3" + ); + + // Test with 10 placeholders (like in update_mixnodes) + assert_eq!( + convert_placeholders(r"INSERT INTO t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), + r"INSERT INTO t VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)" + ); + + // No placeholders + assert_eq!( + convert_placeholders(r"SELECT * FROM table"), + r"SELECT * FROM table" + ); + + // Placeholder at the beginning + assert_eq!(convert_placeholders(r"? AND ?"), r"$1 AND $2"); + + // Placeholder at the end + assert_eq!( + convert_placeholders(r"SELECT * FROM table WHERE id = ?"), + r"SELECT * FROM table WHERE id = $1" + ); + + // Adjacent placeholders + assert_eq!( + convert_placeholders(r"VALUES(?,? ,?)"), + r"VALUES($1,$2 ,$3)" + ); + + // Escaped single quote + assert_eq!( + convert_placeholders(r"SELECT * FROM foo WHERE bar = 'it\'s a test' AND baz = ?"), + r"SELECT * FROM foo WHERE bar = 'it\'s a test' AND baz = $1" + ); + + // Double quotes + assert_eq!( + convert_placeholders(r#"SELECT * FROM "table" WHERE "column" = ? AND name = "test?""#), + r#"SELECT * FROM "table" WHERE "column" = $1 AND name = "test?""# + ); + + // Mixed quotes + assert_eq!( + convert_placeholders( + r#"SELECT * FROM table WHERE a = 'single?' AND b = "double?" AND c = ?"# + ), + r#"SELECT * FROM table WHERE a = 'single?' AND b = "double?" AND c = $1"# + ); + + // Escaped backslash before quote + assert_eq!( + convert_placeholders(r"SELECT * FROM table WHERE path = 'C:\\?' AND id = ?"), + r"SELECT * FROM table WHERE path = 'C:\\?' AND id = $1" + ); + + // Multiple escaped quotes + assert_eq!( + convert_placeholders( + r#"INSERT INTO table (msg) VALUES ('it\'s "complex" test') WHERE id = ?"# + ), + r#"INSERT INTO table (msg) VALUES ('it\'s "complex" test') WHERE id = $1"# + ); + + // Very long query with many placeholders + let long_query = r"INSERT INTO very_long_table_name (col1, col2, col3, col4, col5, col6, col7, col8, col9, col10, col11, col12, col13, col14, col15) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + let expected = r"INSERT INTO very_long_table_name (col1, col2, col3, col4, col5, col6, col7, col8, col9, col10, col11, col12, col13, col14, col15) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)"; + assert_eq!(convert_placeholders(long_query), expected); + + // Query with comments (question marks in comments are also converted) + assert_eq!( + convert_placeholders( + r"-- This is a comment with ? + SELECT * FROM table WHERE id = ? -- another comment ?" + ), + r"-- This is a comment with $1 + SELECT * FROM table WHERE id = $2 -- another comment $3" + ); + + // Multiline strings + assert_eq!( + convert_placeholders( + r"SELECT * FROM table + WHERE description = 'This is a + multiline string with ?' + AND id = ?" + ), + r"SELECT * FROM table + WHERE description = 'This is a + multiline string with ?' + AND id = $1" + ); + + // Complex nested quotes + assert_eq!( + convert_placeholders( + r#"SELECT json_extract(data, '$.items[?(@.name=="test?")]') FROM table WHERE id = ?"# + ), + r#"SELECT json_extract(data, '$.items[?(@.name=="test?")]') FROM table WHERE id = $1"# + ); + + // Empty string + assert_eq!(convert_placeholders(""), ""); + + // Only placeholders + assert_eq!(convert_placeholders("???"), "$1$2$3"); + + // Unicode in strings + assert_eq!( + convert_placeholders(r"SELECT * FROM table WHERE name = '测试?' AND id = ?"), + r"SELECT * FROM table WHERE name = '测试?' AND id = $1" + ); + + // Test case with backslash at end of string + assert_eq!( + convert_placeholders(r"SELECT * FROM table WHERE path LIKE '%\\' AND id = ?"), + r"SELECT * FROM table WHERE path LIKE '%\\' AND id = $1" + ); + + // Mismatched quotes + assert_eq!( + convert_placeholders(r#"SELECT * FROM foo WHERE bar = "'" AND baz = ?"#), + r#"SELECT * FROM foo WHERE bar = "'" AND baz = $1"# + ); + + // Unmatched quote + assert_eq!(convert_placeholders(r"SELECT 'oops?"), r"SELECT 'oops?"); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/tests.rs b/nym-node-status-api/nym-node-status-api/src/db/tests.rs new file mode 100644 index 0000000000..a176e6d340 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/db/tests.rs @@ -0,0 +1,461 @@ +#[cfg(test)] +mod db_tests { + + #[test] + fn test_gateway_dto_try_from() { + let gateway_dto = crate::db::models::GatewayDto { + gateway_identity_key: "test_identity".to_string(), + bonded: true, + performance: 100, + self_described: Some("{\"key\":\"value\"}".to_string()), + explorer_pretty_bond: Some("{\"key\":\"value\"}".to_string()), + last_probe_result: Some("{\"key\":\"value\"}".to_string()), + last_probe_log: Some("log".to_string()), + last_testrun_utc: Some(1672531200), + last_updated_utc: 1672531200, + moniker: "moniker".to_string(), + security_contact: "contact".to_string(), + details: "details".to_string(), + website: "website".to_string(), + }; + + let http_gateway: crate::http::models::Gateway = gateway_dto.try_into().unwrap(); + + assert_eq!(http_gateway.gateway_identity_key, "test_identity"); + assert!(http_gateway.bonded); + assert_eq!(http_gateway.performance, 100); + assert!(http_gateway.self_described.is_some()); + assert!(http_gateway.explorer_pretty_bond.is_some()); + assert!(http_gateway.last_probe_result.is_some()); + assert_eq!(http_gateway.last_probe_log, Some("log".to_string())); + assert!(http_gateway.last_testrun_utc.is_some()); + assert!(!http_gateway.last_updated_utc.is_empty()); + assert_eq!(http_gateway.description.moniker, "moniker"); + assert_eq!(http_gateway.description.website, "website"); + assert_eq!(http_gateway.description.security_contact, "contact"); + assert_eq!(http_gateway.description.details, "details"); + } + + #[test] + fn test_mixnode_dto_try_from() { + let mixnode_dto = crate::db::models::MixnodeDto { + mix_id: 1, + bonded: true, + is_dp_delegatee: false, + total_stake: 1000000, + full_details: "{\"key\":\"value\"}".to_string(), + self_described: Some("{\"key\":\"value\"}".to_string()), + last_updated_utc: 1672531200, + moniker: "moniker".to_string(), + website: "website".to_string(), + security_contact: "contact".to_string(), + details: "details".to_string(), + }; + + let http_mixnode: crate::http::models::Mixnode = mixnode_dto.try_into().unwrap(); + + assert_eq!(http_mixnode.mix_id, 1); + assert!(http_mixnode.bonded); + assert!(!http_mixnode.is_dp_delegatee); + assert_eq!(http_mixnode.total_stake, 1000000); + assert!(http_mixnode.full_details.is_some()); + assert!(http_mixnode.self_described.is_some()); + assert!(!http_mixnode.last_updated_utc.is_empty()); + assert_eq!(http_mixnode.description.moniker, "moniker"); + assert_eq!(http_mixnode.description.website, "website"); + assert_eq!(http_mixnode.description.security_contact, "contact"); + assert_eq!(http_mixnode.description.details, "details"); + } + + #[test] + fn test_summary_history_dto_try_from() { + let summary_history_dto = crate::db::models::SummaryHistoryDto { + id: 1, + date: "2023-01-01".to_string(), + value_json: "{\"key\":\"value\"}".to_string(), + timestamp_utc: 1672531200, + }; + + let summary_history: crate::http::models::SummaryHistory = + summary_history_dto.try_into().unwrap(); + + assert_eq!(summary_history.date, "2023-01-01"); + assert!(summary_history.value_json.is_object()); + assert!(!summary_history.timestamp_utc.is_empty()); + } + + #[test] + fn test_gateway_sessions_record_try_from() { + let gateway_sessions_record = crate::db::models::GatewaySessionsRecord { + gateway_identity_key: "test_identity".to_string(), + node_id: 1, + day: time::macros::date!(2023 - 01 - 01), + unique_active_clients: 10, + session_started: 100, + users_hashes: Some("{\"key\":\"value\"}".to_string()), + vpn_sessions: Some("{\"key\":\"value\"}".to_string()), + mixnet_sessions: Some("{\"key\":\"value\"}".to_string()), + unknown_sessions: Some("{\"key\":\"value\"}".to_string()), + }; + + let session_stats: crate::http::models::SessionStats = + gateway_sessions_record.try_into().unwrap(); + + assert_eq!(session_stats.gateway_identity_key, "test_identity"); + assert_eq!(session_stats.node_id, 1); + assert_eq!(session_stats.day, time::macros::date!(2023 - 01 - 01)); + assert_eq!(session_stats.unique_active_clients, 10); + assert_eq!(session_stats.session_started, 100); + assert!(session_stats.users_hashes.is_some()); + assert!(session_stats.vpn_sessions.is_some()); + assert!(session_stats.mixnet_sessions.is_some()); + assert!(session_stats.unknown_sessions.is_some()); + } + + #[test] + fn test_nym_node_dto_try_from() { + let ed25519_pk = nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(&[1; 32]).unwrap(); + let x25519_pk = nym_crypto::asymmetric::x25519::PublicKey::from_bytes(&[2; 32]).unwrap(); + + let nym_node_dto = crate::db::models::NymNodeDto { + node_id: 1, + ed25519_identity_pubkey: ed25519_pk.to_base58_string(), + total_stake: 1000000, + ip_addresses: serde_json::json!(["1.1.1.1"]), + mix_port: 1789, + x25519_sphinx_pubkey: x25519_pk.to_base58_string(), + node_role: serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { + layer: 1 + }), + supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRoles { + entry: false, + mixnode: true, + exit_nr: false, + exit_ipr: false, + }), + entry: None, + performance: "1.0".to_string(), + self_described: None, + bond_info: None, + }; + + let skimmed_node: nym_validator_client::nym_api::SkimmedNode = + nym_node_dto.try_into().unwrap(); + + assert_eq!(skimmed_node.node_id, 1); + assert_eq!(skimmed_node.ed25519_identity_pubkey, ed25519_pk); + assert_eq!( + skimmed_node.ip_addresses, + vec!["1.1.1.1".parse::().unwrap()] + ); + assert_eq!(skimmed_node.mix_port, 1789); + assert_eq!(skimmed_node.x25519_sphinx_pubkey, x25519_pk); + + match skimmed_node.role { + nym_validator_client::nym_nodes::NodeRole::Mixnode { layer } => assert_eq!(layer, 1), + _ => panic!("Unexpected node role"), + } + assert!(!skimmed_node.supported_roles.entry); + assert!(skimmed_node.supported_roles.mixnode); + assert!(!skimmed_node.supported_roles.exit_nr); + assert!(!skimmed_node.supported_roles.exit_ipr); + assert!(skimmed_node.entry.is_none()); + assert_eq!( + skimmed_node.performance, + nym_contracts_common::Percent::from_percentage_value(100).unwrap() + ); + } +} + +#[test] +fn test_nym_node_insert_record_new() { + let ed25519_pk = nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(&[1; 32]).unwrap(); + let x25519_pk = nym_crypto::asymmetric::x25519::PublicKey::from_bytes(&[2; 32]).unwrap(); + + let skimmed_node = nym_validator_client::nym_api::SkimmedNode { + node_id: 1, + ed25519_identity_pubkey: ed25519_pk, + ip_addresses: vec!["1.1.1.1".parse().unwrap()], + mix_port: 1789, + x25519_sphinx_pubkey: x25519_pk, + role: nym_validator_client::nym_nodes::NodeRole::Mixnode { layer: 1 }, + supported_roles: nym_validator_client::models::DeclaredRoles { + entry: false, + mixnode: true, + exit_nr: false, + exit_ipr: false, + }, + entry: None, + performance: nym_contracts_common::Percent::from_percentage_value(100).unwrap(), + }; + + let record = crate::db::models::NymNodeInsertRecord::new(skimmed_node, None, None).unwrap(); + + assert_eq!(record.node_id, 1); + assert_eq!( + record.ed25519_identity_pubkey, + ed25519_pk.to_base58_string() + ); + assert_eq!(record.total_stake, 0); + assert_eq!(record.ip_addresses, serde_json::json!(["1.1.1.1"])); + assert_eq!(record.mix_port, 1789); + assert_eq!(record.x25519_sphinx_pubkey, x25519_pk.to_base58_string()); + assert_eq!( + record.node_role, + serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { layer: 1 }) + ); + assert_eq!( + record.supported_roles, + serde_json::json!(nym_validator_client::models::DeclaredRoles { + entry: false, + mixnode: true, + exit_nr: false, + exit_ipr: false, + }) + ); + assert_eq!(record.performance, "1"); + assert!(record.entry.is_none()); + assert!(record.self_described.is_none()); + assert!(record.bond_info.is_none()); +} + +#[test] +fn test_nym_node_insert_record_with_entry() { + let ed25519_pk = nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(&[1; 32]).unwrap(); + let x25519_pk = nym_crypto::asymmetric::x25519::PublicKey::from_bytes(&[2; 32]).unwrap(); + + let skimmed_node = nym_validator_client::nym_api::SkimmedNode { + node_id: 1, + ed25519_identity_pubkey: ed25519_pk, + ip_addresses: vec!["1.1.1.1".parse().unwrap()], + mix_port: 1789, + x25519_sphinx_pubkey: x25519_pk, + role: nym_validator_client::nym_nodes::NodeRole::EntryGateway, + supported_roles: nym_validator_client::models::DeclaredRoles { + entry: true, + mixnode: false, + exit_nr: true, + exit_ipr: false, + }, + entry: Some(nym_validator_client::nym_nodes::BasicEntryInformation { + hostname: Some("gateway.example.com".to_string()), + ws_port: 9001, + wss_port: Some(9002), + }), + performance: nym_contracts_common::Percent::from_percentage_value(99).unwrap(), + }; + + let record = crate::db::models::NymNodeInsertRecord::new(skimmed_node, None, None).unwrap(); + + assert_eq!(record.node_id, 1); + assert_eq!(record.total_stake, 0); // No bond info provided + assert!(record.entry.is_some()); + assert!(record.self_described.is_none()); + assert!(record.bond_info.is_none()); + assert!(record.last_updated_utc > 0); +} + +#[test] +fn test_gateway_dto_with_null_values() { + let gateway_dto = crate::db::models::GatewayDto { + gateway_identity_key: "test_identity".to_string(), + bonded: false, + performance: 0, + self_described: None, + explorer_pretty_bond: None, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: None, + last_updated_utc: 0, + moniker: "".to_string(), + security_contact: "".to_string(), + details: "".to_string(), + website: "".to_string(), + }; + + let http_gateway: crate::http::models::Gateway = gateway_dto.try_into().unwrap(); + + assert_eq!(http_gateway.gateway_identity_key, "test_identity"); + assert!(!http_gateway.bonded); + assert_eq!(http_gateway.performance, 0); + assert!(http_gateway.self_described.is_none()); + assert!(http_gateway.explorer_pretty_bond.is_none()); + assert!(http_gateway.last_probe_result.is_none()); + assert!(http_gateway.last_probe_log.is_none()); + assert!(http_gateway.last_testrun_utc.is_none()); + assert_eq!(http_gateway.last_updated_utc, "1970-01-01T00:00:00Z"); +} + +#[test] +fn test_mixnode_dto_with_invalid_json() { + let mixnode_dto = crate::db::models::MixnodeDto { + mix_id: 1, + bonded: true, + is_dp_delegatee: false, + total_stake: 1000000, + full_details: "invalid json".to_string(), + self_described: Some("also invalid".to_string()), + last_updated_utc: 1672531200, + moniker: "moniker".to_string(), + website: "website".to_string(), + security_contact: "contact".to_string(), + details: "details".to_string(), + }; + + let http_mixnode: crate::http::models::Mixnode = mixnode_dto.try_into().unwrap(); + + // Invalid JSON should result in None + assert!(http_mixnode.full_details.is_none()); + assert_eq!(http_mixnode.self_described, Some(serde_json::Value::Null)); +} + +#[test] +fn test_summary_history_dto_with_invalid_json() { + let summary_history_dto = crate::db::models::SummaryHistoryDto { + id: 1, + date: "2023-01-01".to_string(), + value_json: "not valid json".to_string(), + timestamp_utc: 1672531200, + }; + + let summary_history: crate::http::models::SummaryHistory = + summary_history_dto.try_into().unwrap(); + + assert_eq!(summary_history.date, "2023-01-01"); + // Invalid JSON should result in default (null) + assert!(summary_history.value_json.is_null()); +} + +#[test] +fn test_gateway_sessions_record_with_all_none() { + let gateway_sessions_record = crate::db::models::GatewaySessionsRecord { + gateway_identity_key: "test_identity".to_string(), + node_id: 1, + day: time::macros::date!(2023 - 01 - 01), + unique_active_clients: 0, + session_started: 0, + users_hashes: None, + vpn_sessions: None, + mixnet_sessions: None, + unknown_sessions: None, + }; + + let session_stats: crate::http::models::SessionStats = + gateway_sessions_record.try_into().unwrap(); + + assert_eq!(session_stats.gateway_identity_key, "test_identity"); + assert_eq!(session_stats.node_id, 1); + assert_eq!(session_stats.unique_active_clients, 0); + assert_eq!(session_stats.session_started, 0); + assert!(session_stats.users_hashes.is_none()); + assert!(session_stats.vpn_sessions.is_none()); + assert!(session_stats.mixnet_sessions.is_none()); + assert!(session_stats.unknown_sessions.is_none()); +} + +#[test] +fn test_scraper_node_info_contact_addresses() { + use crate::db::models::{ScrapeNodeKind, ScraperNodeInfo}; + + let node_info = ScraperNodeInfo { + node_kind: ScrapeNodeKind::MixingNymNode { node_id: 123 }, + hosts: vec!["1.1.1.1".to_string(), "example.com".to_string()], + http_api_port: 8080, + }; + + let addresses = node_info.contact_addresses(); + + // Should generate multiple URLs for each host + // Custom port (8080) should be inserted at the beginning + assert!(addresses.contains(&"http://1.1.1.1:8080".to_string())); + assert!(addresses.contains(&"http://example.com:8080".to_string())); + assert!(addresses.contains(&"http://1.1.1.1:8000".to_string())); + assert!(addresses.contains(&"https://1.1.1.1".to_string())); + assert!(addresses.contains(&"http://example.com:8000".to_string())); + // Check that URLs follow the expected pattern + assert!(addresses.len() >= 8); // At least 4 URLs per host +} + +#[test] +fn test_scrape_node_kind_node_id() { + use crate::db::models::ScrapeNodeKind; + + let legacy = ScrapeNodeKind::LegacyMixnode { mix_id: 42 }; + assert_eq!(*legacy.node_id(), 42); + + let mixing = ScrapeNodeKind::MixingNymNode { node_id: 123 }; + assert_eq!(*mixing.node_id(), 123); + + let entry_exit = ScrapeNodeKind::EntryExitNymNode { + node_id: 456, + identity_key: "key123".to_string(), + }; + assert_eq!(*entry_exit.node_id(), 456); +} + +#[test] +fn test_nym_node_dto_with_invalid_keys() { + let nym_node_dto = crate::db::models::NymNodeDto { + node_id: 1, + ed25519_identity_pubkey: "invalid_base58".to_string(), + total_stake: 1000000, + ip_addresses: serde_json::json!(["1.1.1.1"]), + mix_port: 1789, + x25519_sphinx_pubkey: "also_invalid".to_string(), + node_role: serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { + layer: 1 + }), + supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRoles { + entry: false, + mixnode: true, + exit_nr: false, + exit_ipr: false, + }), + entry: None, + performance: "1.0".to_string(), + self_described: None, + bond_info: None, + }; + + let result: Result = nym_node_dto.try_into(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("ed25519_identity_pubkey")); +} + +#[test] +fn test_nym_node_dto_with_invalid_performance() { + let ed25519_pk = nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(&[1; 32]).unwrap(); + let x25519_pk = nym_crypto::asymmetric::x25519::PublicKey::from_bytes(&[2; 32]).unwrap(); + + let nym_node_dto = crate::db::models::NymNodeDto { + node_id: 1, + ed25519_identity_pubkey: ed25519_pk.to_base58_string(), + total_stake: 1000000, + ip_addresses: serde_json::json!(["1.1.1.1"]), + mix_port: 1789, + x25519_sphinx_pubkey: x25519_pk.to_base58_string(), + node_role: serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { + layer: 1 + }), + supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRoles { + entry: false, + mixnode: true, + exit_nr: false, + exit_ipr: false, + }), + entry: None, + performance: "invalid_percent".to_string(), + self_described: None, + bond_info: None, + }; + + let result: Result = nym_node_dto.try_into(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("can't parse Percent")); +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/country.rs b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/country.rs new file mode 100644 index 0000000000..290feff8e7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/country.rs @@ -0,0 +1,86 @@ +use axum::{ + extract::{Path, State}, + Json, Router, +}; +use itertools::Itertools; +use serde::Deserialize; +use tracing::instrument; +use utoipa::IntoParams; + +use crate::http::{api::dvpn::MIN_SUPPORTED_VERSION, models::DVpnGateway}; +use crate::http::{error::HttpResult, state::AppState}; + +pub(crate) fn routes() -> Router { + Router::new() + .route( + "/country/:two_letter_country_code", + axum::routing::get(get_gateways_by_country), + ) + .route("/countries", axum::routing::get(get_gateway_countries)) +} + +#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +pub(crate) struct TwoLetterCountryCodeParam { + #[param(min_length = 2, max_length = 2)] + #[param(value_type = String)] + #[serde(rename = "two_letter_country_code")] + pub(crate) country: celes::Country, +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + params( + TwoLetterCountryCodeParam + ), + path = "/country/{two_letter_country_code}", + summary = "Gets available gateways from the Nym network directory by country", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getGatewaysByCountry", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state), fields(two_letter_country_code = country.alpha2))] +pub async fn get_gateways_by_country( + Path(TwoLetterCountryCodeParam { country }): Path, + state: State, +) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_dvpn_gateway_list(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await + .into_iter() + .filter(|gw| gw.location.two_letter_iso_country_code.to_uppercase() == country.alpha2) + .collect(), + )) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + path = "/countries", + summary = "Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getGatewayCountries", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn get_gateway_countries(state: State) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_dvpn_gateway_list(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await + .into_iter() + .map(|gw| gw.location.two_letter_iso_country_code.to_string()) + // dedup relies on iterator being sorted by country, but we already do that + .dedup() + .collect(), + )) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/entry.rs b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/entry.rs new file mode 100644 index 0000000000..0de1487f6e --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/entry.rs @@ -0,0 +1,101 @@ +use crate::http::{ + api::dvpn::{country::TwoLetterCountryCodeParam, MIN_SUPPORTED_VERSION}, + models::DVpnGateway, +}; +use crate::http::{error::HttpResult, state::AppState}; +use axum::{ + extract::{Path, State}, + Json, Router, +}; +use itertools::Itertools; +use tracing::instrument; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/entry", axum::routing::get(get_entry_gateways)) + .route( + "/entry/countries", + axum::routing::get(get_entry_gateway_countries), + ) + .route( + "/entry/country/:two_letter_country_code", + axum::routing::get(get_entry_gateways_by_country), + ) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + path = "/entry", + summary = "Gets available entry gateways from the Nym network directory", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getEntryGateways", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn get_entry_gateways(state: State) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_entry_dvpn_gateways(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await, + )) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + path = "/entry/countries", + summary = "Gets available entry gateway countries as two-letter ISO country codes from the Nym network directory", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getEntryGatewayCountries", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn get_entry_gateway_countries(state: State) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_entry_dvpn_gateways(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await + .into_iter() + .map(|gw| gw.location.two_letter_iso_country_code.to_string()) + // dedup relies on iterator being sorted by country, but we already do that + .dedup() + .collect(), + )) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + params( + TwoLetterCountryCodeParam + ), + path = "/entry/country/{two_letter_country_code}", + summary = "Gets available entry gateways from the Nym network directory by country", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getEntryGatewaysByCountry", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state), fields(two_letter_country_code = country.alpha2))] +pub async fn get_entry_gateways_by_country( + Path(TwoLetterCountryCodeParam { country }): Path, + state: State, +) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_entry_dvpn_gateways(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await + .into_iter() + .filter(|gw| gw.location.two_letter_iso_country_code.to_uppercase() == country.alpha2) + .collect(), + )) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs new file mode 100644 index 0000000000..a512fe8ff4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/exit.rs @@ -0,0 +1,101 @@ +use crate::http::{ + api::dvpn::{country::TwoLetterCountryCodeParam, MIN_SUPPORTED_VERSION}, + models::DVpnGateway, +}; +use crate::http::{error::HttpResult, state::AppState}; +use axum::{ + extract::{Path, State}, + Json, Router, +}; +use itertools::Itertools; +use tracing::instrument; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/exit", axum::routing::get(get_exit_gateways)) + .route( + "/exit/countries", + axum::routing::get(get_entry_gateway_countries), + ) + .route( + "/exit/country/:two_letter_country_code", + axum::routing::get(get_exit_gateways_by_country), + ) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + path = "/exit", + summary = "Gets available exit gateways from the Nym network directory", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getExitGateways", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn get_exit_gateways(state: State) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_exit_dvpn_gateways(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await, + )) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + path = "/exit/countries", + summary = "Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getExitGatewayCountries", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn get_entry_gateway_countries(state: State) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_exit_dvpn_gateways(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await + .into_iter() + .map(|gw| gw.location.two_letter_iso_country_code.to_string()) + // dedup relies on iterator being sorted by country, but we already do that + .dedup() + .collect(), + )) +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + params( + TwoLetterCountryCodeParam + ), + path = "/exit/country/{two_letter_country_code}", + summary = "Gets available exit gateways from the Nym network directory by country", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getExitGatewaysByCountry", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state), fields(two_letter_country_code = country.alpha2))] +pub async fn get_exit_gateways_by_country( + Path(TwoLetterCountryCodeParam { country }): Path, + state: State, +) -> HttpResult>> { + Ok(Json( + state + .cache() + .get_exit_dvpn_gateways(state.db_pool(), &MIN_SUPPORTED_VERSION) + .await + .into_iter() + .filter(|gw| gw.location.two_letter_iso_country_code.to_uppercase() == country.alpha2) + .collect(), + )) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/mod.rs new file mode 100644 index 0000000000..5f0f4cbe19 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/dvpn/mod.rs @@ -0,0 +1,100 @@ +use crate::http::state::AppState; +use axum::Router; +use axum::{ + extract::{Query, State}, + Json, +}; +use semver::Version; +use serde::Deserialize; +use std::sync::LazyLock; +use tracing::instrument; +use utoipa::IntoParams; + +use crate::http::error::{HttpError, HttpResult}; +use crate::http::models::DVpnGateway; + +pub mod country; +pub mod entry; +pub mod exit; + +static MIN_SUPPORTED_VERSION: LazyLock = LazyLock::new(|| Version::new(1, 6, 2)); + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(dvpn_gateways)) + .merge(country::routes()) + .merge(entry::routes()) + .merge(exit::routes()) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct MinNodeVersionQuery { + min_node_version: Option, +} + +#[utoipa::path( + tag = "dVPN Directory Cache", + get, + params( + MinNodeVersionQuery + ), + path = "", + summary = "Gets available entry and exit gateways from the Nym network directory", + context_path = "/dvpn/v1/directory/gateways", + operation_id = "getGateways", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +pub async fn dvpn_gateways( + Query(MinNodeVersionQuery { min_node_version }): Query, + state: State, +) -> HttpResult>> { + let min_node_version = match min_node_version { + Some(min_version) => semver::Version::parse(&min_version) + .map_err(|_| HttpError::invalid_input("Min version must be valid semver"))?, + None => MIN_SUPPORTED_VERSION.clone(), + }; + + Ok(Json( + state + .cache() + .get_dvpn_gateway_list(state.db_pool(), &min_node_version) + .await, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_routes_construction() { + let router = routes(); + // Verify the router builds without panic + let _routes = router; + } + + #[test] + fn test_min_node_version_query_deserialization() { + // Test with version + let json = r#"{"min_node_version": "1.2.3"}"#; + let query: MinNodeVersionQuery = serde_json::from_str(json).unwrap(); + assert_eq!(query.min_node_version, Some("1.2.3".to_string())); + + // Test without version + let json_empty = r#"{}"#; + let query_empty: MinNodeVersionQuery = serde_json::from_str(json_empty).unwrap(); + assert_eq!(query_empty.min_node_version, None); + } + + #[test] + fn test_min_supported_version() { + // Test that the lazy static initializes correctly + assert_eq!(MIN_SUPPORTED_VERSION.major, 1); + assert_eq!(MIN_SUPPORTED_VERSION.minor, 6); + assert_eq!(MIN_SUPPORTED_VERSION.patch, 2); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs index 80e870f198..904bcce05f 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/gateways.rs @@ -57,21 +57,7 @@ async fn gateways_skinny( ) -> HttpResult>> { let db = state.db_pool(); let res = state.cache().get_gateway_list(db).await; - let res: Vec = res - .iter() - .filter(|g| g.bonded) - .map(|g| GatewaySkinny { - gateway_identity_key: g.gateway_identity_key.clone(), - self_described: g.self_described.clone(), - performance: g.performance, - explorer_pretty_bond: g.explorer_pretty_bond.clone(), - last_probe_result: g.last_probe_result.clone(), - last_testrun_utc: g.last_testrun_utc.clone(), - last_updated_utc: g.last_updated_utc.clone(), - routing_score: g.routing_score, - config_score: g.config_score, - }) - .collect(); + let res: Vec = filter_bonded_gateways_to_skinny(res); Ok(Json(PagedResult::paginate(pagination, res))) } @@ -108,3 +94,126 @@ async fn get_gateway( None => Err(HttpError::invalid_input(identity_key)), } } + +// Extract filtering logic for testing +fn filter_bonded_gateways_to_skinny(gateways: Vec) -> Vec { + gateways + .iter() + .filter(|g| g.bonded) + .map(|g| GatewaySkinny { + gateway_identity_key: g.gateway_identity_key.clone(), + self_described: g.self_described.clone(), + performance: g.performance, + explorer_pretty_bond: g.explorer_pretty_bond.clone(), + last_probe_result: g.last_probe_result.clone(), + last_testrun_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + routing_score: g.routing_score, + config_score: g.config_score, + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::models::Gateway; + use nym_node_requests::api::v1::node::models::NodeDescription; + + fn create_test_gateway(identity_key: &str, bonded: bool, performance: u8) -> Gateway { + Gateway { + gateway_identity_key: identity_key.to_string(), + bonded, + performance, + self_described: Some(serde_json::json!({"test": "data"})), + explorer_pretty_bond: Some(serde_json::json!({"bond": "info"})), + description: NodeDescription { + moniker: "Test Gateway".to_string(), + website: "".to_string(), + security_contact: "".to_string(), + details: "".to_string(), + }, + last_probe_result: Some(serde_json::json!({"result": "ok"})), + last_probe_log: None, + last_testrun_utc: Some("2024-01-20T10:00:00Z".to_string()), + last_updated_utc: "2024-01-20T11:00:00Z".to_string(), + routing_score: 0.95, + config_score: 100, + } + } + + #[test] + fn test_filter_bonded_gateways_to_skinny_empty_list() { + let gateways = vec![]; + let result = filter_bonded_gateways_to_skinny(gateways); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_filter_bonded_gateways_to_skinny_all_bonded() { + let gateways = vec![ + create_test_gateway("gw1", true, 90), + create_test_gateway("gw2", true, 95), + create_test_gateway("gw3", true, 85), + ]; + + let result = filter_bonded_gateways_to_skinny(gateways); + assert_eq!(result.len(), 3); + assert_eq!(result[0].gateway_identity_key, "gw1"); + assert_eq!(result[1].gateway_identity_key, "gw2"); + assert_eq!(result[2].gateway_identity_key, "gw3"); + } + + #[test] + fn test_filter_bonded_gateways_to_skinny_mixed() { + let gateways = vec![ + create_test_gateway("gw1", true, 90), + create_test_gateway("gw2", false, 95), + create_test_gateway("gw3", true, 85), + create_test_gateway("gw4", false, 100), + ]; + + let result = filter_bonded_gateways_to_skinny(gateways); + assert_eq!(result.len(), 2); + assert_eq!(result[0].gateway_identity_key, "gw1"); + assert_eq!(result[1].gateway_identity_key, "gw3"); + } + + #[test] + fn test_filter_bonded_gateways_to_skinny_none_bonded() { + let gateways = vec![ + create_test_gateway("gw1", false, 90), + create_test_gateway("gw2", false, 95), + ]; + + let result = filter_bonded_gateways_to_skinny(gateways); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_gateway_to_skinny_conversion() { + let gateway = create_test_gateway("test_gw", true, 98); + let gateways = vec![gateway.clone()]; + + let result = filter_bonded_gateways_to_skinny(gateways); + assert_eq!(result.len(), 1); + + let skinny = &result[0]; + assert_eq!(skinny.gateway_identity_key, gateway.gateway_identity_key); + assert_eq!(skinny.performance, gateway.performance); + assert_eq!(skinny.self_described, gateway.self_described); + assert_eq!(skinny.explorer_pretty_bond, gateway.explorer_pretty_bond); + assert_eq!(skinny.last_probe_result, gateway.last_probe_result); + assert_eq!(skinny.last_testrun_utc, gateway.last_testrun_utc); + assert_eq!(skinny.last_updated_utc, gateway.last_updated_utc); + assert_eq!(skinny.routing_score, gateway.routing_score); + assert_eq!(skinny.config_score, gateway.config_score); + } + + #[test] + fn test_identity_key_param_deserialization() { + let json = r#"{"identity_key": "test_key_123"}"#; + let param: IdentityKeyParam = serde_json::from_str(json).unwrap(); + assert_eq!(param.identity_key, "test_key_123"); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs index 8703f92830..a3da492dcb 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs @@ -8,3 +8,15 @@ pub(crate) fn routes() -> Router { Router::new().nest("/sessions", sessions::routes()) //eventually add other metrics type } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_routes_construction() { + let router = routes(); + // Verify the router builds without panic + let _routes = router; + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs index 409c042753..6c229e94b4 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs @@ -77,7 +77,7 @@ async fn get_all_sessions( }; Ok(Json(PagedResult::paginate( - Pagination { size, page }, + Pagination::new(size, page), day_and_node_filtered, ))) } diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs index cbcfe1fb85..ed18bcdb6a 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs @@ -64,17 +64,11 @@ async fn get_mixnodes( Path(MixIdParam { mix_id }): Path, State(state): State, ) -> HttpResult> { - match mix_id.parse::() { - Ok(parsed_mix_id) => { - let res = state.cache().get_mixnodes_list(state.db_pool()).await; - - match res.iter().find(|item| item.mix_id == parsed_mix_id) { - Some(res) => Ok(Json(res.clone())), - None => Err(HttpError::invalid_input(mix_id)), - } - } - Err(_e) => Err(HttpError::invalid_input(mix_id)), - } + find_mixnode_by_id( + &mix_id, + state.cache().get_mixnodes_list(state.db_pool()).await, + ) + .map(Json) } #[derive(Deserialize, IntoParams)] @@ -99,10 +93,7 @@ async fn get_stats( Query(MixStatsQueryParams { offset }): Query, State(state): State, ) -> HttpResult>> { - let offset: usize = offset - .unwrap_or(0) - .try_into() - .map_err(|_| HttpError::invalid_input("Offset must be non-negative"))?; + let offset = validate_offset(offset)?; let last_30_days = state .cache() .get_mixnode_stats(state.db_pool(), offset) @@ -110,3 +101,146 @@ async fn get_stats( Ok(Json(last_30_days)) } + +// Extract business logic for testing +fn find_mixnode_by_id(mix_id: &str, mixnodes: Vec) -> HttpResult { + match mix_id.parse::() { + Ok(parsed_mix_id) => mixnodes + .into_iter() + .find(|item| item.mix_id == parsed_mix_id) + .ok_or_else(|| HttpError::invalid_input(mix_id)), + Err(_e) => Err(HttpError::invalid_input(mix_id)), + } +} + +fn validate_offset(offset: Option) -> HttpResult { + offset + .unwrap_or(0) + .try_into() + .map_err(|_| HttpError::invalid_input("Offset must be non-negative")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::models::{DailyStats, Mixnode}; + use nym_node_requests::api::v1::node::models::NodeDescription; + + fn create_test_mixnode(mix_id: u32, is_dp_delegatee: bool) -> Mixnode { + Mixnode { + mix_id, + bonded: true, + is_dp_delegatee, + total_stake: 100000, + full_details: Some(serde_json::json!({"test": "data"})), + self_described: Some(serde_json::json!({"version": "1.0"})), + description: NodeDescription { + moniker: format!("Mixnode {mix_id}"), + website: "".to_string(), + security_contact: "".to_string(), + details: "".to_string(), + }, + last_updated_utc: "2024-01-20T10:00:00Z".to_string(), + } + } + + #[test] + fn test_routes_construction() { + let router = routes(); + // Just verify the router builds without panic + // Actual route testing would require integration tests + let _routes = router; + } + + #[test] + fn test_find_mixnode_by_id_success() { + let mixnodes = vec![ + create_test_mixnode(1, false), + create_test_mixnode(42, true), + create_test_mixnode(100, false), + ]; + + let result = find_mixnode_by_id("42", mixnodes).unwrap(); + assert_eq!(result.mix_id, 42); + assert!(result.is_dp_delegatee); + } + + #[test] + fn test_find_mixnode_by_id_not_found() { + let mixnodes = vec![create_test_mixnode(1, false), create_test_mixnode(2, false)]; + + let result = find_mixnode_by_id("99", mixnodes); + assert!(result.is_err()); + } + + #[test] + fn test_find_mixnode_by_id_invalid_format() { + let mixnodes = vec![create_test_mixnode(1, false)]; + + // Test various invalid formats + assert!(find_mixnode_by_id("abc", mixnodes.clone()).is_err()); + assert!(find_mixnode_by_id("", mixnodes.clone()).is_err()); + assert!(find_mixnode_by_id("12.34", mixnodes.clone()).is_err()); + assert!(find_mixnode_by_id("-1", mixnodes).is_err()); + } + + #[test] + fn test_find_mixnode_by_id_edge_cases() { + let mixnodes = vec![ + create_test_mixnode(0, false), + create_test_mixnode(u32::MAX, false), + ]; + + assert!(find_mixnode_by_id("0", mixnodes.clone()).is_ok()); + assert!(find_mixnode_by_id(&u32::MAX.to_string(), mixnodes).is_ok()); + } + + #[test] + fn test_validate_offset_valid() { + assert_eq!(validate_offset(None).unwrap(), 0); + assert_eq!(validate_offset(Some(0)).unwrap(), 0); + assert_eq!(validate_offset(Some(10)).unwrap(), 10); + assert_eq!(validate_offset(Some(1000)).unwrap(), 1000); + } + + #[test] + fn test_validate_offset_invalid() { + assert!(validate_offset(Some(-1)).is_err()); + assert!(validate_offset(Some(-100)).is_err()); + assert!(validate_offset(Some(i64::MIN)).is_err()); + } + + #[test] + fn test_mix_id_param_deserialization() { + let json = r#"{"mix_id": "123"}"#; + let param: MixIdParam = serde_json::from_str(json).unwrap(); + assert_eq!(param.mix_id, "123"); + } + + #[test] + fn test_mix_stats_query_params_deserialization() { + let json = r#"{"offset": 50}"#; + let params: MixStatsQueryParams = serde_json::from_str(json).unwrap(); + assert_eq!(params.offset, Some(50)); + + let json_empty = r#"{}"#; + let params_empty: MixStatsQueryParams = serde_json::from_str(json_empty).unwrap(); + assert_eq!(params_empty.offset, None); + } + + #[test] + fn test_daily_stats_creation() { + let stats = DailyStats { + date_utc: "2024-01-20".to_string(), + total_packets_received: 1000000, + total_packets_sent: 999000, + total_packets_dropped: 1000, + total_stake: 5000000, + }; + + assert_eq!(stats.total_packets_received, 1000000); + assert_eq!(stats.total_packets_sent, 999000); + assert_eq!(stats.total_packets_dropped, 1000); + assert_eq!(stats.total_stake, 5000000); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs index 1ba2711ec3..bc0a37d34e 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs @@ -1,17 +1,20 @@ use anyhow::anyhow; use axum::{response::Redirect, Router}; +use nym_http_api_common::middleware::logging::log_request_debug; use tokio::net::ToSocketAddrs; -use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use tower_http::cors::CorsLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; use crate::http::{server::HttpServer, state::AppState}; +pub(crate) mod dvpn; pub(crate) mod gateways; pub(crate) mod metrics; pub(crate) mod mixnodes; pub(crate) mod nym_nodes; pub(crate) mod services; +pub(crate) mod status; pub(crate) mod summary; pub(crate) mod testruns; @@ -37,9 +40,17 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()) - .nest("/metrics", metrics::routes()), + .nest("/metrics", metrics::routes()) + .nest("/status", status::routes()), + ) + .nest( + "/explorer/v3", + Router::new().nest("/nym-nodes", nym_nodes::routes()), + ) + .nest( + "/dvpn/v1", + Router::new().nest("/directory/gateways", dvpn::routes()), ) - .nest("/v3", Router::new().nest("/nym-nodes", nym_nodes::routes())) .nest( "/internal", Router::new().nest("/testruns", testruns::routes()), @@ -62,7 +73,7 @@ impl RouterBuilder { // CORS layer needs to wrap other API layers .layer(setup_cors()) // logger should be outermost layer - .layer(TraceLayer::new_for_http()) + .layer(axum::middleware::from_fn(log_request_debug)) } } @@ -90,3 +101,38 @@ fn setup_cors() -> CorsLayer { .allow_headers(tower_http::cors::Any) .allow_credentials(false) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cors_configuration() { + let cors = setup_cors(); + + // Test that CORS is configured (this tests that the function returns a valid CorsLayer) + // The actual CORS behavior would need integration tests + let _layer = cors; // This ensures the cors layer is valid + } + + #[test] + fn test_router_builder_creates_routes() { + let router_builder = RouterBuilder::with_default_routes(); + + // Test that the router builder has the expected structure + // The router itself is private, but we can test that the builder is created + let unfinished_router = router_builder.unfinished_router; + + // Convert to a testable format - this will compile only if routes are properly configured + let _test_router = unfinished_router; + } + + #[test] + fn test_router_builder_finalize() { + let router_builder = RouterBuilder::with_default_routes(); + let finalized = router_builder.finalize_routes(); + + // This tests that finalize_routes produces a valid Router + let _router = finalized; + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs index 796760de41..f829082ed4 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/nym_nodes.rs @@ -1,32 +1,41 @@ use axum::{ - extract::{Query, State}, + extract::{Path, Query, State}, Json, Router, }; +use nym_validator_client::client::NodeId; +use serde::Deserialize; use tracing::instrument; +use utoipa::IntoParams; use crate::http::{ error::{HttpError, HttpResult}, - models::ExtendedNymNode, + models::{ExtendedNymNode, NodeDelegation}, state::AppState, PagedResult, Pagination, }; pub(crate) fn routes() -> Router { - Router::new().route("/", axum::routing::get(nym_nodes)) + Router::new() + .route("/", axum::routing::get(nym_nodes)) + .route( + "/:node_id/delegations", + axum::routing::get(node_delegations), + ) } #[utoipa::path( - tag = "Nym Nodes", + tag = "Nym Explorer", get, params( Pagination ), - path = "/v3/nym-nodes", + path = "/nym-nodes", + context_path = "/explorer/v3", responses( (status = 200, body = PagedResult) ) )] -#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))] +#[instrument(level = tracing::Level::INFO, skip_all, fields(page=pagination.page, size=pagination.size))] async fn nym_nodes( Query(pagination): Query, State(state): State, @@ -45,3 +54,65 @@ async fn nym_nodes( Ok(Json(PagedResult::paginate(pagination, nodes))) } + +#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct NodeIdParam { + #[param(minimum = 0)] + node_id: NodeId, +} + +#[utoipa::path( + tag = "Nym Explorer", + get, + params( + NodeIdParam + ), + path = "/{node_id}/delegations", + context_path = "/explorer/v3/nym-nodes", + responses( + (status = 200, body = NodeDelegation) + ) +)] +#[instrument(level = tracing::Level::INFO, skip(state))] +async fn node_delegations( + Path(node_id): Path, + State(state): State, +) -> HttpResult>> { + state + .node_delegations(node_id) + .await + .ok_or_else(|| HttpError::no_delegations_for_node(node_id)) + .map(Json) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_routes_construction() { + let router = routes(); + // Verify the router builds without panic + let _routes = router; + } + + #[test] + fn test_node_id_param_deserialization() { + // Test valid node ID + let json = r#"{"node_id": 42}"#; + let param: NodeIdParam = serde_json::from_str(json).unwrap(); + assert_eq!(param.node_id, 42); + + // Test zero node ID + let json_zero = r#"{"node_id": 0}"#; + let param_zero: NodeIdParam = serde_json::from_str(json_zero).unwrap(); + assert_eq!(param_zero.node_id, 0); + + // Test max node ID + let json_max = format!(r#"{{"node_id": {}}}"#, u32::MAX); + let param_max: NodeIdParam = serde_json::from_str(&json_max).unwrap(); + assert_eq!(param_max.node_id, u32::MAX); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/services/json_path.rs b/nym-node-status-api/nym-node-status-api/src/http/api/services/json_path.rs index caefc6489d..6169784765 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/services/json_path.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/services/json_path.rs @@ -56,3 +56,87 @@ impl ParsedDetails { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::models::Gateway; + use nym_node_requests::api::v1::node::models::NodeDescription; + use serde_json::json; + + fn create_mock_gateway(self_described: Option) -> Gateway { + Gateway { + gateway_identity_key: "mock_identity".to_string(), + bonded: true, + performance: 100, + self_described, + explorer_pretty_bond: None, + description: NodeDescription { + moniker: "mock_moniker".to_string(), + website: "https://nymtech.net".to_string(), + security_contact: "security@nymtech.net".to_string(), + details: "mock_details".to_string(), + }, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: None, + last_updated_utc: "2025-01-01T12:00:00Z".to_string(), + routing_score: 1.0, + config_score: 100, + } + } + + #[test] + fn test_parse_json_paths() { + let paths = ParseJsonPaths::new().unwrap(); + assert_eq!( + paths.path_ip_address.to_string(), + "$.host_information.ip_address[0]" + ); + assert_eq!( + paths.path_hostname.to_string(), + "$.host_information.hostname" + ); + assert_eq!( + paths.path_service_provider_client_id.to_string(), + "$.network_requester.address" + ); + } + + #[test] + fn test_parsed_details() { + let paths = ParseJsonPaths::new().unwrap(); + + // Test with full data + let gateway1 = create_mock_gateway(Some(json!({ + "host_information": { + "ip_address": ["1.1.1.1"], + "hostname": "nymtech.net" + }, + "network_requester": { + "address": "client_address.sP" + } + }))); + let details1 = ParsedDetails::new(&paths, &gateway1); + assert_eq!(details1.ip_address, Some("1.1.1.1".to_string())); + assert_eq!(details1.hostname, Some("nymtech.net".to_string())); + assert_eq!( + details1.service_provider_client_id, + Some("client_address.sP".to_string()) + ); + + // Test with missing data + let gateway2 = create_mock_gateway(Some(json!({}))); + let details2 = ParsedDetails::new(&paths, &gateway2); + assert_eq!(details2.ip_address, None); + assert_eq!(details2.hostname, None); + assert_eq!(details2.service_provider_client_id, None); + + // Test with no self_described field + let gateway3 = create_mock_gateway(None); + let details3 = ParsedDetails::new(&paths, &gateway3); + assert_eq!(details3.ip_address, None); + assert_eq!(details3.hostname, None); + assert_eq!(details3.service_provider_client_id, None); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/services/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/services/mod.rs index 4a817ab759..7d5b5d012c 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/services/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/services/mod.rs @@ -40,31 +40,39 @@ pub(crate) struct ServicesQueryParams { )] #[instrument(level = tracing::Level::DEBUG, skip(state))] async fn mixnodes( - Query(ServicesQueryParams { - size, - page, - wss, - hostname, - entry, - }): Query, + Query(params): Query, State(state): State, ) -> HttpResult>> { let db = state.db_pool(); let cache = state.cache(); - let show_only_wss = wss.unwrap_or(false); - let show_only_with_hostname = hostname.unwrap_or(false); - let show_entry_gateways_only = entry.unwrap_or(false); - let paths = ParseJsonPaths::new().map_err(|e| { tracing::error!("Invalidly configured ParseJsonPaths: {e}"); HttpError::internal() })?; let res = cache.get_gateway_list(db).await; - let res: Vec = res + let services = gateway_list_to_services(&paths, res, params.clone()); + + Ok(Json(PagedResult::paginate( + Pagination::new(params.size, params.page), + services, + ))) +} + +// Extract the conversion and filtering logic for testing +fn gateway_list_to_services( + paths: &ParseJsonPaths, + gateways: Vec, + params: ServicesQueryParams, +) -> Vec { + let show_only_wss = params.wss.unwrap_or(false); + let show_only_with_hostname = params.hostname.unwrap_or(false); + let show_entry_gateways_only = params.entry.unwrap_or(false); + + gateways .iter() .map(|g| { - let details = ParsedDetails::new(&paths, g); + let details = ParsedDetails::new(paths, g); let s = Service { gateway_identity_key: g.gateway_identity_key.clone(), @@ -86,25 +94,38 @@ async fn mixnodes( (s, f) }) .filter(|(_, f)| { - let mut keep = f.has_network_requester_sp; - - if show_entry_gateways_only { - keep = true; - } - - if show_only_wss { - keep &= f.has_wss; - } - if show_only_with_hostname { - keep &= f.has_hostname; - } - - keep + apply_service_filters( + f, + show_only_wss, + show_only_with_hostname, + show_entry_gateways_only, + ) }) .map(|(s, _)| s) - .collect(); + .collect() +} - Ok(Json(PagedResult::paginate(Pagination { size, page }, res))) +// Extract filter application logic +fn apply_service_filters( + filter: &ServiceFilter, + show_only_wss: bool, + show_only_with_hostname: bool, + show_entry_gateways_only: bool, +) -> bool { + let mut keep = filter.has_network_requester_sp; + + if show_entry_gateways_only { + keep = true; + } + + if show_only_wss { + keep &= filter.has_wss; + } + if show_only_with_hostname { + keep &= filter.has_hostname; + } + + keep } struct ServiceFilter { @@ -132,3 +153,253 @@ impl ServiceFilter { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::models::{Gateway, Service}; + use nym_node_requests::api::v1::node::models::NodeDescription; + use serde_json::json; + + fn create_test_gateway(key: &str, has_wss: bool, has_network_requester: bool) -> Gateway { + let mut self_described = json!({}); + if has_wss { + self_described["mixnet_websockets"] = json!({ "wss_port": 1234 }); + } + if has_network_requester { + // ParsedDetails looks for these specific paths + self_described["host_information"] = json!({ + "ip_address": ["192.168.1.1"], + "hostname": "test.nymtech.net" + }); + self_described["network_requester"] = json!({ + "address": "client123" + }); + } + + Gateway { + gateway_identity_key: key.to_string(), + bonded: true, + performance: 95, + self_described: Some(self_described), + explorer_pretty_bond: None, + description: NodeDescription { + moniker: "Test Gateway".to_string(), + website: "".to_string(), + security_contact: "".to_string(), + details: "".to_string(), + }, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: Some("2024-01-20T10:00:00Z".to_string()), + last_updated_utc: "2024-01-20T11:00:00Z".to_string(), + routing_score: 0.95, + config_score: 100, + } + } + + #[test] + fn test_service_filter() { + // Test with all fields + let service1 = Service { + gateway_identity_key: "1".to_string(), + last_updated_utc: "".to_string(), + routing_score: 1.0, + service_provider_client_id: Some("client_id".to_string()), + ip_address: Some("1.1.1.1".to_string()), + hostname: Some("nymtech.net".to_string()), + mixnet_websockets: Some(json!({ "wss_port": 1234 })), + last_successful_ping_utc: None, + }; + let filter1 = ServiceFilter::new(&service1); + assert!(filter1.has_wss); + assert!(filter1.has_network_requester_sp); + assert!(filter1.has_hostname); + + // Test with no fields + let service2 = Service { + gateway_identity_key: "2".to_string(), + last_updated_utc: "".to_string(), + routing_score: 0.0, + service_provider_client_id: None, + ip_address: None, + hostname: None, + mixnet_websockets: None, + last_successful_ping_utc: None, + }; + let filter2 = ServiceFilter::new(&service2); + assert!(!filter2.has_wss); + assert!(!filter2.has_network_requester_sp); + assert!(!filter2.has_hostname); + + // Test with some fields + let service3 = Service { + gateway_identity_key: "3".to_string(), + last_updated_utc: "".to_string(), + routing_score: 0.5, + service_provider_client_id: Some("".to_string()), + ip_address: None, + hostname: Some("nymtech.net".to_string()), + mixnet_websockets: Some(json!({})), + last_successful_ping_utc: None, + }; + let filter3 = ServiceFilter::new(&service3); + assert!(!filter3.has_wss); + assert!(!filter3.has_network_requester_sp); + assert!(filter3.has_hostname); + } + + #[test] + fn test_apply_service_filters() { + let filter_all = ServiceFilter { + has_wss: true, + has_network_requester_sp: true, + has_hostname: true, + }; + + let filter_none = ServiceFilter { + has_wss: false, + has_network_requester_sp: false, + has_hostname: false, + }; + + // Test default behavior (requires network_requester_sp) + assert!(apply_service_filters(&filter_all, false, false, false)); + assert!(!apply_service_filters(&filter_none, false, false, false)); + + // Test entry gateway mode (accepts all) + assert!(apply_service_filters(&filter_all, false, false, true)); + assert!(apply_service_filters(&filter_none, false, false, true)); + + // Test wss filter + assert!(apply_service_filters(&filter_all, true, false, false)); + assert!(!apply_service_filters(&filter_none, true, false, false)); + + // Test hostname filter + assert!(apply_service_filters(&filter_all, false, true, false)); + assert!(!apply_service_filters(&filter_none, false, true, false)); + + // Test combined filters + assert!(apply_service_filters(&filter_all, true, true, false)); + assert!(!apply_service_filters(&filter_none, true, true, false)); + + // Test entry mode does NOT override other filters - it just sets initial keep=true + // But wss and hostname filters can still exclude items + assert!(!apply_service_filters(&filter_none, true, true, true)); + } + + #[test] + fn test_gateway_list_to_services() { + let paths = ParseJsonPaths::new().unwrap(); + let gateways = vec![ + create_test_gateway("gw1", true, true), + create_test_gateway("gw2", false, true), + create_test_gateway("gw3", true, false), + create_test_gateway("gw4", false, false), + ]; + + // Test no filters - only gateways with network_requester pass + let params = ServicesQueryParams { + size: None, + page: None, + wss: None, + hostname: None, + entry: None, + }; + let services = gateway_list_to_services(&paths, gateways.clone(), params); + assert_eq!(services.len(), 2); // gw1 and gw2 have network_requester + assert!(services.iter().any(|s| s.gateway_identity_key == "gw1")); + assert!(services.iter().any(|s| s.gateway_identity_key == "gw2")); + + // Test entry mode (accepts all) + let params = ServicesQueryParams { + size: None, + page: None, + wss: None, + hostname: None, + entry: Some(true), + }; + let services = gateway_list_to_services(&paths, gateways.clone(), params); + assert_eq!(services.len(), 4); + + // Test wss filter with entry mode + let params = ServicesQueryParams { + size: None, + page: None, + wss: Some(true), + hostname: None, + entry: Some(true), + }; + let services = gateway_list_to_services(&paths, gateways.clone(), params); + assert_eq!(services.len(), 2); // gw1 and gw3 have wss + + // Test hostname filter with entry mode + let params = ServicesQueryParams { + size: None, + page: None, + wss: None, + hostname: Some(true), + entry: Some(true), + }; + let services = gateway_list_to_services(&paths, gateways.clone(), params); + assert_eq!(services.len(), 2); // gw1 and gw2 have hostname + + // Test combined filters + let params = ServicesQueryParams { + size: None, + page: None, + wss: Some(true), + hostname: Some(true), + entry: Some(true), + }; + let services = gateway_list_to_services(&paths, gateways, params); + assert_eq!(services.len(), 1); // Only gw1 has both + assert_eq!(services[0].gateway_identity_key, "gw1"); + } + + #[test] + fn test_services_query_params_defaults() { + let params = ServicesQueryParams { + size: None, + page: None, + wss: None, + hostname: None, + entry: None, + }; + + assert!(!params.wss.unwrap_or(false)); + assert!(!params.hostname.unwrap_or(false)); + assert!(!params.entry.unwrap_or(false)); + } + + #[test] + fn test_service_filter_edge_cases() { + // Test with null wss_port value + let service = Service { + gateway_identity_key: "test".to_string(), + last_updated_utc: "".to_string(), + routing_score: 1.0, + service_provider_client_id: Some("client".to_string()), + ip_address: None, + hostname: None, + mixnet_websockets: Some(json!({ "wss_port": null })), + last_successful_ping_utc: None, + }; + let filter = ServiceFilter::new(&service); + assert!(!filter.has_wss); // null port should be treated as no wss + + // Test with wss_port = 0 + let service2 = Service { + gateway_identity_key: "test2".to_string(), + last_updated_utc: "".to_string(), + routing_score: 1.0, + service_provider_client_id: None, + ip_address: None, + hostname: None, + mixnet_websockets: Some(json!({ "wss_port": 0 })), + last_successful_ping_utc: None, + }; + let filter2 = ServiceFilter::new(&service2); + assert!(filter2.has_wss); // Port 0 is still considered as having wss + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/status.rs b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs new file mode 100644 index 0000000000..a269272e16 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs @@ -0,0 +1,58 @@ +use axum::{extract::State, Json, Router}; +use nym_validator_client::models::BinaryBuildInformationOwned; +use tracing::instrument; + +use crate::http::{ + error::HttpResult, + state::{AppState, HealthInfo}, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/build_information", axum::routing::get(build_information)) + .route("/health", axum::routing::get(health)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/build_information", + context_path = "/v2/status", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn build_information( + State(state): State, +) -> HttpResult> { + let build_info = state.build_information().to_owned(); + + Ok(Json(build_info)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/health", + context_path = "/v2/status", + responses( + (status = 200, body = HealthInfo) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn health(State(state): State) -> HttpResult> { + Ok(Json(state.health())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_routes_construction() { + let router = routes(); + // Verify the router builds without panic + let _routes = router; + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/summary.rs b/nym-node-status-api/nym-node-status-api/src/http/api/summary.rs index 729141509c..3a01ee00d2 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/summary.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/summary.rs @@ -41,3 +41,15 @@ async fn summary_history(State(state): State) -> HttpResult Router { Router::new() .route("/", axum::routing::get(request_testrun)) .route("/:testrun_id", axum::routing::post(submit_testrun)) + .route("/:testrun_id/v2", axum::routing::post(submit_testrun_v2)) .layer(DefaultBodyLimit::max(1024 * 1024 * 5)) } @@ -37,8 +36,8 @@ async fn request_testrun( Json(request): Json, ) -> HttpResult> { // TODO dz log agent's network probe version - authenticate(&request, &state)?; - is_fresh(&request.payload.timestamp)?; + state.authenticate_agent_submission(&request)?; + state.is_fresh(&request.payload.timestamp)?; tracing::debug!("Agent requested testrun"); @@ -80,17 +79,20 @@ async fn request_testrun( #[tracing::instrument(level = "debug", skip_all)] async fn submit_testrun( - Path(submitted_testrun_id): Path, + Path(submitted_testrun_id): Path, State(state): State, Json(submitted_result): Json, ) -> HttpResult { - authenticate(&submitted_result, &state)?; + state.authenticate_agent_submission(&submitted_result)?; let db = state.db_pool(); let mut conn = db .acquire() .await - .map_err(HttpError::internal_with_logging)?; + .map_err(|e| { + tracing::error!(testrun_id = %submitted_testrun_id, error = %e, "Failed to acquire database connection for testrun submission"); + HttpError::internal_with_logging(e) + })?; let assigned_testrun = queries::testruns::get_in_progress_testrun_by_id(&mut conn, submitted_testrun_id) @@ -101,7 +103,10 @@ async fn submit_testrun( submitted_testrun_id, err ); - HttpError::invalid_input("Invalid testrun submitted") + HttpError::invalid_input(format!( + "Testrun {submitted_testrun_id} not found in progress state (may be already completed or expired)" + + )) })?; if Some(submitted_result.payload.assigned_at_utc) != assigned_testrun.last_assigned_utc { tracing::warn!( @@ -109,7 +114,12 @@ async fn submit_testrun( submitted_result.payload.assigned_at_utc, assigned_testrun.last_assigned_utc ); - return Err(HttpError::invalid_input("Invalid testrun submitted")); + return Err(HttpError::invalid_input(format!( + "Testrun {} timestamp mismatch: expected {:?}, got {}", + submitted_testrun_id, + assigned_testrun.last_assigned_utc, + submitted_result.payload.assigned_at_utc + ))); } let gw_identity = db::queries::select_gateway_identity(&mut conn, assigned_testrun.gateway_id) @@ -137,7 +147,7 @@ async fn submit_testrun( queries::testruns::update_gateway_last_probe_log( &mut conn, assigned_testrun.gateway_id, - &submitted_result.payload.probe_result, + submitted_result.payload.probe_result.clone(), ) .await .map_err(HttpError::internal_with_logging)?; @@ -145,7 +155,7 @@ async fn submit_testrun( queries::testruns::update_gateway_last_probe_result( &mut conn, assigned_testrun.gateway_id, - &result, + result, ) .await .map_err(HttpError::internal_with_logging)?; @@ -153,58 +163,165 @@ async fn submit_testrun( .await .map_err(HttpError::internal_with_logging)?; - let created_at = chrono::DateTime::from_timestamp(assigned_testrun.created_utc, 0) - .map(|d| d.to_rfc3339()) - .unwrap_or_default(); + let created_at = unix_timestamp_to_utc_rfc3339(assigned_testrun.created_utc); let last_assigned = assigned_testrun .last_assigned_utc - .and_then(|d| chrono::DateTime::from_timestamp(d, 0)) - .map(|d| d.to_rfc3339()) - .unwrap_or_default(); + .map(unix_timestamp_to_utc_rfc3339) + .unwrap_or_else(|| String::from("never")); tracing::info!( - "✅ Testrun row_id {} for gateway {} complete (last assigned at {}, created at {})", + gateway_id = gw_identity, + last_assigned = last_assigned, + created_at = created_at, + "✅ Testrun row_id {} for gateway complete", assigned_testrun.id, - gw_identity, - last_assigned, - created_at ); Ok(StatusCode::CREATED) } -// TODO dz this should be middleware #[tracing::instrument(level = "debug", skip_all)] -fn authenticate(request: &impl VerifiableRequest, state: &AppState) -> HttpResult<()> { - if !state.is_registered(request.public_key()) { - tracing::warn!("Public key not registered with NS API, rejecting"); - return Err(HttpError::unauthorized()); - }; +async fn submit_testrun_v2( + Path(submitted_testrun_id): Path, + State(state): State, + Json(submission): Json, +) -> HttpResult { + state.authenticate_agent_submission(&submission)?; - request.verify_signature().map_err(|_| { - tracing::warn!("Signature verification failed, rejecting"); - HttpError::unauthorized() - })?; + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; - Ok(()) -} + // Try to find existing testrun + match queries::testruns::get_testrun_by_id(&mut conn, submitted_testrun_id).await { + Ok(testrun) => { + // Validate it matches the submission + let gw_identity = queries::select_gateway_identity(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; -fn is_fresh(request_time: &i64) -> HttpResult<()> { - // if a request took longer than N minutes to reach NS API, something is very wrong - let freshness_cutoff = chrono::Duration::minutes(1); - let cutoff_timestamp = (now_utc() - freshness_cutoff).timestamp(); - if *request_time < cutoff_timestamp { - tracing::warn!("Request older than {}s, rejecting", cutoff_timestamp); - return Err(HttpError::unauthorized()); + if gw_identity != submission.payload.gateway_identity_key { + tracing::warn!( + "Gateway mismatch for testrun {}: expected {}, got {}", + submitted_testrun_id, + gw_identity, + submission.payload.gateway_identity_key + ); + return Err(HttpError::invalid_input("Gateway identity mismatch")); + } + + // Process normally using existing testrun + process_testrun_submission(testrun, submission.payload, &mut conn).await + } + Err(_) => { + // External testrun - create records + tracing::info!( + "Creating external testrun {} for gateway {}", + submitted_testrun_id, + submission.payload.gateway_identity_key + ); + + // Get or create gateway + let gateway_id = + queries::get_or_create_gateway(&mut conn, &submission.payload.gateway_identity_key) + .await + .map_err(HttpError::internal_with_logging)?; + + // Create testrun + queries::testruns::insert_external_testrun( + &mut conn, + submitted_testrun_id, + gateway_id, + submission.payload.assigned_at_utc, + ) + .await + .map_err(HttpError::internal_with_logging)?; + + // Process submission + process_testrun_submission_by_gateway(gateway_id, submission.payload, &mut conn).await + } } - Ok(()) } fn get_result_from_log(log: &str) -> String { - let re = regex::Regex::new(r"\n\{\s").unwrap(); - let result: Vec<_> = re.splitn(log, 2).collect(); + static RE: std::sync::LazyLock = + std::sync::LazyLock::new(|| regex::Regex::new(r"\n\{\s").expect("Invalid regex pattern")); + + let result: Vec<_> = RE.splitn(log, 2).collect(); if result.len() == 2 { let res = format!("{} {}", "{", result[1]).to_string(); return res; } "".to_string() } + +async fn process_testrun_submission( + testrun: TestRunDto, + payload: submit_results_v2::Payload, + conn: &mut DbConnection, +) -> HttpResult { + // Validate timestamp matches + if Some(payload.assigned_at_utc) != testrun.last_assigned_utc { + tracing::warn!( + "Submitted testrun timestamp mismatch: {} != {:?}, rejecting", + payload.assigned_at_utc, + testrun.last_assigned_utc + ); + return Err(HttpError::invalid_input(format!( + "Testrun timestamp mismatch: expected {:?}, got {}", + testrun.last_assigned_utc, payload.assigned_at_utc + ))); + } + + // Process the submission + process_testrun_submission_by_gateway(testrun.gateway_id, payload, conn).await +} + +async fn process_testrun_submission_by_gateway( + gateway_id: i32, + payload: submit_results_v2::Payload, + conn: &mut DbConnection, +) -> HttpResult { + let gw_identity = &payload.gateway_identity_key; + + tracing::debug!( + "Processing testrun submission for gateway {} ({} bytes)", + gw_identity, + payload.probe_result.len(), + ); + + // Update testrun status to complete + queries::testruns::update_testrun_status_by_gateway(conn, gateway_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + + // Update gateway with results + queries::testruns::update_gateway_last_probe_log( + conn, + gateway_id, + payload.probe_result.clone(), + ) + .await + .map_err(HttpError::internal_with_logging)?; + + let result = get_result_from_log(&payload.probe_result); + queries::testruns::update_gateway_last_probe_result(conn, gateway_id, result) + .await + .map_err(HttpError::internal_with_logging)?; + + queries::testruns::update_gateway_score(conn, gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + + let assigned_at = unix_timestamp_to_utc_rfc3339(payload.assigned_at_utc); + let now = now_utc(); + tracing::info!( + "✅ Testrun for gateway {} complete (assigned at {}, current time {})", + gw_identity, + assigned_at, + now + ); + + Ok(StatusCode::CREATED) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/error.rs b/nym-node-status-api/nym-node-status-api/src/http/error.rs index ed37966742..a58a6341e3 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/error.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/error.rs @@ -1,7 +1,9 @@ +use nym_mixnet_contract_common::NodeId; use std::fmt::Display; pub(crate) type HttpResult = Result; +#[derive(Debug)] pub(crate) struct HttpError { message: String, status: axum::http::StatusCode, @@ -10,14 +12,14 @@ pub(crate) struct HttpError { impl HttpError { pub(crate) fn invalid_input(msg: impl Display) -> Self { Self { - message: serde_json::json!({"message": msg.to_string()}).to_string(), + message: msg.to_string(), status: axum::http::StatusCode::BAD_REQUEST, } } pub(crate) fn unauthorized() -> Self { Self { - message: serde_json::json!({"message": "Make sure your public key is registered with NS API"}).to_string(), + message: String::from("Make sure your public key is registered with NS API"), status: axum::http::StatusCode::UNAUTHORIZED, } } @@ -29,17 +31,24 @@ impl HttpError { pub(crate) fn internal() -> Self { Self { - message: serde_json::json!({"message": "Internal server error"}).to_string(), + message: String::from("Internal server error"), status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, } } pub(crate) fn no_testruns_available() -> Self { Self { - message: serde_json::json!({"message": "No testruns available"}).to_string(), + message: String::from("No testruns available"), status: axum::http::StatusCode::SERVICE_UNAVAILABLE, } } + + pub(crate) fn no_delegations_for_node(node_id: NodeId) -> Self { + Self { + message: format!("No delegation data for node_id={node_id}"), + status: axum::http::StatusCode::NOT_FOUND, + } + } } impl axum::response::IntoResponse for HttpError { @@ -47,3 +56,103 @@ impl axum::response::IntoResponse for HttpError { (self.status, self.message).into_response() } } + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::StatusCode; + use axum::response::IntoResponse; + + #[test] + fn test_invalid_input_error() { + let error = HttpError::invalid_input("Invalid request data"); + assert_eq!(error.message, "Invalid request data"); + assert_eq!(error.status, StatusCode::BAD_REQUEST); + + // Test with different input types + let error2 = HttpError::invalid_input(42); + assert_eq!(error2.message, "42"); + + let error3 = HttpError::invalid_input(String::from("Dynamic string")); + assert_eq!(error3.message, "Dynamic string"); + } + + #[test] + fn test_unauthorized_error() { + let error = HttpError::unauthorized(); + assert_eq!( + error.message, + "Make sure your public key is registered with NS API" + ); + assert_eq!(error.status, StatusCode::UNAUTHORIZED); + } + + #[test] + fn test_internal_error() { + let error = HttpError::internal(); + assert_eq!(error.message, "Internal server error"); + assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_internal_with_logging() { + // This would log to error but we can still test the result + let error = HttpError::internal_with_logging("Database connection failed"); + assert_eq!(error.message, "Internal server error"); + assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_no_testruns_available() { + let error = HttpError::no_testruns_available(); + assert_eq!(error.message, "No testruns available"); + assert_eq!(error.status, StatusCode::SERVICE_UNAVAILABLE); + } + + #[test] + fn test_no_delegations_for_node() { + let node_id: NodeId = 42; + let error = HttpError::no_delegations_for_node(node_id); + assert_eq!(error.message, "No delegation data for node_id=42"); + assert_eq!(error.status, StatusCode::NOT_FOUND); + + let node_id_2: NodeId = 999; + let error2 = HttpError::no_delegations_for_node(node_id_2); + assert_eq!(error2.message, "No delegation data for node_id=999"); + } + + #[test] + fn test_into_response() { + let error = HttpError::invalid_input("Test error"); + let response = error.into_response(); + + // Extract status from response + let status = response.status(); + assert_eq!(status, StatusCode::BAD_REQUEST); + } + + #[test] + fn test_different_error_types_into_response() { + // Test each error type converts to response properly + let errors = vec![ + HttpError::invalid_input("test"), + HttpError::unauthorized(), + HttpError::internal(), + HttpError::no_testruns_available(), + HttpError::no_delegations_for_node(1), + ]; + + let expected_statuses = vec![ + StatusCode::BAD_REQUEST, + StatusCode::UNAUTHORIZED, + StatusCode::INTERNAL_SERVER_ERROR, + StatusCode::SERVICE_UNAVAILABLE, + StatusCode::NOT_FOUND, + ]; + + for (error, expected_status) in errors.into_iter().zip(expected_statuses) { + let response = error.into_response(); + assert_eq!(response.status(), expected_status); + } + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/mod.rs index 2fa8373318..7ea53a11d7 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/mod.rs @@ -18,7 +18,7 @@ pub struct PagedResult { impl PagedResult { pub fn paginate(pagination: Pagination, res: Vec) -> Self { let total = res.len(); - let (size, mut page) = pagination.intoto_inner_values(); + let (size, mut page) = pagination.into_inner_values(); if page * size > total { page = total / size; @@ -42,17 +42,201 @@ pub(crate) struct Pagination { page: Option, } +const SIZE_DEFAULT: usize = 10; +const SIZE_MAX: usize = 200; +const PAGE_DEFAULT: usize = 0; + +impl Default for Pagination { + fn default() -> Self { + Self { + size: Some(SIZE_DEFAULT), + page: Some(PAGE_DEFAULT), + } + } +} + impl Pagination { - // unwrap stored values or use predefined defaults - pub(crate) fn intoto_inner_values(self) -> (usize, usize) { - const SIZE_DEFAULT: usize = 10; - const SIZE_MAX: usize = 200; - - const PAGE_DEFAULT: usize = 0; + pub(crate) fn new(size: Option, page: Option) -> Self { + Self { size, page } + } + pub(crate) fn into_inner_values(self) -> (usize, usize) { ( self.size.unwrap_or(SIZE_DEFAULT).min(SIZE_MAX), self.page.unwrap_or(PAGE_DEFAULT), ) } } + +#[cfg(test)] +mod tests { + use super::*; + + // Use a simple type for testing instead of a custom struct + type TestItem = String; + + #[test] + fn test_pagination_default() { + let pagination = Pagination::default(); + let (size, page) = pagination.into_inner_values(); + assert_eq!(size, SIZE_DEFAULT); + assert_eq!(page, PAGE_DEFAULT); + } + + #[test] + fn test_pagination_new() { + let pagination = Pagination::new(Some(50), Some(3)); + let (size, page) = pagination.into_inner_values(); + assert_eq!(size, 50); + assert_eq!(page, 3); + } + + #[test] + fn test_pagination_max_size_limit() { + let pagination = Pagination::new(Some(1000), Some(0)); + let (size, page) = pagination.into_inner_values(); + assert_eq!(size, SIZE_MAX); + assert_eq!(page, 0); + } + + #[test] + fn test_pagination_none_values() { + let pagination = Pagination::new(None, None); + let (size, page) = pagination.into_inner_values(); + assert_eq!(size, SIZE_DEFAULT); + assert_eq!(page, PAGE_DEFAULT); + } + + #[test] + fn test_paged_result_empty_list() { + let items: Vec = vec![]; + let pagination = Pagination::new(Some(10), Some(0)); + let result = PagedResult::paginate(pagination, items); + + assert_eq!(result.page, 0); + assert_eq!(result.size, 10); + assert_eq!(result.total, 0); + assert_eq!(result.items.len(), 0); + } + + #[test] + fn test_paged_result_single_page() { + let items: Vec = (0..5).map(|i| format!("Item {i}")).collect(); + + let pagination = Pagination::new(Some(10), Some(0)); + let result = PagedResult::paginate(pagination, items.clone()); + + assert_eq!(result.page, 0); + assert_eq!(result.size, 10); + assert_eq!(result.total, 5); + assert_eq!(result.items.len(), 5); + assert_eq!(result.items[0], "Item 0"); + assert_eq!(result.items[4], "Item 4"); + } + + #[test] + fn test_paged_result_multiple_pages() { + let items: Vec = (0..25).map(|i| format!("Item {i}")).collect(); + + // First page + let pagination = Pagination::new(Some(10), Some(0)); + let result = PagedResult::paginate(pagination, items.clone()); + assert_eq!(result.page, 0); + assert_eq!(result.size, 10); + assert_eq!(result.total, 25); + assert_eq!(result.items.len(), 10); + assert_eq!(result.items[0], "Item 0"); + assert_eq!(result.items[9], "Item 9"); + + // Second page + let pagination = Pagination::new(Some(10), Some(1)); + let result = PagedResult::paginate(pagination, items.clone()); + assert_eq!(result.page, 1); + assert_eq!(result.size, 10); + assert_eq!(result.total, 25); + assert_eq!(result.items.len(), 10); + assert_eq!(result.items[0], "Item 10"); + assert_eq!(result.items[9], "Item 19"); + + // Last page (partial) + let pagination = Pagination::new(Some(10), Some(2)); + let result = PagedResult::paginate(pagination, items); + assert_eq!(result.page, 2); + assert_eq!(result.size, 10); + assert_eq!(result.total, 25); + assert_eq!(result.items.len(), 5); + assert_eq!(result.items[0], "Item 20"); + assert_eq!(result.items[4], "Item 24"); + } + + #[test] + fn test_paged_result_page_out_of_bounds() { + let items: Vec = (0..15).map(|i| format!("Item {i}")).collect(); + + // Page way out of bounds + let pagination = Pagination::new(Some(10), Some(10)); + let result = PagedResult::paginate(pagination, items); + + // Should adjust to last valid page + assert_eq!(result.page, 1); // 15 items / 10 per page = 1 (0-indexed) + assert_eq!(result.size, 10); + assert_eq!(result.total, 15); + assert_eq!(result.items.len(), 5); + assert_eq!(result.items[0], "Item 10"); + } + + #[test] + fn test_paged_result_exact_page_boundary() { + let items: Vec = (0..20).map(|i| format!("Item {i}")).collect(); + + // Exactly 2 pages of 10 items each + let pagination = Pagination::new(Some(10), Some(1)); + let result = PagedResult::paginate(pagination, items); + + assert_eq!(result.page, 1); + assert_eq!(result.size, 10); + assert_eq!(result.total, 20); + assert_eq!(result.items.len(), 10); + } + + #[test] + fn test_paged_result_single_item_per_page() { + let items: Vec = (0..5).map(|i| format!("Item {i}")).collect(); + + let pagination = Pagination::new(Some(1), Some(3)); + let result = PagedResult::paginate(pagination, items); + + assert_eq!(result.page, 3); + assert_eq!(result.size, 1); + assert_eq!(result.total, 5); + assert_eq!(result.items.len(), 1); + assert_eq!(result.items[0], "Item 3"); + } + + #[test] + fn test_pagination_serialization() { + let pagination = Pagination::new(Some(25), Some(2)); + let json = serde_json::to_string(&pagination).unwrap(); + assert!(json.contains("\"size\":25")); + assert!(json.contains("\"page\":2")); + + let deserialized: Pagination = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.size, Some(25)); + assert_eq!(deserialized.page, Some(2)); + } + + #[test] + fn test_paged_result_serialization() { + let items = vec!["First".to_string(), "Second".to_string()]; + let pagination = Pagination::new(Some(10), Some(0)); + let result = PagedResult::paginate(pagination, items); + + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("\"page\":0")); + assert!(json.contains("\"size\":10")); + assert!(json.contains("\"total\":2")); + assert!(json.contains("\"items\":")); + assert!(json.contains("First")); + assert!(json.contains("Second")); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index f39decfa73..9ff469eee2 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -1,9 +1,20 @@ -use cosmwasm_std::Decimal; +use std::net::IpAddr; + +use crate::monitor::ExplorerPrettyBond; +use cosmwasm_std::{Addr, Coin, Decimal}; +use nym_mixnet_contract_common::CoinSchema; use nym_node_requests::api::v1::node::models::NodeDescription; -use nym_validator_client::client::NodeId; +use nym_validator_client::{ + client::NodeId, + models::{AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails}, + nym_api::SkimmedNode, + nym_nodes::{BasicEntryInformation, NodeRole}, +}; use serde::{Deserialize, Serialize}; +use tracing::{error, instrument}; use utoipa::ToSchema; +use crate::db::models::NymNodeDataDeHelper; pub(crate) use nym_node_status_client::models::TestrunAssignment; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -22,6 +33,458 @@ pub struct Gateway { pub config_score: u32, } +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct BuildInformation { + pub build_version: String, + pub commit_branch: String, + pub commit_sha: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Location { + pub two_letter_iso_country_code: String, + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct DVpnGateway { + pub identity_key: String, + pub name: String, + pub ip_packet_router: Option, + pub authenticator: Option, + pub location: Location, + pub last_probe: Option, + #[schema(value_type = Vec)] + pub ip_addresses: Vec, + pub mix_port: u16, + pub role: NodeRole, + pub entry: Option, + // The performance data here originates from the nym-api, and is effectively mixnet performance + // at the time of writing this + pub performance: String, + pub build_information: BinaryBuildInformationOwned, +} + +impl DVpnGateway { + pub fn can_route_entry(&self) -> bool { + self.last_probe + .as_ref() + .map(|probe| match &probe.outcome.as_entry { + directory_gw_probe_outcome::Entry::Tested(entry_test_result) => { + entry_test_result.can_route + } + directory_gw_probe_outcome::Entry::NotTested + | directory_gw_probe_outcome::Entry::EntryFailure => false, + }) + .unwrap_or(false) + } + + pub fn can_route_exit(&self) -> bool { + self.last_probe + .as_ref() + .map(|probe| { + probe + .outcome + .as_exit + .as_ref() + .map(|outcome| { + outcome.can_route_ip_external_v4 && outcome.can_route_ip_external_v6 + }) + .unwrap_or(false) + }) + .unwrap_or(false) + } +} + +/// based on +/// https://github.com/nymtech/nym-vpn-client/blob/nym-vpn-core-v1.10.0/nym-vpn-core/crates/nym-gateway-probe/src/types.rs +/// TODO: long term types should be moved into this repo because nym-vpn-client +/// could pull it as a dependency and we'd have a single source of truth +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct LastProbeResult { + node: String, + used_entry: String, + outcome: ProbeOutcome, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct DirectoryGwProbe { + last_updated_utc: String, + outcome: ProbeOutcome, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct ProbeOutcome { + as_entry: directory_gw_probe_outcome::Entry, + as_exit: Option, + wg: Option, +} + +pub mod directory_gw_probe_outcome { + use super::*; + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + #[serde(untagged)] + #[allow(clippy::enum_variant_names)] + pub enum Entry { + Tested(EntryTestResult), + NotTested, + EntryFailure, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub struct EntryTestResult { + pub can_connect: bool, + pub can_route: bool, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub struct Exit { + pub can_connect: bool, + pub can_route_ip_v4: bool, + pub can_route_ip_external_v4: bool, + pub can_route_ip_v6: bool, + pub can_route_ip_external_v6: bool, + } +} + +pub mod wg_outcome_versions { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub struct ProbeOutcomeV1 { + pub can_register: bool, + + pub can_handshake_v4: bool, + pub can_resolve_dns_v4: bool, + pub ping_hosts_performance_v4: f32, + pub ping_ips_performance_v4: f32, + + pub can_handshake_v6: bool, + pub can_resolve_dns_v6: bool, + pub ping_hosts_performance_v6: f32, + pub ping_ips_performance_v6: f32, + + pub download_duration_sec_v4: u64, + pub downloaded_file_v4: String, + pub download_error_v4: String, + + pub download_duration_sec_v6: u64, + pub downloaded_file_v6: String, + pub download_error_v6: String, + } +} + +impl DVpnGateway { + #[instrument(level = tracing::Level::INFO, name = "dvpn_gw_new", skip_all, fields(gateway_key = gateway.gateway_identity_key, node_id = skimmed_node.node_id))] + pub(crate) fn new(gateway: Gateway, skimmed_node: &SkimmedNode) -> anyhow::Result { + let location = gateway + .explorer_pretty_bond + .ok_or_else(|| anyhow::anyhow!("Missing explorer_pretty_bond")) + .and_then(|value| { + serde_json::from_value::(value).map_err(From::from) + }) + .map(|bond| bond.location)?; + + let self_described: NymNodeDataDeHelper = gateway + .self_described + .ok_or_else(|| anyhow::anyhow!("Missing self_described")) + .and_then(|value| { + serde_json::from_value::(value).map_err(From::from) + })?; + + let last_probe_result = match gateway.last_probe_result { + Some(value) => { + let parsed = + serde_json::from_value::(value).inspect_err(|err| { + error!("Failed to deserialize probe result: {err}"); + })?; + Some(parsed) + } + None => None, + }; + + Ok(Self { + identity_key: gateway.gateway_identity_key, + name: gateway.description.moniker, + ip_packet_router: self_described.ip_packet_router, + authenticator: self_described.authenticator, + location: Location { + latitude: location.location.latitude, + longitude: location.location.longitude, + two_letter_iso_country_code: location.two_letter_iso_country_code, + }, + last_probe: last_probe_result.map(|res| DirectoryGwProbe { + last_updated_utc: gateway.last_testrun_utc.unwrap_or_default(), + outcome: res.outcome, + }), + ip_addresses: skimmed_node.ip_addresses.clone(), + mix_port: skimmed_node.mix_port, + role: skimmed_node.role.clone(), + entry: skimmed_node.entry.clone(), + performance: to_percent(gateway.performance), + build_information: self_described.build_information, + }) + } +} + +fn to_percent(performance: u8) -> String { + let fraction = performance as f32 / 100.0; + format!("{fraction:.2}") +} + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + + #[test] + fn to_percent_should_work() { + let starting = [0u8, 33, 50, 99, 100]; + let expected = ["0.00", "0.33", "0.50", "0.99", "1.00"]; + + for (starting, expected) in starting.into_iter().zip(expected) { + assert_eq!(expected, to_percent(starting)); + } + } + + #[test] + fn to_percent_edge_cases() { + // Test edge cases + assert_eq!("0.00", to_percent(0)); + assert_eq!("1.00", to_percent(100)); + assert_eq!("2.55", to_percent(255)); // Over 100% + } + + #[test] + fn node_delegation_from_conversion() { + use cosmwasm_std::Uint128; + + let delegation = nym_mixnet_contract_common::Delegation { + node_id: 42, + amount: Coin { + denom: "unym".to_string(), + amount: Uint128::new(1000000), + }, + cumulative_reward_ratio: Decimal::from_str("1.23456789").unwrap(), + height: 12345, + owner: Addr::unchecked("owner1"), + proxy: Some(Addr::unchecked("proxy1")), + }; + + let node_delegation: NodeDelegation = delegation.clone().into(); + + assert_eq!(node_delegation.amount.denom, "unym"); + assert_eq!(node_delegation.amount.amount, Uint128::new(1000000)); + assert_eq!(node_delegation.cumulative_reward_ratio, "1.23456789"); + assert_eq!(node_delegation.block_height, 12345); + assert_eq!(node_delegation.owner, Addr::unchecked("owner1")); + assert_eq!(node_delegation.proxy, Some(Addr::unchecked("proxy1"))); + } + + #[test] + fn node_delegation_from_conversion_no_proxy() { + use cosmwasm_std::Uint128; + + let delegation = nym_mixnet_contract_common::Delegation { + node_id: 0, + amount: Coin { + denom: "uatom".to_string(), + amount: Uint128::new(0), + }, + cumulative_reward_ratio: Decimal::zero(), + height: 0, + owner: Addr::unchecked("owner2"), + proxy: None, + }; + + let node_delegation: NodeDelegation = delegation.into(); + + assert_eq!(node_delegation.amount.denom, "uatom"); + assert_eq!(node_delegation.amount.amount, Uint128::new(0)); + assert_eq!(node_delegation.cumulative_reward_ratio, "0"); + assert_eq!(node_delegation.block_height, 0); + assert_eq!(node_delegation.owner, Addr::unchecked("owner2")); + assert_eq!(node_delegation.proxy, None); + } + + #[test] + fn node_delegation_from_conversion_max_values() { + use cosmwasm_std::Uint128; + + let delegation = nym_mixnet_contract_common::Delegation { + node_id: u32::MAX, + amount: Coin { + denom: "test".to_string(), + amount: Uint128::MAX, + }, + cumulative_reward_ratio: Decimal::from_str("999999999.999999999").unwrap(), + height: u64::MAX, + owner: Addr::unchecked("owner3"), + proxy: Some(Addr::unchecked("proxy3")), + }; + + let node_delegation: NodeDelegation = delegation.into(); + + assert_eq!(node_delegation.amount.amount, Uint128::MAX); + assert_eq!( + node_delegation.cumulative_reward_ratio, + "999999999.999999999" + ); + assert_eq!(node_delegation.block_height, u64::MAX); + } + + #[test] + fn location_struct_creation() { + let location = Location { + two_letter_iso_country_code: "US".to_string(), + latitude: 40.7128, + longitude: -74.0060, + }; + + assert_eq!(location.two_letter_iso_country_code, "US"); + assert_eq!(location.latitude, 40.7128); + assert_eq!(location.longitude, -74.0060); + } + + #[test] + fn location_extreme_coordinates() { + // Test extreme coordinates + let north_pole = Location { + two_letter_iso_country_code: "XX".to_string(), + latitude: 90.0, + longitude: 0.0, + }; + + let south_pole = Location { + two_letter_iso_country_code: "AQ".to_string(), + latitude: -90.0, + longitude: 0.0, + }; + + let date_line = Location { + two_letter_iso_country_code: "FJ".to_string(), + latitude: -17.0, + longitude: 180.0, + }; + + assert_eq!(north_pole.latitude, 90.0); + assert_eq!(south_pole.latitude, -90.0); + assert_eq!(date_line.longitude, 180.0); + } + + #[test] + fn build_information_creation() { + let build_info = BuildInformation { + build_version: "1.2.3".to_string(), + commit_branch: "main".to_string(), + commit_sha: "abcdef123456".to_string(), + }; + + assert_eq!(build_info.build_version, "1.2.3"); + assert_eq!(build_info.commit_branch, "main"); + assert_eq!(build_info.commit_sha, "abcdef123456"); + } + + #[test] + fn daily_stats_creation() { + let stats = DailyStats { + date_utc: "2024-01-20".to_string(), + total_packets_received: 1_000_000, + total_packets_sent: 999_000, + total_packets_dropped: 1_000, + total_stake: 5_000_000, + }; + + assert_eq!(stats.date_utc, "2024-01-20"); + assert_eq!(stats.total_packets_received, 1_000_000); + assert_eq!(stats.total_packets_sent, 999_000); + assert_eq!(stats.total_packets_dropped, 1_000); + assert_eq!(stats.total_stake, 5_000_000); + } + + #[test] + fn daily_stats_negative_values() { + // Test with edge case values + let stats = DailyStats { + date_utc: "".to_string(), + total_packets_received: i64::MAX, + total_packets_sent: 0, + total_packets_dropped: -1, // Should this be allowed? + total_stake: i64::MIN, + }; + + assert_eq!(stats.date_utc, ""); + assert_eq!(stats.total_packets_received, i64::MAX); + assert_eq!(stats.total_packets_sent, 0); + assert_eq!(stats.total_packets_dropped, -1); + assert_eq!(stats.total_stake, i64::MIN); + } + + #[test] + fn gateway_skinny_creation() { + let gateway = GatewaySkinny { + gateway_identity_key: "gateway123".to_string(), + self_described: Some(serde_json::json!({"test": "value"})), + explorer_pretty_bond: None, + last_probe_result: Some(serde_json::json!({"status": "ok"})), + last_testrun_utc: Some("2024-01-20T10:00:00Z".to_string()), + last_updated_utc: "2024-01-20T11:00:00Z".to_string(), + routing_score: 0.95, + config_score: 100, + performance: 98, + }; + + assert_eq!(gateway.gateway_identity_key, "gateway123"); + assert!(gateway.self_described.is_some()); + assert!(gateway.explorer_pretty_bond.is_none()); + assert_eq!(gateway.performance, 98); + assert_eq!(gateway.routing_score, 0.95); + } + + #[test] + fn service_creation_with_all_fields() { + let service = Service { + gateway_identity_key: "gw123".to_string(), + last_updated_utc: "2024-01-20T10:00:00Z".to_string(), + routing_score: 0.85, + service_provider_client_id: Some("client123".to_string()), + ip_address: Some("192.168.1.1".to_string()), + hostname: Some("gateway.example.com".to_string()), + mixnet_websockets: Some(serde_json::json!({"port": 8080})), + last_successful_ping_utc: Some("2024-01-20T09:55:00Z".to_string()), + }; + + assert_eq!(service.gateway_identity_key, "gw123"); + assert_eq!(service.routing_score, 0.85); + assert_eq!(service.ip_address, Some("192.168.1.1".to_string())); + assert_eq!(service.hostname, Some("gateway.example.com".to_string())); + } + + #[test] + fn service_creation_minimal() { + let service = Service { + gateway_identity_key: "gw456".to_string(), + last_updated_utc: "2024-01-20T10:00:00Z".to_string(), + routing_score: 0.0, + service_provider_client_id: None, + ip_address: None, + hostname: None, + mixnet_websockets: None, + last_successful_ping_utc: None, + }; + + assert_eq!(service.gateway_identity_key, "gw456"); + assert_eq!(service.routing_score, 0.0); + assert!(service.service_provider_client_id.is_none()); + assert!(service.ip_address.is_none()); + assert!(service.hostname.is_none()); + assert!(service.mixnet_websockets.is_none()); + assert!(service.last_successful_ping_utc.is_none()); + } +} + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub struct GatewaySkinny { pub gateway_identity_key: String, @@ -55,13 +518,13 @@ pub(crate) struct ExtendedNymNode { #[schema(value_type = String)] pub(crate) total_stake: Decimal, pub(crate) original_pledge: u128, - pub(crate) bonding_address: String, + pub(crate) bonding_address: Option, pub(crate) bonded: bool, - pub(crate) node_type: String, + pub(crate) node_type: nym_validator_client::models::DescribedNodeType, pub(crate) ip_address: String, pub(crate) accepted_tnc: bool, - pub(crate) self_description: serde_json::Value, - pub(crate) rewarding_details: serde_json::Value, + pub(crate) self_description: nym_validator_client::models::NymNodeData, + pub(crate) rewarding_details: Option, pub(crate) description: NodeDescription, pub(crate) geoip: Option, } @@ -120,3 +583,27 @@ pub struct SessionStats { pub mixnet_sessions: Option, pub unknown_sessions: Option, } + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct NodeDelegation { + #[schema(value_type = CoinSchema)] + pub amount: Coin, + pub cumulative_reward_ratio: String, + pub block_height: u64, + #[schema(value_type = String)] + pub owner: Addr, + #[schema(value_type = Option)] + pub proxy: Option, +} + +impl From for NodeDelegation { + fn from(value: nym_mixnet_contract_common::Delegation) -> Self { + Self { + amount: value.amount, + cumulative_reward_ratio: value.cumulative_reward_ratio.to_string(), + block_height: value.height, + owner: value.owner, + proxy: value.proxy, + } + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/server.rs b/nym-node-status-api/nym-node-status-api/src/http/server.rs index ce5abe2234..f197929d9f 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/server.rs @@ -1,24 +1,28 @@ use axum::Router; use core::net::SocketAddr; use nym_crypto::asymmetric::ed25519::PublicKey; -use tokio::{net::TcpListener, task::JoinHandle}; +use std::sync::Arc; +use tokio::{net::TcpListener, sync::RwLock, task::JoinHandle}; use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; use crate::{ db::DbPool, http::{api::RouterBuilder, state::AppState}, - monitor::NodeGeoCache, + monitor::{DelegationsCache, NodeGeoCache}, }; /// Return handles that allow for graceful shutdown of server + awaiting its /// background tokio task +#[allow(clippy::too_many_arguments)] pub(crate) async fn start_http_api( db_pool: DbPool, http_port: u16, nym_http_cache_ttl: u64, agent_key_list: Vec, agent_max_count: i64, + agent_request_freshness_requirement: time::Duration, node_geocache: NodeGeoCache, + node_delegations: Arc>, ) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); @@ -27,12 +31,14 @@ pub(crate) async fn start_http_api( nym_http_cache_ttl, agent_key_list, agent_max_count, + agent_request_freshness_requirement, node_geocache, + node_delegations, ) .await; let router = router_builder.with_state(state); - let bind_addr = format!("0.0.0.0:{}", http_port); + let bind_addr = format!("0.0.0.0:{http_port}"); tracing::info!("Binding server to {bind_addr}"); let server = router.build_server(bind_addr).await?; diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index cfd10e936c..35a3799202 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -1,20 +1,33 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; - use cosmwasm_std::Decimal; +use itertools::Itertools; use moka::{future::Cache, Entry}; +use nym_bin_common::bin_info_owned; use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::ed25519::PublicKey; -use nym_validator_client::{models::DescribedNodeType, nym_api::SkimmedNode}; +use nym_mixnet_contract_common::NodeId; +use nym_node_status_client::auth::VerifiableRequest; +use nym_validator_client::nym_api::SkimmedNode; +use semver::Version; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use time::UtcDateTime; use tokio::sync::RwLock; -use tracing::instrument; - -use crate::{ - db::{queries, DbPool}, - http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory}, - monitor::NodeGeoCache, -}; +use tracing::{error, instrument, warn}; +use utoipa::ToSchema; use super::models::SessionStats; +use crate::{ + db::{queries, DbPool}, + http::{ + error::{HttpError, HttpResult}, + models::{ + DVpnGateway, DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory, + }, + }, + monitor::{DelegationsCache, NodeGeoCache}, +}; + +pub(crate) use nym_validator_client::models::BinaryBuildInformationOwned; #[derive(Debug, Clone)] pub(crate) struct AppState { @@ -22,7 +35,10 @@ pub(crate) struct AppState { cache: HttpCache, agent_key_list: Vec, agent_max_count: i64, + agent_request_freshness_requirement: time::Duration, node_geocache: NodeGeoCache, + node_delegations: Arc>, + bin_info: BinaryInfo, } impl AppState { @@ -31,14 +47,19 @@ impl AppState { cache_ttl: u64, agent_key_list: Vec, agent_max_count: i64, + agent_request_freshness_requirement: time::Duration, node_geocache: NodeGeoCache, + node_delegations: Arc>, ) -> Self { Self { db_pool, cache: HttpCache::new(cache_ttl).await, agent_key_list, agent_max_count, + agent_request_freshness_requirement, node_geocache, + node_delegations, + bin_info: BinaryInfo::new(), } } @@ -61,9 +82,67 @@ impl AppState { pub(crate) fn node_geocache(&self) -> NodeGeoCache { self.node_geocache.clone() } + + pub(crate) async fn node_delegations( + &self, + node_id: NodeId, + ) -> Option> { + self.node_delegations + .read() + .await + .delegations_owned(node_id) + } + + pub(crate) fn health(&self) -> HealthInfo { + let uptime = (UtcDateTime::now() - self.bin_info.startup_time).whole_seconds(); + HealthInfo { uptime } + } + + pub(crate) fn build_information(&self) -> &BinaryBuildInformationOwned { + &self.bin_info.build_info + } + + #[tracing::instrument(level = "debug", skip_all)] + pub(crate) fn authenticate_agent_submission( + &self, + request: &impl VerifiableRequest, + ) -> HttpResult<()> { + if !self.is_registered(request.public_key()) { + tracing::warn!("Public key not registered with NS API, rejecting"); + return Err(HttpError::unauthorized()); + }; + + request.verify_signature().map_err(|_| { + tracing::warn!("Signature verification failed, rejecting"); + HttpError::unauthorized() + })?; + + Ok(()) + } + + pub(crate) fn is_fresh(&self, request_time: &i64) -> HttpResult<()> { + // if a request took longer than N minutes to reach NS API, something is very wrong + let request_time = time::UtcDateTime::from_unix_timestamp(*request_time).map_err(|e| { + warn!("Failed to parse request time: {e}"); + HttpError::unauthorized() + })?; + + let cutoff_timestamp = crate::utils::now_utc() - self.agent_request_freshness_requirement; + if request_time < cutoff_timestamp { + warn!( + "Request time {} is older than cutoff {} ({}s ago), rejecting", + request_time, + cutoff_timestamp, + self.agent_request_freshness_requirement.whole_seconds() + ); + return Err(HttpError::unauthorized()); + } + Ok(()) + } } static GATEWAYS_LIST_KEY: &str = "gateways"; +static DVPN_GATEWAYS_LIST_KEY: &str = "dvpn_gateways"; static MIXNODES_LIST_KEY: &str = "mixnodes"; static NYM_NODES_LIST_KEY: &str = "nym_nodes"; static MIXSTATS_LIST_KEY: &str = "mixstats"; @@ -75,6 +154,7 @@ const MIXNODE_STATS_HISTORY_DAYS: usize = 30; #[derive(Debug, Clone)] pub(crate) struct HttpCache { gateways: Cache>>>, + dvpn_gateways: Cache>>>, mixnodes: Cache>>>, nym_nodes: Cache>>>, mixstats: Cache>>>, @@ -89,6 +169,10 @@ impl HttpCache { .max_capacity(2) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), + dvpn_gateways: Cache::builder() + .max_capacity(6) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), mixnodes: Cache::builder() .max_capacity(2) .time_to_live(Duration::from_secs(ttl_seconds)) @@ -142,13 +226,24 @@ impl HttpCache { // the key is missing so populate it tracing::trace!("No gateways in cache, refreshing cache from DB..."); - let gateways = crate::db::queries::get_all_gateways(db) - .await - .unwrap_or_default(); - self.upsert_gateway_list(gateways.clone()).await; + let gateways = match crate::db::queries::get_all_gateways(db).await { + Ok(gws) => { + tracing::info!("Successfully fetched {} gateways from database", gws.len()); + if !gws.is_empty() { + self.upsert_gateway_list(gws.clone()).await; + } + gws + } + Err(err) => { + tracing::error!("CRITICAL: Failed to fetch gateways from database: {err}"); + panic!( + "Cannot read gateways table - this should never happen! Error: {err}" + ); + } + }; if gateways.is_empty() { - tracing::warn!("Database contains 0 gateways"); + tracing::warn!("Database: gateway list is empty"); } gateways @@ -156,6 +251,181 @@ impl HttpCache { } } + pub async fn upsert_dvpn_gateway_list( + &self, + new_gateway_list: Vec, + ) -> Entry>>> { + self.dvpn_gateways + .entry_by_ref(DVPN_GATEWAYS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_gateway_list; + v.clone() + } else { + Arc::new(RwLock::new(new_gateway_list)) + } + }) + .await + } + + pub async fn get_dvpn_gateway_list( + &self, + db: &DbPool, + min_node_version: &Version, + ) -> Vec { + match self.dvpn_gateways.get(DVPN_GATEWAYS_LIST_KEY).await { + Some(guard) => { + tracing::trace!("Fetching from cache..."); + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + tracing::info!("No gateways (dVPN) in cache, refreshing from DB..."); + + let gateways = self.get_gateway_list(db).await; + tracing::info!("Found {} gateways in database", gateways.len()); + + let started_with = gateways.len(); + let skimmed_nodes = match crate::db::queries::get_described_bonded_nym_nodes(db) + .await + { + Ok(records) => { + let mut nodes = HashMap::new(); + for dto in records { + match SkimmedNode::try_from(dto) { + Ok(skimmed_node) => { + let key = + skimmed_node.ed25519_identity_pubkey.to_base58_string(); + nodes.insert(key, skimmed_node); + } + Err(err) => { + error!("CRITICAL: Failed to convert NymNodeDto to SkimmedNode: {err}"); + panic!("Cannot convert database record to SkimmedNode - this should never happen! Error: {err}"); + } + } + } + nodes + } + Err(err) => { + error!("CRITICAL: Failed to query nym_nodes from database: {err}"); + panic!( + "Cannot read nym_nodes table - database connection issue? Error: {err}" + ); + } + }; + + let res_gws = gateways + .iter() + .filter(|gw| gw.bonded) + .filter_map(|gw| match skimmed_nodes.get(&gw.gateway_identity_key) { + Some(skimmed_node) => Some((gw, skimmed_node)), + None => { + error!( + "CRITICAL: Gateway {} exists in gateways table but not in nym_nodes table! This should not happen.", + gw.gateway_identity_key + ); + None + } + }) + .filter_map( + |(gw, skimmed_node)| match DVpnGateway::new(gw.clone(), skimmed_node) { + Ok(gw) => Some(gw), + Err(err) => { + error!( + "CRITICAL: Failed to create DVpnGateway for node_id={}, identity_key={}: {}", + skimmed_node.node_id, + skimmed_node.ed25519_identity_pubkey.to_base58_string(), + err + ); + // Don't panic here as this might be due to missing fields, but log it loudly + None + } + }, + ) + .filter(|gw| { + let gw_version = &gw.build_information.build_version; + if let Ok(gw_version) = Version::parse(gw_version) { + &gw_version >= min_node_version + } else { + warn!("Failed to parse GW version {}", gw_version); + false + } + }) + .filter(|gw| { + // gateways must have a country + if gw.location.two_letter_iso_country_code.len() == 2 { + true + } else { + warn!( + "Invalid country code: {}", + gw.location.two_letter_iso_country_code + ); + false + } + }) + // sort by country, then by identity key + .sorted_by_key(|item| { + ( + item.location.two_letter_iso_country_code.clone(), + item.identity_key.clone(), + ) + }) + .collect::>(); + + let bonded_count = gateways.iter().filter(|gw| gw.bonded).count(); + tracing::info!( + "DVpn gateway filtering: {} total gateways, {} bonded, {} nym_nodes, {} final DVpn gateways", + started_with, + bonded_count, + skimmed_nodes.len(), + res_gws.len() + ); + + if res_gws.is_empty() && started_with > 0 { + tracing::error!( + "CRITICAL: Started with {} gateways but got 0 DVpn gateways! Min version: {}", + started_with, + min_node_version + ); + } else { + tracing::info!( + "Successfully loaded {} DVpn gateways into cache", + res_gws.len() + ); + self.upsert_dvpn_gateway_list(res_gws.clone()).await; + } + + res_gws + } + } + } + + pub async fn get_entry_dvpn_gateways( + &self, + db: &DbPool, + min_node_version: &Version, + ) -> Vec { + self.get_dvpn_gateway_list(db, min_node_version) + .await + .into_iter() + .filter(DVpnGateway::can_route_entry) + .collect() + } + + pub async fn get_exit_dvpn_gateways( + &self, + db: &DbPool, + min_node_version: &Version, + ) -> Vec { + self.get_dvpn_gateway_list(db, min_node_version) + .await + .into_iter() + .filter(DVpnGateway::can_route_exit) + .collect() + } + pub async fn upsert_mixnode_list( &self, new_mixnode_list: Vec, @@ -188,10 +458,11 @@ impl HttpCache { let mixnodes = crate::db::queries::get_all_mixnodes(db) .await .unwrap_or_default(); - self.upsert_mixnode_list(mixnodes.clone()).await; if mixnodes.is_empty() { tracing::warn!("Database contains 0 mixnodes"); + } else { + self.upsert_mixnode_list(mixnodes.clone()).await; } mixnodes @@ -232,14 +503,13 @@ impl HttpCache { None => { tracing::trace!("No nym nodes in cache, refreshing cache from DB..."); - let nym_nodes = aggregate_node_info_from_db(db, node_geocache) - .await - .inspect(|nym_nodes| { - if nym_nodes.is_empty() { - tracing::warn!("Database contains 0 nym nodes"); - } - })?; - self.upsert_nym_node_list(nym_nodes.clone()).await; + let nym_nodes = aggregate_node_info_from_db(db, node_geocache).await?; + + if nym_nodes.is_empty() { + tracing::warn!("Database contains 0 nym nodes"); + } else { + self.upsert_nym_node_list(nym_nodes.clone()).await; + } Ok(nym_nodes) } @@ -401,11 +671,7 @@ async fn aggregate_node_info_from_db( .get(&node_id) .map(|node| node.performance.naive_to_f64()) .unwrap_or(0.0); - let node_type = match described_node.contract_node_type { - DescribedNodeType::NymNode => "nym_node".to_string(), - DescribedNodeType::LegacyMixnode => "legacy_mixnode".to_string(), - DescribedNodeType::LegacyGateway => "legacy_gateway".to_string(), - }; + let node_type = described_node.contract_node_type; let ip_address = described_node .description .host_information @@ -417,11 +683,10 @@ async fn aggregate_node_info_from_db( .description .auxiliary_details .accepted_operator_terms_and_conditions; - let description = described_node.description; + let self_described = described_node.description; - let bonding_address = bond_details - .map(|details| details.bond_information.owner.to_string()) - .unwrap_or_default(); + let bonding_address = + bond_details.map(|details| details.bond_information.owner.to_string()); let node_description = node_descriptions.get(&node_id).cloned().unwrap_or_default(); let geoip = { @@ -449,8 +714,8 @@ async fn aggregate_node_info_from_db( bonded, node_type, accepted_tnc, - self_description: serde_json::to_value(description).unwrap_or_default(), - rewarding_details: serde_json::to_value(rewarding_details).unwrap_or_default(), + self_description: self_described, + rewarding_details: rewarding_details.to_owned(), description: node_description, geoip, }); @@ -458,3 +723,23 @@ async fn aggregate_node_info_from_db( Ok(parsed_nym_nodes) } + +#[derive(Debug, Clone)] +pub(crate) struct BinaryInfo { + startup_time: UtcDateTime, + build_info: BinaryBuildInformationOwned, +} + +impl BinaryInfo { + fn new() -> Self { + Self { + startup_time: UtcDateTime::now(), + build_info: bin_info_owned!(), + } + } +} + +#[derive(Serialize, ToSchema, Deserialize)] +pub(crate) struct HealthInfo { + pub(crate) uptime: i64, +} diff --git a/nym-node-status-api/nym-node-status-api/src/logging.rs b/nym-node-status-api/nym-node-status-api/src/logging.rs index bf3960e27b..c4581af169 100644 --- a/nym-node-status-api/nym-node-status-api/src/logging.rs +++ b/nym-node-status-api/nym-node-status-api/src/logging.rs @@ -30,6 +30,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { "hyper", "sqlx", "h2", + "nym_http_api_client", "tendermint_rpc", "tower_http", "axum", @@ -38,7 +39,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { "hickory_resolver", ]; for crate_name in warn_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))?); } let log_level_hint = filter.max_level_hint(); diff --git a/nym-node-status-api/nym-node-status-api/src/main.rs b/nym-node-status-api/nym-node-status-api/src/main.rs index 65128d8932..2723222415 100644 --- a/nym-node-status-api/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/nym-node-status-api/src/main.rs @@ -1,12 +1,21 @@ +use crate::monitor::DelegationsCache; use clap::Parser; use nym_crypto::asymmetric::ed25519::PublicKey; use nym_task::signal::wait_for_signal; +use nym_validator_client::nyxd::NyxdClient; +use std::sync::Arc; + +#[cfg(all(feature = "sqlite", feature = "pg"))] +compile_error!("Features 'sqlite' and 'pg' are mutually exclusive"); + +#[cfg(not(any(feature = "sqlite", feature = "pg")))] +compile_error!("Either 'sqlite' or 'pg' feature must be enabled"); mod cli; mod db; mod http; mod logging; -mod mixnet_scraper; +mod metrics_scraper; mod monitor; mod node_scraper; mod testruns; @@ -28,11 +37,18 @@ async fn main() -> anyhow::Result<()> { let connection_url = args.database_url.clone(); tracing::debug!("Using config:\n{:#?}", args); - let storage = db::Storage::init(connection_url).await?; + let storage = db::Storage::init(connection_url, args.sqlx_busy_timeout_s).await?; let db_pool = storage.pool_owned(); // Start the node scraper - let scraper = mixnet_scraper::Scraper::new(storage.pool_owned()); + let scraper = node_scraper::DescriptionScraper::new(storage.pool_owned()); + tokio::spawn(async move { + scraper.start().await; + }); + let scraper = node_scraper::PacketScraper::new( + storage.pool_owned(), + args.packet_stats_max_concurrent_tasks, + ); tokio::spawn(async move { scraper.start().await; }); @@ -41,19 +57,27 @@ async fn main() -> anyhow::Result<()> { let geocache = moka::future::Cache::builder() .time_to_live(args.geodata_ttl) .build(); + let delegations_cache = DelegationsCache::new(); // Start the monitor let args_clone = args.clone(); let geocache_clone = geocache.clone(); + let delegations_cache_clone = Arc::clone(&delegations_cache); + let config = nym_validator_client::nyxd::Config::try_from_nym_network_details( + &nym_network_defaults::NymNetworkDetails::new_from_env(), + )?; + let nyxd_client = NyxdClient::connect(config, args.nyxd_addr.as_str()) + .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; tokio::spawn(async move { monitor::spawn_in_background( db_pool, args_clone.nym_api_client_timeout, - args_clone.nyxd_addr, + nyxd_client, args_clone.monitor_refresh_interval, args_clone.ipinfo_api_token, geocache_clone, + delegations_cache_clone, ) .await; tracing::info!("Started monitor task"); @@ -63,7 +87,8 @@ async fn main() -> anyhow::Result<()> { let db_pool_scraper = storage.pool_owned(); tokio::spawn(async move { - node_scraper::spawn_in_background(db_pool_scraper, args_clone.nym_api_client_timeout).await; + metrics_scraper::spawn_in_background(db_pool_scraper, args_clone.nym_api_client_timeout) + .await; tracing::info!("Started metrics scraper task"); }); @@ -73,7 +98,9 @@ async fn main() -> anyhow::Result<()> { args.nym_http_cache_ttl, agent_key_list.to_owned(), args.max_agent_count, + args.agent_request_freshness, geocache, + delegations_cache, ) .await .expect("Failed to start server"); diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs new file mode 100644 index 0000000000..99cff49a06 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs @@ -0,0 +1,139 @@ +use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_node_requests::api::client::NymNodeApiClientError; +use nym_validator_client::client::NodeId; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NodeScraperError { + #[error("node {node_id} has provided malformed host information ({host}: {source}")] + MalformedHost { + host: String, + node_id: NodeId, + #[source] + source: NymNodeApiClientError, + }, + + #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] + NoHttpPortsAvailable { host: String, node_id: NodeId }, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::error::Error; + + #[test] + fn test_malformed_host_error() { + // Create a generic error to test with + let source_error = NymNodeApiClientError::GenericRequestFailure("Invalid URL".to_string()); + let error = NodeScraperError::MalformedHost { + host: "invalid-host:abc".to_string(), + node_id: 42, + source: source_error, + }; + + // Test error message formatting + let error_msg = error.to_string(); + assert!(error_msg.contains("node 42")); + assert!(error_msg.contains("invalid-host:abc")); + assert!(error_msg.contains("malformed host information")); + + // Test that source error is accessible + assert!(error.source().is_some()); + } + + #[test] + fn test_malformed_host_error_edge_cases() { + // Test with empty host + let error = NodeScraperError::MalformedHost { + host: "".to_string(), + node_id: 0, + source: NymNodeApiClientError::NotFound, + }; + + let error_msg = error.to_string(); + assert!(error_msg.contains("node 0")); + + // Test with very long host + let long_host = "x".repeat(1000); + let error = NodeScraperError::MalformedHost { + host: long_host.clone(), + node_id: u32::MAX, + source: NymNodeApiClientError::GenericRequestFailure("Too long".to_string()), + }; + + let error_msg = error.to_string(); + assert!(error_msg.contains(&format!("node {}", u32::MAX))); + assert!(error_msg.contains(&long_host)); + } + + #[test] + fn test_no_http_ports_available_error() { + let error = NodeScraperError::NoHttpPortsAvailable { + host: "example.com".to_string(), + node_id: 123, + }; + + let error_msg = error.to_string(); + assert!(error_msg.contains("node 123")); + assert!(error_msg.contains("example.com")); + assert!(error_msg.contains("doesn't seem to expose its declared http port")); + assert!(error_msg.contains("80, 443 or")); + assert!(error_msg.contains(&DEFAULT_NYM_NODE_HTTP_PORT.to_string())); + + // This error type has no source + assert!(error.source().is_none()); + } + + #[test] + fn test_no_http_ports_special_characters() { + // Test with host containing special characters + let error = NodeScraperError::NoHttpPortsAvailable { + host: "test-node_123.example.com:8080".to_string(), + node_id: 999, + }; + + let error_msg = error.to_string(); + assert!(error_msg.contains("test-node_123.example.com:8080")); + } + + #[test] + fn test_error_different_sources() { + // Test with different NymNodeApiClientError variants + let not_found_error = NymNodeApiClientError::NotFound; + let error1 = NodeScraperError::MalformedHost { + host: "host1".to_string(), + node_id: 1, + source: not_found_error, + }; + + let generic_error = NymNodeApiClientError::GenericRequestFailure("404 error".to_string()); + let error2 = NodeScraperError::MalformedHost { + host: "host2".to_string(), + node_id: 2, + source: generic_error, + }; + + // Both should format differently based on their source + assert!(error1.to_string().contains("host1")); + assert!(error2.to_string().contains("host2")); + } + + #[test] + fn test_error_trait_implementation() { + // Test that NodeScraperError implements std::error::Error properly + let error = NodeScraperError::NoHttpPortsAvailable { + host: "test.com".to_string(), + node_id: 42, + }; + + // Can be used as dyn Error + let _error_ref: &dyn std::error::Error = &error; + + // Display trait is implemented + let _display = format!("{error}"); + + // Debug trait is implemented + let _debug = format!("{error:?}"); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs new file mode 100644 index 0000000000..bc3cd23fe7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs @@ -0,0 +1,284 @@ +use crate::db::{models::GatewaySessionsRecord, queries, DbPool}; +use error::NodeScraperError; +use nym_network_defaults::{NymNetworkDetails, DEFAULT_NYM_NODE_HTTP_PORT}; +use nym_node_requests::api::{client::NymNodeApiClientExt, v1::metrics::models::SessionStats}; +use nym_validator_client::{ + client::{NodeId, NymNodeDetails}, + models::{DescribedNodeType, NymNodeDescription}, + NymApiClient, +}; +use time::OffsetDateTime; + +use nym_statistics_common::types::SessionType; +use std::collections::HashMap; +use tokio::time::Duration; +use tracing::instrument; + +mod error; + +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); +const REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60 * 6); +const STALE_DURATION: Duration = Duration::from_secs(86400 * 365); //one year + +#[instrument(level = "info", name = "metrics_scraper", skip_all)] +pub(crate) async fn spawn_in_background(db_pool: DbPool, nym_api_client_timeout: Duration) { + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + tracing::info!("Refreshing node self-described metrics..."); + + if let Err(e) = run(&db_pool, &network_defaults, nym_api_client_timeout).await { + tracing::error!( + "Metrics collection failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Metrics successfully collected, sleeping for {}s...", + REFRESH_INTERVAL.as_secs() + ); + tokio::time::sleep(REFRESH_INTERVAL).await; + } + } +} + +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + nym_api_client_timeout: Duration, +) -> anyhow::Result<()> { + let default_api_url = network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); + + let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()]) + .no_hickory_dns() + .with_timeout(nym_api_client_timeout) + .build::<&str>()?; + + let api_client = NymApiClient::from(nym_api); + + //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes + let bonded_nodes = api_client.get_all_bonded_nym_nodes().await?; + let all_nodes = api_client.get_all_described_nodes().await?; //legacy node that did not upgrade the contract bond yet + tracing::debug!("Fetched {} total nodes", all_nodes.len()); + + let mut nodes_to_scrape: HashMap = bonded_nodes + .into_iter() + .map(|n| (n.node_id(), n.into())) + .collect(); + + all_nodes + .into_iter() + .filter(|n| n.contract_node_type != DescribedNodeType::LegacyMixnode) + .for_each(|n| { + nodes_to_scrape.entry(n.node_id).or_insert_with(|| n.into()); + }); + tracing::debug!("Will try to scrape {} nodes", nodes_to_scrape.len()); + + let mut session_records = Vec::new(); + for n in nodes_to_scrape.into_values() { + if let Some(stat) = n.try_scrape_metrics().await { + session_records.push(prepare_session_data(stat, &n)); + } + } + + queries::insert_session_records(pool, session_records) + .await + .map(|_| { + tracing::debug!("Session info written to DB!"); + })?; + let cut_off_date = (OffsetDateTime::now_utc() - STALE_DURATION).date(); + queries::delete_old_records(pool, cut_off_date) + .await + .map(|_| { + tracing::debug!("Cleared old data before {}", cut_off_date); + })?; + + Ok(()) +} + +#[derive(Debug)] +struct MetricsScrapingData { + host: String, + node_id: NodeId, + id_key: String, + port: Option, +} + +impl MetricsScrapingData { + pub fn new( + host: impl Into, + node_id: NodeId, + id_key: String, + port: Option, + ) -> Self { + MetricsScrapingData { + host: host.into(), + node_id, + id_key, + port, + } + } + + #[instrument(level = "info", name = "metrics_scraper", skip_all)] + async fn try_scrape_metrics(&self) -> Option { + match self.try_get_client().await { + Ok(client) => { + match client.get_sessions_metrics().await { + Ok(session_stats) => { + if session_stats.update_time != OffsetDateTime::UNIX_EPOCH { + Some(session_stats) + } else { + //means no data + None + } + } + Err(e) => { + tracing::warn!("{e}"); + None + } + } + } + Err(e) => { + tracing::warn!("{e}"); + None + } + } + } + + async fn try_get_client(&self) -> Result { + // first try the standard port in case the operator didn't put the node behind the proxy, + // then default https (443) + // finally default http (80) + let mut addresses_to_try = vec![ + format!("http://{0}:{DEFAULT_NYM_NODE_HTTP_PORT}", self.host), // 'standard' nym-node + format!("https://{0}", self.host), // node behind https proxy (443) + format!("http://{0}", self.host), // node behind http proxy (80) + ]; + + // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via + // the 'custom_port' since it should have been present in the contract. + + if let Some(port) = self.port { + addresses_to_try.insert(0, format!("http://{0}:{port}", self.host)); + } + + for address in addresses_to_try { + // if provided host was malformed, no point in continuing + let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { + b.with_timeout(Duration::from_secs(5)) + .with_user_agent("node-status-api-metrics-scraper") + .no_hickory_dns() + .build() + }) { + Ok(client) => client, + Err(err) => { + return Err(NodeScraperError::MalformedHost { + host: self.host.to_string(), + node_id: self.node_id, + source: err, + }); + } + }; + + if let Ok(health) = client.get_health().await { + if health.status.is_up() { + return Ok(client); + } + } + } + + Err(NodeScraperError::NoHttpPortsAvailable { + host: self.host.to_string(), + node_id: self.node_id, + }) + } +} + +impl From for MetricsScrapingData { + fn from(value: NymNodeDetails) -> Self { + MetricsScrapingData::new( + value.bond_information.node.host.clone(), + value.node_id(), + value.bond_information.node.identity_key, + value.bond_information.node.custom_http_port, + ) + } +} + +impl From for MetricsScrapingData { + fn from(value: NymNodeDescription) -> Self { + MetricsScrapingData::new( + value.description.host_information.ip_address[0].to_string(), + value.node_id, + value.ed25519_identity_key().to_base58_string(), + None, + ) + } +} + +fn prepare_session_data( + stat: SessionStats, + node_data: &MetricsScrapingData, +) -> GatewaySessionsRecord { + let users_hashes = if !stat.unique_active_users_hashes.is_empty() { + Some(serde_json::to_string(&stat.unique_active_users_hashes).unwrap()) + } else { + None + }; + let vpn_durations = stat + .sessions + .iter() + .filter(|s| SessionType::from_string(&s.typ) == SessionType::Vpn) + .map(|s| s.duration_ms) + .collect::>(); + + let mixnet_durations = stat + .sessions + .iter() + .filter(|s| SessionType::from_string(&s.typ) == SessionType::Mixnet) + .map(|s| s.duration_ms) + .collect::>(); + + let unknown_durations = stat + .sessions + .iter() + .filter(|s| SessionType::from_string(&s.typ) == SessionType::Unknown) + .map(|s| s.duration_ms) + .collect::>(); + + let vpn_sessions = if !vpn_durations.is_empty() { + Some(serde_json::to_string(&vpn_durations).unwrap()) + } else { + None + }; + let mixnet_sessions = if !mixnet_durations.is_empty() { + Some(serde_json::to_string(&mixnet_durations).unwrap()) + } else { + None + }; + let unknown_sessions = if !unknown_durations.is_empty() { + Some(serde_json::to_string(&unknown_durations).unwrap()) + } else { + None + }; + + GatewaySessionsRecord { + gateway_identity_key: node_data.id_key.clone(), + node_id: node_data.node_id as i64, + day: stat.update_time.date(), + unique_active_clients: stat.unique_active_users as i64, + session_started: stat.sessions_started as i64, + users_hashes, + vpn_sessions, + mixnet_sessions, + unknown_sessions, + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/mixnet_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/mixnet_scraper/mod.rs deleted file mode 100644 index 686d043987..0000000000 --- a/nym-node-status-api/nym-node-status-api/src/mixnet_scraper/mod.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -pub mod helpers; -use anyhow::Result; -use helpers::{scrape_and_store_description, scrape_and_store_packet_stats}; -use sqlx::SqlitePool; -use tracing::{debug, error, instrument, warn}; - -use crate::db::models::ScraperNodeInfo; -use crate::db::queries::get_nodes_for_scraping; - -const DESCRIPTION_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60 * 4); -const PACKET_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60); -const QUEUE_CHECK_INTERVAL: Duration = Duration::from_millis(250); -const MAX_CONCURRENT_TASKS: usize = 5; - -static TASK_COUNTER: AtomicUsize = AtomicUsize::new(0); -static TASK_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); - -pub struct Scraper { - pool: SqlitePool, - description_queue: Arc>>, - packet_queue: Arc>>, -} - -impl Scraper { - pub fn new(pool: SqlitePool) -> Self { - Self { - pool, - description_queue: Arc::new(Mutex::new(Vec::new())), - packet_queue: Arc::new(Mutex::new(Vec::new())), - } - } - - pub async fn start(&self) { - self.spawn_description_scraper().await; - self.spawn_packet_scraper().await; - } - - async fn spawn_description_scraper(&self) { - let pool = self.pool.clone(); - let queue = self.description_queue.clone(); - tracing::info!("Starting description scraper"); - tokio::spawn(async move { - loop { - if let Err(e) = Self::run_description_scraper(&pool, queue.clone()).await { - error!(name: "description_scraper", "Description scraper failed: {}", e); - } - debug!(name: "description_scraper", "Sleeping for {}s", DESCRIPTION_SCRAPE_INTERVAL.as_secs()); - tokio::time::sleep(DESCRIPTION_SCRAPE_INTERVAL).await; - } - }); - } - - async fn spawn_packet_scraper(&self) { - let pool = self.pool.clone(); - let queue = self.packet_queue.clone(); - tracing::info!("Starting packet scraper"); - - tokio::spawn(async move { - loop { - if let Err(e) = Self::run_packet_scraper(&pool, queue.clone()).await { - error!(name: "packet_scraper", "Packet scraper failed: {}", e); - } - debug!(name: "packet_scraper", "Sleeping for {}s", PACKET_SCRAPE_INTERVAL.as_secs()); - tokio::time::sleep(PACKET_SCRAPE_INTERVAL).await; - } - }); - } - - #[instrument(level = "info", name = "description_scraper", skip_all)] - async fn run_description_scraper( - pool: &SqlitePool, - queue: Arc>>, - ) -> Result<()> { - let nodes = get_nodes_for_scraping(pool).await?; - if let Ok(mut queue_lock) = queue.lock() { - queue_lock.extend(nodes); - } else { - warn!("Failed to acquire description queue lock"); - return Ok(()); - } - - Self::process_description_queue(pool, queue).await; - Ok(()) - } - - #[instrument(level = "info", name = "packet_scraper", skip_all)] - async fn run_packet_scraper( - pool: &SqlitePool, - queue: Arc>>, - ) -> Result<()> { - let nodes = get_nodes_for_scraping(pool).await?; - tracing::info!("Querying {} mixing nodes", nodes.len()); - if let Ok(mut queue_lock) = queue.lock() { - queue_lock.extend(nodes); - } else { - warn!("Failed to acquire packet queue lock"); - return Ok(()); - } - - Self::process_packet_queue(pool, queue).await; - Ok(()) - } - - async fn process_description_queue(pool: &SqlitePool, queue: Arc>>) { - loop { - let running_tasks = TASK_COUNTER.load(Ordering::Relaxed); - - if running_tasks < MAX_CONCURRENT_TASKS { - let node = { - if let Ok(mut queue_lock) = queue.lock() { - if queue_lock.is_empty() { - TASK_ID_COUNTER.store(0, Ordering::Relaxed); - break; - } - queue_lock.remove(0) - } else { - warn!("Failed to acquire description queue lock"); - break; - } - }; - - TASK_COUNTER.fetch_add(1, Ordering::Relaxed); - let task_id = TASK_ID_COUNTER.fetch_add(1, Ordering::Relaxed); - let pool = pool.clone(); - - tokio::spawn(async move { - match scrape_and_store_description(&pool, &node).await { - Ok(_) => debug!( - "📝 ✅ Description task #{} for node {} complete", - task_id, - node.node_id() - ), - Err(e) => debug!( - "📝 ❌ Description task #{} for {} {} failed: {}", - task_id, - node.node_kind, - node.node_id(), - e - ), - } - TASK_COUNTER.fetch_sub(1, Ordering::Relaxed); - }); - } else { - tokio::time::sleep(QUEUE_CHECK_INTERVAL).await; - } - } - } - - async fn process_packet_queue(pool: &SqlitePool, queue: Arc>>) { - loop { - let running_tasks = TASK_COUNTER.load(Ordering::Relaxed); - - if running_tasks < MAX_CONCURRENT_TASKS { - let node = { - if let Ok(mut queue_lock) = queue.lock() { - if queue_lock.is_empty() { - TASK_ID_COUNTER.store(0, Ordering::Relaxed); - break; - } - queue_lock.remove(0) - } else { - warn!("Failed to acquire packet queue lock"); - break; - } - }; - - TASK_COUNTER.fetch_add(1, Ordering::Relaxed); - let task_id = TASK_ID_COUNTER.fetch_add(1, Ordering::Relaxed); - let pool = pool.clone(); - - tokio::spawn(async move { - match scrape_and_store_packet_stats(&pool, &node).await { - Ok(_) => debug!( - "📊 ✅ Packet stats task #{} for node {} complete", - task_id, - node.node_id() - ), - Err(e) => debug!( - "📊 ❌ Packet stats task #{} for {} {} failed: {}", - task_id, - node.node_kind, - node.node_id(), - e - ), - } - TASK_COUNTER.fetch_sub(1, Ordering::Relaxed); - }); - } else { - tokio::time::sleep(QUEUE_CHECK_INTERVAL).await; - } - } - } -} diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs index 9a7fb3a0d2..e9d6e554ea 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs @@ -63,7 +63,7 @@ impl IpInfoClient { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ExplorerPrettyBond { pub(crate) identity_key: String, pub(crate) owner: Addr, @@ -71,7 +71,7 @@ pub(crate) struct ExplorerPrettyBond { pub(crate) location: Location, } -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub(crate) struct Location { pub(crate) two_letter_iso_country_code: String, #[serde(flatten)] diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 56563be519..d41d0cedc9 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -7,30 +7,35 @@ use crate::db::models::{ NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT, }; use crate::db::{queries, DbPool}; -use crate::monitor::geodata::{ExplorerPrettyBond, Location}; +use crate::utils::now_utc; use crate::utils::{decimal_to_i64, LogError, NumericalCheckedCast}; use anyhow::anyhow; use moka::future::Cache; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::client::{NodeId, NymApiClientExt, NymNodeDetails}; -use nym_validator_client::models::{ - LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription, +use nym_validator_client::{ + client::{NodeId, NymApiClientExt, NymNodeDetails}, + models::{LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription}, }; -use nym_validator_client::nym_nodes::{NodeRole, SkimmedNode}; -use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; -use nym_validator_client::nyxd::{AccountId, NyxdClient}; -use nym_validator_client::NymApiClient; -use reqwest::Url; -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; -use tokio::time::Duration; +use nym_validator_client::{ + nym_nodes::{NodeRole, SkimmedNode}, + nyxd::{contract_traits::PagedMixnetQueryClient, AccountId}, + NymApiClient, QueryHttpRpcNyxdClient, +}; +use std::{ + collections::{HashMap, HashSet}, + str::FromStr, + sync::Arc, +}; +use tokio::{sync::RwLock, time::Duration}; use tracing::instrument; -pub(crate) use geodata::IpInfoClient; +pub(crate) use geodata::{ExplorerPrettyBond, IpInfoClient, Location}; +pub(crate) use node_delegations::DelegationsCache; mod geodata; +mod node_delegations; -const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); +const MONITOR_FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; pub(crate) type NodeGeoCache = Cache; @@ -38,9 +43,10 @@ struct Monitor { db_pool: DbPool, network_details: NymNetworkDetails, nym_api_client_timeout: Duration, - nyxd_addr: Url, + nyxd_client: QueryHttpRpcNyxdClient, ipinfo: IpInfoClient, geocache: NodeGeoCache, + node_delegations: Arc>, } // TODO dz: query many NYM APIs: @@ -49,19 +55,22 @@ struct Monitor { pub(crate) async fn spawn_in_background( db_pool: DbPool, nym_api_client_timeout: Duration, - nyxd_addr: Url, + nyxd_client: nym_validator_client::QueryHttpRpcNyxdClient, refresh_interval: Duration, ipinfo_api_token: String, geocache: NodeGeoCache, + node_delegations: Arc>, ) { let ipinfo = IpInfoClient::new(ipinfo_api_token.clone()); + let mut monitor = Monitor { db_pool, network_details: nym_network_defaults::NymNetworkDetails::new_from_env(), nym_api_client_timeout, - nyxd_addr, + nyxd_client, ipinfo, geocache, + node_delegations, }; loop { @@ -70,10 +79,9 @@ pub(crate) async fn spawn_in_background( if let Err(e) = monitor.run().await { tracing::error!( "Monitor run failed: {e}, retrying in {}s...", - FAILURE_RETRY_DELAY.as_secs() + MONITOR_FAILURE_RETRY_DELAY.as_secs() ); - // TODO dz implement some sort of backoff - tokio::time::sleep(FAILURE_RETRY_DELAY).await; + tokio::time::sleep(MONITOR_FAILURE_RETRY_DELAY).await; } else { tracing::info!( "Info successfully collected, sleeping for {}s...", @@ -97,12 +105,13 @@ impl Monitor { .clone() .expect("rust sdk mainnet default missing api_url"); - let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url) - .no_hickory_dns() - .with_timeout(self.nym_api_client_timeout) - .build::<&str>()?; + let nym_api = + nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()]) + .no_hickory_dns() + .with_timeout(self.nym_api_client_timeout) + .build::<&str>()?; - let api_client = NymApiClient { nym_api }; + let api_client = NymApiClient::from(nym_api); let described_nodes = api_client .get_all_described_nodes() @@ -124,6 +133,8 @@ impl Monitor { }) .collect::>(); + tracing::info!("🟣 🚪 gateway nodes: {}", gateways.len()); + let bonded_nym_nodes = api_client .get_all_bonded_nym_nodes() .await? @@ -140,7 +151,8 @@ impl Monitor { .await .log_error("get_all_basic_nodes")?; - tracing::info!("🟣 get_all_basic_nodes: {}", nym_nodes.len()); + let nym_node_count = nym_nodes.len(); + tracing::info!("🟣 get_all_basic_nodes: {}", nym_node_count); let nym_node_records = self.prepare_nym_node_data(nym_nodes.clone(), &bonded_nym_nodes, &described_nodes); @@ -151,7 +163,7 @@ impl Monitor { })?; // refresh geodata for all nodes - for (_, node_description) in described_nodes.iter() { + for node_description in described_nodes.values() { self.location_cached(node_description).await; } @@ -187,14 +199,13 @@ impl Monitor { tracing::info!("🟣 mixnodes_described: {}", mixnodes_described.len()); let mixing_assigned_nodes = api_client .nym_api - .get_basic_active_mixing_assigned_nodes(false, None, None) + .get_basic_active_mixing_assigned_nodes(false, None, None, false) .await .log_error("get_basic_active_mixing_assigned_nodes")? .nodes .data; - let delegation_program_members = - get_delegation_program_details(&self.network_details, &self.nyxd_addr).await?; + let delegation_program_members = self.get_delegation_program_details().await?; // keep stats for later let assigned_entry_count = nym_nodes @@ -233,40 +244,48 @@ impl Monitor { tracing::debug!("{} mixnode info written to DB!", mixnodes_count); })?; - let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(&pool).await?; + self.refresh_node_delegations(&bonded_nym_nodes).await; + + let (all_historical_gateways, all_historical_mixnodes) = historical_count(&pool).await?; // // write summary keys and values to table // let nodes_summary = vec![ - (NYMNODE_COUNT, nym_nodes.len()), - (ASSIGNED_MIXING_COUNT, assigned_mixing_count), - (MIXNODES_LEGACY_COUNT, count_legacy_mixnodes), - (NYMNODES_DESCRIBED_COUNT, described_nodes.len()), - (GATEWAYS_BONDED_COUNT, count_bonded_gateways), - (ASSIGNED_ENTRY_COUNT, assigned_entry_count), - (ASSIGNED_EXIT_COUNT, assigned_exit_count), + (NYMNODE_COUNT.to_string(), nym_node_count), + (ASSIGNED_MIXING_COUNT.to_string(), assigned_mixing_count), + (MIXNODES_LEGACY_COUNT.to_string(), count_legacy_mixnodes), + (NYMNODES_DESCRIBED_COUNT.to_string(), described_nodes.len()), + (GATEWAYS_BONDED_COUNT.to_string(), count_bonded_gateways), + (ASSIGNED_ENTRY_COUNT.to_string(), assigned_entry_count), + (ASSIGNED_EXIT_COUNT.to_string(), assigned_exit_count), // TODO dz doesn't make sense, could make sense with historical Nym // Nodes if we really need this data - (MIXNODES_HISTORICAL_COUNT, all_historical_mixnodes), - (GATEWAYS_HISTORICAL_COUNT, all_historical_gateways), + ( + MIXNODES_HISTORICAL_COUNT.to_string(), + all_historical_mixnodes, + ), + ( + GATEWAYS_HISTORICAL_COUNT.to_string(), + all_historical_gateways, + ), ]; - let last_updated = chrono::offset::Utc::now(); - let last_updated_utc = last_updated.timestamp().to_string(); + let last_updated = now_utc(); + let last_updated_utc = last_updated.unix_timestamp().to_string(); let network_summary = NetworkSummary { - total_nodes: nym_nodes.len().cast_checked()?, + total_nodes: nym_node_count.cast_checked()?, mixnodes: mixnode::MixnodeSummary { bonded: mixnode::MixingNodesSummary { count: assigned_mixing_count.cast_checked()?, self_described: described_nodes.len().cast_checked()?, legacy: count_legacy_mixnodes.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), + last_updated_utc: last_updated_utc.clone(), }, historical: mixnode::MixnodeSummaryHistorical { count: all_historical_mixnodes.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), + last_updated_utc: last_updated_utc.clone(), }, }, gateways: gateway::GatewaySummary { @@ -274,20 +293,21 @@ impl Monitor { count: count_bonded_gateways.cast_checked()?, entry: assigned_entry_count.cast_checked()?, exit: assigned_exit_count.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), + last_updated_utc: last_updated_utc.clone(), }, historical: gateway::GatewaySummaryHistorical { count: all_historical_gateways.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), + last_updated_utc, }, }, }; - queries::insert_summaries(&pool, &nodes_summary, &network_summary, last_updated).await?; + queries::insert_summaries(&pool, nodes_summary.clone(), network_summary, last_updated) + .await?; let mut log_lines: Vec = vec![]; for (key, value) in nodes_summary.iter() { - log_lines.push(format!("{} = {}", key, value)); + log_lines.push(format!("{key} = {value}")); } tracing::info!("Directory summary: \n{}", log_lines.join("\n")); @@ -295,7 +315,7 @@ impl Monitor { Ok(()) } - #[instrument(level = "debug", skip_all)] + #[instrument(level = "info", skip_all)] async fn location_cached(&mut self, node: &NymNodeDescription) -> Location { let node_id = node.node_id; @@ -358,7 +378,7 @@ impl Monitor { for gateway in described_gateways { let identity_key = gateway.ed25519_identity_key().to_base58_string(); let bonded = bonded_nodes.contains_key(&gateway.node_id); - let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let last_updated_utc = now_utc().unix_timestamp(); let self_described = serde_json::to_string(&gateway.description)?; @@ -424,7 +444,7 @@ impl Monitor { let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); let is_dp_delegatee = delegation_program_members.contains(&mix_id); - let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let last_updated_utc = now_utc().unix_timestamp(); mixnode_records.push(MixnodeRecord { mix_id, @@ -446,53 +466,68 @@ impl Monitor { async fn check_ipinfo_bandwidth(&self) { match self.ipinfo.check_remaining_bandwidth().await { Ok(bandwidth) => { - tracing::info!( - "ipinfo monthly bandwidth: {}/{} spent", - bandwidth.month, - bandwidth.limit - ); + tracing::info!("ipinfo monthly bandwidth: {} spent", bandwidth.month); } Err(err) => { tracing::debug!("Couldn't check ipinfo bandwidth: {}", err); } } } + + #[instrument(level = "info", skip_all)] + async fn refresh_node_delegations(&mut self, bonded_nodes: &HashMap) { + let delegations_per_node = node_delegations::refresh(&self.nyxd_client, bonded_nodes).await; + + // update after refreshing all to avoid holding write lock for too long + *self.node_delegations.write().await = delegations_per_node; + } + + async fn get_delegation_program_details(&self) -> anyhow::Result> { + let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) + .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; + + let delegations = self + .nyxd_client + .get_all_delegator_delegations(&account_id) + .await?; + + let mix_ids: Vec = delegations + .iter() + .map(|delegation| delegation.node_id) + .collect(); + + Ok(mix_ids) + } } -async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> { +async fn historical_count(pool: &DbPool) -> anyhow::Result<(usize, usize)> { let mut conn = pool.acquire().await?; + #[cfg(feature = "sqlite")] let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) .fetch_one(&mut *conn) .await? .cast_checked()?; + #[cfg(feature = "pg")] + let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) + .fetch_one(&mut *conn) + .await? + .unwrap_or(0) + .cast_checked()?; + + #[cfg(feature = "sqlite")] let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) .fetch_one(&mut *conn) .await? .cast_checked()?; + #[cfg(feature = "pg")] + let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) + .fetch_one(&mut *conn) + .await? + .unwrap_or(0) + .cast_checked()?; + Ok((all_historical_gateways, all_historical_mixnodes)) } - -async fn get_delegation_program_details( - network_details: &NymNetworkDetails, - nyxd_addr: &Url, -) -> anyhow::Result> { - let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; - - let client = NyxdClient::connect(config, nyxd_addr.as_str()) - .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; - - let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) - .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; - - let delegations = client.get_all_delegator_delegations(&account_id).await?; - - let mix_ids: Vec = delegations - .iter() - .map(|delegation| delegation.node_id) - .collect(); - - Ok(mix_ids) -} diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/node_delegations.rs b/nym-node-status-api/nym-node-status-api/src/monitor/node_delegations.rs new file mode 100644 index 0000000000..66f91a6082 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/monitor/node_delegations.rs @@ -0,0 +1,53 @@ +use nym_mixnet_contract_common::{NodeId, NymNodeDetails}; +use nym_validator_client::{nyxd::contract_traits::PagedMixnetQueryClient, QueryHttpRpcNyxdClient}; +use std::{collections::HashMap, sync::Arc}; +use tokio::{sync::RwLock, time::Instant}; +use tracing::{info, warn}; + +// abstracts away data structure that holds delegations +#[derive(Clone, Debug)] +pub(crate) struct DelegationsCache { + pub inner: HashMap>, +} + +impl DelegationsCache { + pub(crate) fn new() -> Arc> { + let a = Self { + inner: HashMap::new(), + }; + Arc::new(RwLock::new(a)) + } + + pub(crate) fn delegations_owned( + &self, + node_id: NodeId, + ) -> Option> { + self.inner.get(&node_id).cloned() + } +} + +pub(super) async fn refresh( + client: &QueryHttpRpcNyxdClient, + bonded_nodes: &HashMap, +) -> DelegationsCache { + info!("👥 Refreshing {} node delegations...", bonded_nodes.len()); + let now = Instant::now(); + + let mut delegations_per_node = HashMap::new(); + for node_id in bonded_nodes.keys() { + if let Ok(delegations) = client + .get_all_single_mixnode_delegations(*node_id) + .await + .inspect_err(|err| warn!("Failed to get delegations for {}: {}", node_id, err)) + { + delegations_per_node + .insert(*node_id, delegations.into_iter().map(From::from).collect()); + } + } + let time_taken = Instant::now() - now; + info!("👥 Node delegations refreshed in {}s", time_taken.as_secs(),); + + DelegationsCache { + inner: delegations_per_node, + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/description.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/description.rs new file mode 100644 index 0000000000..aa4096de1f --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/description.rs @@ -0,0 +1,114 @@ +use super::helpers::scrape_and_store_description; +use crate::db::DbPool; +use anyhow::Result; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tracing::{debug, error, instrument, warn}; + +use crate::db::models::ScraperNodeInfo; +use crate::db::queries::get_nodes_for_scraping; + +const DESCRIPTION_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60 * 4); +const QUEUE_CHECK_INTERVAL: Duration = Duration::from_millis(250); +const MAX_CONCURRENT_TASKS: usize = 5; + +static TASK_COUNTER: AtomicUsize = AtomicUsize::new(0); +static TASK_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub struct DescriptionScraper { + pool: DbPool, + description_queue: Arc>>, +} + +impl DescriptionScraper { + pub fn new(pool: DbPool) -> Self { + Self { + pool, + description_queue: Arc::new(Mutex::new(Vec::new())), + } + } + + pub async fn start(&self) { + self.spawn_description_scraper().await; + } + + async fn spawn_description_scraper(&self) { + let pool = self.pool.clone(); + let queue = self.description_queue.clone(); + tracing::info!("Starting description scraper"); + tokio::spawn(async move { + loop { + if let Err(e) = Self::run_description_scraper(&pool, queue.clone()).await { + error!(name: "description_scraper", "Description scraper failed: {}", e); + } + debug!(name: "description_scraper", "Sleeping for {}s", DESCRIPTION_SCRAPE_INTERVAL.as_secs()); + tokio::time::sleep(DESCRIPTION_SCRAPE_INTERVAL).await; + } + }); + } + + #[instrument(level = "info", name = "description_scraper", skip_all)] + async fn run_description_scraper( + pool: &DbPool, + queue: Arc>>, + ) -> Result<()> { + let nodes = get_nodes_for_scraping(pool).await?; + if let Ok(mut queue_lock) = queue.lock() { + queue_lock.extend(nodes); + } else { + warn!("Failed to acquire description queue lock"); + return Ok(()); + } + + Self::process_description_queue(pool, queue).await; + Ok(()) + } + + async fn process_description_queue(pool: &DbPool, queue: Arc>>) { + loop { + let running_tasks = TASK_COUNTER.load(Ordering::Relaxed); + + if running_tasks < MAX_CONCURRENT_TASKS { + let node = { + if let Ok(mut queue_lock) = queue.lock() { + if queue_lock.is_empty() { + TASK_ID_COUNTER.store(0, Ordering::Relaxed); + break; + } + queue_lock.remove(0) + } else { + warn!("Failed to acquire description queue lock"); + break; + } + }; + + TASK_COUNTER.fetch_add(1, Ordering::Relaxed); + let task_id = TASK_ID_COUNTER.fetch_add(1, Ordering::Relaxed); + let pool = pool.clone(); + + tokio::spawn(async move { + match scrape_and_store_description(&pool, node.clone()).await { + Ok(_) => debug!( + "📝 ✅ Description task #{} for node {} complete", + task_id, + node.node_id() + ), + Err(e) => debug!( + "📝 ❌ Description task #{} for {} {} failed: {}", + task_id, + node.node_kind, + node.node_id(), + e + ), + } + TASK_COUNTER.fetch_sub(1, Ordering::Relaxed); + }); + } else { + tokio::time::sleep(QUEUE_CHECK_INTERVAL).await; + } + } + + // TODO After all tasks complete, write results to the DB + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/error.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/error.rs deleted file mode 100644 index 42f96647e3..0000000000 --- a/nym-node-status-api/nym-node-status-api/src/node_scraper/error.rs +++ /dev/null @@ -1,18 +0,0 @@ -use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_node_requests::api::client::NymNodeApiClientError; -use nym_validator_client::client::NodeId; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum NodeScraperError { - #[error("node {node_id} has provided malformed host information ({host}: {source}")] - MalformedHost { - host: String, - node_id: NodeId, - #[source] - source: NymNodeApiClientError, - }, - - #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] - NoHttpPortsAvailable { host: String, node_id: NodeId }, -} diff --git a/nym-node-status-api/nym-node-status-api/src/mixnet_scraper/helpers.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/helpers.rs similarity index 52% rename from nym-node-status-api/nym-node-status-api/src/mixnet_scraper/helpers.rs rename to nym-node-status-api/nym-node-status-api/src/node_scraper/helpers.rs index dfce27ad51..9b861cd4c3 100644 --- a/nym-node-status-api/nym-node-status-api/src/mixnet_scraper/helpers.rs +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/helpers.rs @@ -1,22 +1,19 @@ use crate::{ db::{ - models::{NodeStats, ScraperNodeInfo}, - queries::{ - get_raw_node_stats, insert_daily_node_stats, insert_node_packet_stats, - insert_scraped_node_description, - }, + models::{InsertStatsRecord, NodeStats, ScrapeNodeKind, ScraperNodeInfo}, + queries::insert_scraped_node_description, + DbPool, }, - utils::generate_node_name, + utils::{generate_node_name, now_utc}, }; use ammonia::Builder; use anyhow::{anyhow, Result}; -use chrono::{DateTime, Datelike, Utc}; -use reqwest; use serde::{Deserialize, Serialize}; -use sqlx::SqlitePool; +use sqlx::Transaction; use std::time::Duration; +use time::UtcDateTime; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct NodeDescriptionResponse { pub moniker: Option, pub website: Option, @@ -29,10 +26,11 @@ const DESCRIPTION_URL: &str = "/description"; const PACKET_STATS_URL: &str = "/stats"; // We need this as some of the mixnodes respond with float values for the packet statistics (?????) -pub fn get_packet_value(response: &serde_json::Value, key: &str) -> Option { +pub fn get_packet_value(response: &serde_json::Value, key: &str) -> Option { response .get(key) .and_then(|value| value.as_i64().or_else(|| value.as_f64().map(|f| f as i64))) + .map(|v| v as i32) } pub fn parse_mixnet_stats(response: serde_json::Value) -> Option { @@ -65,7 +63,7 @@ pub fn parse_mixnet_stats(response: serde_json::Value) -> Option { None } -pub fn calculate_packet_difference(current: i64, previous: i64) -> i64 { +pub fn calculate_packet_difference(current: i32, previous: i32) -> i32 { if current >= previous { current - previous } else { @@ -95,10 +93,10 @@ pub fn sanitize_description( const UNKNOWN: &str = "N/A"; let sanitize_field = |opt: Option| -> Option { - Some( - opt.filter(|s| !s.trim().is_empty()) - .map_or_else(|| UNKNOWN.to_string(), |s| sanitizer.clean(&s).to_string()), - ) + Some(opt.filter(|s| !s.trim().is_empty()).map_or_else( + || UNKNOWN.to_string(), + |s| sanitizer.clean(s.trim()).to_string().trim().to_string(), + )) }; let mut moniker = sanitize_field(description.moniker); @@ -116,7 +114,7 @@ pub fn sanitize_description( } } -pub async fn scrape_and_store_description(pool: &SqlitePool, node: &ScraperNodeInfo) -> Result<()> { +pub async fn scrape_and_store_description(pool: &DbPool, node: ScraperNodeInfo) -> Result<()> { let client = build_client()?; let urls = node.contact_addresses(); @@ -151,15 +149,12 @@ pub async fn scrape_and_store_description(pool: &SqlitePool, node: &ScraperNodeI })?; let sanitized_description = sanitize_description(description, *node.node_id()); - insert_scraped_node_description(pool, &node.node_kind, &sanitized_description).await?; + insert_scraped_node_description(pool, node.node_kind.clone(), sanitized_description).await?; Ok(()) } -pub async fn scrape_and_store_packet_stats( - pool: &SqlitePool, - node: &ScraperNodeInfo, -) -> Result<()> { +pub async fn scrape_packet_stats(node: &ScraperNodeInfo) -> Result { let client = build_client()?; let urls = node.contact_addresses(); @@ -187,31 +182,36 @@ pub async fn scrape_and_store_packet_stats( anyhow::anyhow!("Failed to fetch description from any URL: {}", err_msg) })?; - let timestamp = Utc::now(); - let timestamp_utc = timestamp.timestamp(); - insert_node_packet_stats(pool, &node.node_kind, &stats, timestamp_utc).await?; + let timestamp_utc = now_utc(); + let unix_timestamp = timestamp_utc.unix_timestamp(); + let result = InsertStatsRecord { + node_kind: node.node_kind.to_owned(), + timestamp_utc, + unix_timestamp, + stats, + }; - // Update daily stats - update_daily_stats(pool, node, timestamp, &stats).await?; - - Ok(()) + Ok(result) } -pub async fn update_daily_stats( - pool: &SqlitePool, - node: &ScraperNodeInfo, - timestamp: DateTime, +#[cfg(feature = "sqlite")] +pub async fn update_daily_stats_uncommitted( + tx: &mut Transaction<'static, sqlx::Sqlite>, + node_kind: &ScrapeNodeKind, + timestamp: UtcDateTime, current_stats: &NodeStats, ) -> Result<()> { + use crate::db::queries::{get_raw_node_stats, insert_daily_node_stats_uncommitted}; + let date_utc = format!( "{:04}-{:02}-{:02}", timestamp.year(), - timestamp.month(), + timestamp.month() as u8, timestamp.day() ); // Get previous stats - let previous_stats = get_raw_node_stats(pool, node).await?; + let previous_stats = get_raw_node_stats(tx, node_kind).await?; let (diff_received, diff_sent, diff_dropped) = if let Some(prev) = previous_stats { ( @@ -223,9 +223,9 @@ pub async fn update_daily_stats( (0, 0, 0) // No previous stats available }; - insert_daily_node_stats( - pool, - node, + insert_daily_node_stats_uncommitted( + tx, + node_kind, &date_utc, NodeStats { packets_received: diff_received, @@ -237,3 +237,123 @@ pub async fn update_daily_stats( Ok(()) } + +#[cfg(feature = "pg")] +pub async fn update_daily_stats_uncommitted( + tx: &mut Transaction<'static, sqlx::Postgres>, + node_kind: &ScrapeNodeKind, + timestamp: UtcDateTime, + current_stats: &NodeStats, +) -> Result<()> { + use crate::db::queries::{get_raw_node_stats, insert_daily_node_stats_uncommitted}; + + let date_utc = format!( + "{:04}-{:02}-{:02}", + timestamp.year(), + timestamp.month() as u8, + timestamp.day() + ); + + // Get previous stats + let previous_stats = get_raw_node_stats(tx, node_kind).await?; + + let (diff_received, diff_sent, diff_dropped) = if let Some(prev) = previous_stats { + ( + calculate_packet_difference(current_stats.packets_received, prev.packets_received), + calculate_packet_difference(current_stats.packets_sent, prev.packets_sent), + calculate_packet_difference(current_stats.packets_dropped, prev.packets_dropped), + ) + } else { + (0, 0, 0) // No previous stats available + }; + + insert_daily_node_stats_uncommitted( + tx, + node_kind, + &date_utc, + NodeStats { + packets_received: diff_received, + packets_sent: diff_sent, + packets_dropped: diff_dropped, + }, + ) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_get_packet_value() { + let json = json!({ "packets": 100, "dropped": 50.5 }); + assert_eq!(get_packet_value(&json, "packets"), Some(100)); + assert_eq!(get_packet_value(&json, "dropped"), Some(50)); + assert_eq!(get_packet_value(&json, "non_existent"), None); + } + + #[test] + fn test_parse_mixnet_stats() { + let old_format = json!({ + "packets_explicitly_dropped_since_startup": 10, + "packets_sent_since_startup": 100, + "packets_received_since_startup": 200 + }); + let new_format = json!({ + "dropped_since_startup": 20, + "sent_since_startup": 150, + "received_since_startup": 250 + }); + let invalid_format = json!({ "foo": "bar" }); + + let stats1 = parse_mixnet_stats(old_format).unwrap(); + assert_eq!(stats1.packets_dropped, 10); + assert_eq!(stats1.packets_sent, 100); + assert_eq!(stats1.packets_received, 200); + + let stats2 = parse_mixnet_stats(new_format).unwrap(); + assert_eq!(stats2.packets_dropped, 20); + assert_eq!(stats2.packets_sent, 150); + assert_eq!(stats2.packets_received, 250); + + assert!(parse_mixnet_stats(invalid_format).is_none()); + } + + #[test] + fn test_calculate_packet_difference() { + assert_eq!(calculate_packet_difference(100, 50), 50); + assert_eq!(calculate_packet_difference(50, 100), 50); + assert_eq!(calculate_packet_difference(100, 100), 0); + } + + #[test] + fn test_sanitize_description() { + let desc = NodeDescriptionResponse { + moniker: Some(" Moniker ".to_string()), + website: Some("https://example.com".to_string()), + security_contact: Some("".to_string()), + details: None, + }; + + let sanitized = sanitize_description(desc, 123); + assert_eq!(sanitized.moniker, Some("Moniker".to_string())); + assert_eq!(sanitized.website, Some("https://example.com".to_string())); + assert_eq!(sanitized.security_contact, Some("N/A".to_string())); + assert_eq!(sanitized.details, Some("N/A".to_string())); + + let desc_empty = NodeDescriptionResponse { + moniker: None, + website: None, + security_contact: None, + details: None, + }; + let sanitized_empty = sanitize_description(desc_empty, 123); + assert_ne!(sanitized_empty.moniker, Some("N/A".to_string())); // should generate a name + assert_eq!(sanitized_empty.website, Some("N/A".to_string())); + assert_eq!(sanitized_empty.security_contact, Some("N/A".to_string())); + assert_eq!(sanitized_empty.details, Some("N/A".to_string())); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs index f294b47b0f..851db3c1e4 100644 --- a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs @@ -1,284 +1,6 @@ -use crate::db::{models::GatewaySessionsRecord, queries, DbPool}; -use error::NodeScraperError; -use nym_network_defaults::{NymNetworkDetails, DEFAULT_NYM_NODE_HTTP_PORT}; -use nym_node_requests::api::{client::NymNodeApiClientExt, v1::metrics::models::SessionStats}; -use nym_validator_client::{ - client::{NodeId, NymNodeDetails}, - models::{DescribedNodeType, NymNodeDescription}, - NymApiClient, -}; -use time::OffsetDateTime; +pub(crate) mod description; +pub(crate) mod helpers; +pub(crate) mod packet_stats; -use nym_node_metrics::entry::SessionType; -use std::collections::HashMap; -use tokio::time::Duration; -use tracing::instrument; - -mod error; - -const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); -const REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60 * 6); -const STALE_DURATION: Duration = Duration::from_secs(86400 * 365); //one year - -#[instrument(level = "info", name = "metrics_scraper", skip_all)] -pub(crate) async fn spawn_in_background(db_pool: DbPool, nym_api_client_timeout: Duration) { - let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); - - loop { - tracing::info!("Refreshing node self-described metrics..."); - - if let Err(e) = run(&db_pool, &network_defaults, nym_api_client_timeout).await { - tracing::error!( - "Metrics collection failed: {e}, retrying in {}s...", - FAILURE_RETRY_DELAY.as_secs() - ); - - tokio::time::sleep(FAILURE_RETRY_DELAY).await; - } else { - tracing::info!( - "Metrics successfully collected, sleeping for {}s...", - REFRESH_INTERVAL.as_secs() - ); - tokio::time::sleep(REFRESH_INTERVAL).await; - } - } -} - -async fn run( - pool: &DbPool, - network_details: &NymNetworkDetails, - nym_api_client_timeout: Duration, -) -> anyhow::Result<()> { - let default_api_url = network_details - .endpoints - .first() - .expect("rust sdk mainnet default incorrectly configured") - .api_url() - .clone() - .expect("rust sdk mainnet default missing api_url"); - - let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url) - .no_hickory_dns() - .with_timeout(nym_api_client_timeout) - .build::<&str>()?; - - let api_client = NymApiClient { nym_api }; - - //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes - let bonded_nodes = api_client.get_all_bonded_nym_nodes().await?; - let all_nodes = api_client.get_all_described_nodes().await?; //legacy node that did not upgrade the contract bond yet - tracing::debug!("Fetched {} total nodes", all_nodes.len()); - - let mut nodes_to_scrape: HashMap = bonded_nodes - .into_iter() - .map(|n| (n.node_id(), n.into())) - .collect(); - - all_nodes - .into_iter() - .filter(|n| n.contract_node_type != DescribedNodeType::LegacyMixnode) - .for_each(|n| { - nodes_to_scrape.entry(n.node_id).or_insert_with(|| n.into()); - }); - tracing::debug!("Will try to scrape {} nodes", nodes_to_scrape.len()); - - let mut session_records = Vec::new(); - for n in nodes_to_scrape.into_values() { - if let Some(stat) = n.try_scrape_metrics().await { - session_records.push(prepare_session_data(stat, &n)); - } - } - - queries::insert_session_records(pool, session_records) - .await - .map(|_| { - tracing::debug!("Session info written to DB!"); - })?; - let cut_off_date = (OffsetDateTime::now_utc() - STALE_DURATION).date(); - queries::delete_old_records(pool, cut_off_date) - .await - .map(|_| { - tracing::debug!("Cleared old data before {}", cut_off_date); - })?; - - Ok(()) -} - -#[derive(Debug)] -struct MetricsScrapingData { - host: String, - node_id: NodeId, - id_key: String, - port: Option, -} - -impl MetricsScrapingData { - pub fn new( - host: impl Into, - node_id: NodeId, - id_key: String, - port: Option, - ) -> Self { - MetricsScrapingData { - host: host.into(), - node_id, - id_key, - port, - } - } - - #[instrument(level = "info", name = "metrics_scraper", skip_all)] - async fn try_scrape_metrics(&self) -> Option { - match self.try_get_client().await { - Ok(client) => { - match client.get_sessions_metrics().await { - Ok(session_stats) => { - if session_stats.update_time != OffsetDateTime::UNIX_EPOCH { - Some(session_stats) - } else { - //means no data - None - } - } - Err(e) => { - tracing::warn!("{e}"); - None - } - } - } - Err(e) => { - tracing::warn!("{e}"); - None - } - } - } - - async fn try_get_client(&self) -> Result { - // first try the standard port in case the operator didn't put the node behind the proxy, - // then default https (443) - // finally default http (80) - let mut addresses_to_try = vec![ - format!("http://{0}:{DEFAULT_NYM_NODE_HTTP_PORT}", self.host), // 'standard' nym-node - format!("https://{0}", self.host), // node behind https proxy (443) - format!("http://{0}", self.host), // node behind http proxy (80) - ]; - - // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via - // the 'custom_port' since it should have been present in the contract. - - if let Some(port) = self.port { - addresses_to_try.insert(0, format!("http://{0}:{port}", self.host)); - } - - for address in addresses_to_try { - // if provided host was malformed, no point in continuing - let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { - b.with_timeout(Duration::from_secs(5)) - .with_user_agent("node-status-api-metrics-scraper") - .no_hickory_dns() - .build() - }) { - Ok(client) => client, - Err(err) => { - return Err(NodeScraperError::MalformedHost { - host: self.host.to_string(), - node_id: self.node_id, - source: err, - }); - } - }; - - if let Ok(health) = client.get_health().await { - if health.status.is_up() { - return Ok(client); - } - } - } - - Err(NodeScraperError::NoHttpPortsAvailable { - host: self.host.to_string(), - node_id: self.node_id, - }) - } -} - -impl From for MetricsScrapingData { - fn from(value: NymNodeDetails) -> Self { - MetricsScrapingData::new( - value.bond_information.node.host.clone(), - value.node_id(), - value.bond_information.node.identity_key, - value.bond_information.node.custom_http_port, - ) - } -} - -impl From for MetricsScrapingData { - fn from(value: NymNodeDescription) -> Self { - MetricsScrapingData::new( - value.description.host_information.ip_address[0].to_string(), - value.node_id, - value.ed25519_identity_key().to_base58_string(), - None, - ) - } -} - -fn prepare_session_data( - stat: SessionStats, - node_data: &MetricsScrapingData, -) -> GatewaySessionsRecord { - let users_hashes = if !stat.unique_active_users_hashes.is_empty() { - Some(serde_json::to_string(&stat.unique_active_users_hashes).unwrap()) - } else { - None - }; - let vpn_durations = stat - .sessions - .iter() - .filter(|s| SessionType::from_string(&s.typ) == SessionType::Vpn) - .map(|s| s.duration_ms) - .collect::>(); - - let mixnet_durations = stat - .sessions - .iter() - .filter(|s| SessionType::from_string(&s.typ) == SessionType::Mixnet) - .map(|s| s.duration_ms) - .collect::>(); - - let unkown_durations = stat - .sessions - .iter() - .filter(|s| SessionType::from_string(&s.typ) == SessionType::Unknown) - .map(|s| s.duration_ms) - .collect::>(); - - let vpn_sessions = if !vpn_durations.is_empty() { - Some(serde_json::to_string(&vpn_durations).unwrap()) - } else { - None - }; - let mixnet_sessions = if !mixnet_durations.is_empty() { - Some(serde_json::to_string(&mixnet_durations).unwrap()) - } else { - None - }; - let unknown_sessions = if !unkown_durations.is_empty() { - Some(serde_json::to_string(&unkown_durations).unwrap()) - } else { - None - }; - - GatewaySessionsRecord { - gateway_identity_key: node_data.id_key.clone(), - node_id: node_data.node_id as i64, - day: stat.update_time.date(), - unique_active_clients: stat.unique_active_users as i64, - session_started: stat.sessions_started as i64, - users_hashes, - vpn_sessions, - mixnet_sessions, - unknown_sessions, - } -} +pub(crate) use description::DescriptionScraper; +pub(crate) use packet_stats::PacketScraper; diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/packet_stats.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/packet_stats.rs new file mode 100644 index 0000000000..e8a53d7ca4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/packet_stats.rs @@ -0,0 +1,132 @@ +use super::helpers::scrape_packet_stats; +use crate::db::DbPool; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; +use tokio::task::JoinSet; +use tracing::{debug, error, info, instrument, warn}; + +use crate::db::models::{InsertStatsRecord, ScraperNodeInfo}; +use crate::db::queries; + +const PACKET_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60); +const QUEUE_CHECK_INTERVAL: Duration = Duration::from_millis(250); + +static TASK_COUNTER: AtomicUsize = AtomicUsize::new(0); +static TASK_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +pub struct PacketScraper { + pool: DbPool, + max_concurrent_tasks: usize, +} + +impl PacketScraper { + pub fn new(pool: DbPool, max_concurrent_tasks: usize) -> Self { + Self { + pool, + max_concurrent_tasks, + } + } + + pub async fn start(&self) { + self.spawn_packet_scraper().await; + } + + async fn spawn_packet_scraper(&self) { + let pool = self.pool.clone(); + tracing::info!("Starting packet scraper"); + let max_concurrent_tasks = self.max_concurrent_tasks; + + tokio::spawn(async move { + loop { + if let Err(e) = Self::run_packet_scraper(&pool, max_concurrent_tasks).await { + error!(name: "packet_scraper", "Packet scraper failed: {}", e); + } + debug!(name: "packet_scraper", "Sleeping for {}s", PACKET_SCRAPE_INTERVAL.as_secs()); + tokio::time::sleep(PACKET_SCRAPE_INTERVAL).await; + } + }); + } + + #[instrument(level = "info", name = "packet_scraper", skip_all)] + async fn run_packet_scraper(pool: &DbPool, max_concurrent_tasks: usize) -> anyhow::Result<()> { + let queue = queries::get_nodes_for_scraping(pool).await?; + tracing::info!("Adding {} nodes to the queue", queue.len(),); + + let results = Self::process_packet_queue(queue, max_concurrent_tasks).await; + queries::batch_store_packet_stats(pool, results) + .await + .map_err(|err| anyhow::anyhow!("Failed to store packet stats to DB: {err}")) + } + + async fn process_packet_queue( + queue: Vec, + max_concurrent_tasks: usize, + ) -> Arc>> { + let mut queue = queue; + let results = Arc::new(Mutex::new(Vec::new())); + let mut task_set = JoinSet::new(); + + loop { + let running_tasks = TASK_COUNTER.load(Ordering::Relaxed); + + if running_tasks < max_concurrent_tasks { + let node = { + if queue.is_empty() { + TASK_ID_COUNTER.store(0, Ordering::Relaxed); + break; + } + queue.remove(0) + }; + + TASK_COUNTER.fetch_add(1, Ordering::Relaxed); + let task_id = TASK_ID_COUNTER.fetch_add(1, Ordering::Relaxed); + let results_clone = Arc::clone(&results); + + task_set.spawn(async move { + match scrape_packet_stats(&node).await { + Ok(result) => { + // each task contributes their result to a shared vec + results_clone.lock().await.push(result); + debug!( + "📊 ✅ Packet stats task #{} for node {} complete", + task_id, + node.node_id() + ) + } + Err(e) => debug!( + "📊 ❌ Packet stats task #{} for {} {} failed: {}", + task_id, + node.node_kind, + node.node_id(), + e + ), + } + TASK_COUNTER.fetch_sub(1, Ordering::Relaxed); + }); + } else { + tokio::time::sleep(QUEUE_CHECK_INTERVAL).await; + } + } + + // wait for all the tasks to complete before returning their results + let total_count = task_set.len(); + let mut success_count = 0; + while let Some(res) = task_set.join_next().await { + if let Err(err) = res { + warn!("Packet stats task panicked: {err}"); + } else { + success_count += 1; + } + } + let msg = format!("Successfully completed {success_count}/{total_count} tasks ",); + if success_count != total_count { + warn!(msg); + } else { + info!(msg); + } + + results + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs b/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs new file mode 100644 index 0000000000..ee201d555d --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/test_helpers/mod.rs @@ -0,0 +1,176 @@ +#[cfg(test)] +pub mod builders { + use crate::http::models::*; + use nym_validator_client::nym_api::SkimmedNode; + use nym_validator_client::nym_nodes::NodeRole; + use nym_validator_client::models::DeclaredRoles; + use nym_contracts_common::Percent; + use nym_crypto::asymmetric::{ed25519, x25519}; + + /// Builder for creating test Gateway instances + pub struct GatewayBuilder { + gateway: Gateway, + } + + impl GatewayBuilder { + pub fn new() -> Self { + Self { + gateway: Gateway { + gateway_identity_key: "test_gateway".to_string(), + bonded: true, + performance: 95, + self_described: Some(serde_json::json!({})), + explorer_pretty_bond: Some(serde_json::json!({})), + description: nym_node_requests::api::v1::node::models::NodeDescription { + moniker: "Test Gateway".to_string(), + website: "https://example.com".to_string(), + security_contact: "admin@example.com".to_string(), + details: "Test gateway node".to_string(), + }, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: None, + last_updated_utc: "2024-01-01T00:00:00Z".to_string(), + routing_score: 0.95, + config_score: 100, + }, + } + } + + pub fn with_identity(mut self, key: &str) -> Self { + self.gateway.gateway_identity_key = key.to_string(); + self + } + + pub fn with_performance(mut self, performance: u8) -> Self { + self.gateway.performance = performance; + self + } + + pub fn unbonded(mut self) -> Self { + self.gateway.bonded = false; + self + } + + pub fn with_last_probe_result(mut self, result: serde_json::Value) -> Self { + self.gateway.last_probe_result = Some(result); + self + } + + pub fn build(self) -> Gateway { + self.gateway + } + } + + /// Builder for creating test SkimmedNode instances + pub struct SkimmedNodeBuilder { + node: SkimmedNode, + } + + impl SkimmedNodeBuilder { + pub fn new() -> Self { + let ed25519_pk = ed25519::PublicKey::from_bytes(&[1; 32]).unwrap(); + let x25519_pk = x25519::PublicKey::from_bytes(&[2; 32]).unwrap(); + + Self { + node: SkimmedNode { + node_id: 1, + ed25519_identity_pubkey: ed25519_pk, + ip_addresses: vec!["127.0.0.1".parse().unwrap()], + mix_port: 1789, + x25519_sphinx_pubkey: x25519_pk, + role: NodeRole::Mixnode { layer: 1 }, + supported_roles: DeclaredRoles { + entry: false, + mixnode: true, + exit_nr: false, + exit_ipr: false, + }, + entry: None, + performance: Percent::from_percentage_value(95).unwrap(), + }, + } + } + + pub fn with_node_id(mut self, id: u32) -> Self { + self.node.node_id = id; + self + } + + pub fn with_role(mut self, role: NodeRole) -> Self { + self.node.role = role; + self + } + + pub fn as_gateway(mut self) -> Self { + self.node.role = NodeRole::EntryGateway; + self.node.supported_roles = DeclaredRoles { + entry: true, + mixnode: false, + exit_nr: true, + exit_ipr: false, + }; + self + } + + pub fn with_performance(mut self, perf: u8) -> Self { + self.node.performance = Percent::from_percentage_value(perf as u64).unwrap(); + self + } + + pub fn build(self) -> SkimmedNode { + self.node + } + } + + /// Builder for creating test Mixnode instances + pub struct MixnodeBuilder { + mixnode: Mixnode, + } + + impl MixnodeBuilder { + pub fn new() -> Self { + Self { + mixnode: Mixnode { + mix_id: 1, + bonded: true, + is_dp_delegatee: false, + total_stake: 1_000_000, + full_details: Some(serde_json::json!({})), + self_described: Some(serde_json::json!({})), + description: nym_node_requests::api::v1::node::models::NodeDescription { + moniker: "Test Mixnode".to_string(), + website: "https://example.com".to_string(), + security_contact: "admin@example.com".to_string(), + details: "Test mixnode".to_string(), + }, + last_updated_utc: "2024-01-01T00:00:00Z".to_string(), + }, + } + } + + pub fn with_mix_id(mut self, id: u32) -> Self { + self.mixnode.mix_id = id; + self + } + + pub fn with_stake(mut self, stake: i64) -> Self { + self.mixnode.total_stake = stake; + self + } + + pub fn as_dp_delegatee(mut self) -> Self { + self.mixnode.is_dp_delegatee = true; + self + } + + pub fn unbonded(mut self) -> Self { + self.mixnode.bonded = false; + self + } + + pub fn build(self) -> Mixnode { + self.mixnode + } + } +} \ No newline at end of file diff --git a/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs index 61246fa2dc..e103ebaf72 100644 --- a/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/testruns/mod.rs @@ -7,7 +7,6 @@ use tracing::instrument; pub(crate) mod models; mod queue; -pub(crate) use queue::now_utc; pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { tokio::spawn(async move { @@ -25,7 +24,7 @@ pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { }); } -#[instrument(level = "debug", name = "testrun_queue", skip_all)] +#[instrument(level = "info", name = "testrun_queue", skip_all)] async fn run(pool: &DbPool) -> anyhow::Result<()> { tracing::info!("Spawning testruns..."); if pool.is_closed() { @@ -78,7 +77,7 @@ async fn run(pool: &DbPool) -> anyhow::Result<()> { #[instrument(level = "debug", skip_all)] async fn refresh_stale_testruns(pool: &DbPool, refresh_interval: Duration) -> anyhow::Result<()> { - let refresh_interval = chrono::Duration::from_std(refresh_interval)?; + let refresh_interval = time::Duration::try_from(refresh_interval)?; crate::db::queries::testruns::update_testruns_assigned_before(pool, refresh_interval).await?; Ok(()) diff --git a/nym-node-status-api/nym-node-status-api/src/testruns/models.rs b/nym-node-status-api/nym-node-status-api/src/testruns/models.rs index fe4b33384c..309cb8aea8 100644 --- a/nym-node-status-api/nym-node-status-api/src/testruns/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/testruns/models.rs @@ -9,8 +9,165 @@ pub struct GatewayIdentityDto { #[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] pub struct TestRun { - pub id: u32, + pub id: i32, pub identity_key: String, pub status: String, pub log: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gateway_identity_dto_creation() { + let dto = GatewayIdentityDto { + gateway_identity_key: "gateway123".to_string(), + bonded: true, + }; + + assert_eq!(dto.gateway_identity_key, "gateway123"); + assert!(dto.bonded); + } + + #[test] + fn gateway_identity_dto_unbonded() { + let dto = GatewayIdentityDto { + gateway_identity_key: "gateway456".to_string(), + bonded: false, + }; + + assert_eq!(dto.gateway_identity_key, "gateway456"); + assert!(!dto.bonded); + } + + #[test] + fn gateway_identity_dto_clone() { + let original = GatewayIdentityDto { + gateway_identity_key: "gateway789".to_string(), + bonded: true, + }; + + let cloned = original.clone(); + + assert_eq!(cloned.gateway_identity_key, original.gateway_identity_key); + assert_eq!(cloned.bonded, original.bonded); + } + + #[test] + fn test_run_creation() { + let test_run = TestRun { + id: 1, + identity_key: "test_gateway_123".to_string(), + status: "success".to_string(), + log: "Test completed successfully".to_string(), + }; + + assert_eq!(test_run.id, 1); + assert_eq!(test_run.identity_key, "test_gateway_123"); + assert_eq!(test_run.status, "success"); + assert_eq!(test_run.log, "Test completed successfully"); + } + + #[test] + fn test_run_with_error_status() { + let test_run = TestRun { + id: 42, + identity_key: "error_gateway".to_string(), + status: "error".to_string(), + log: "Connection timeout: failed to reach gateway".to_string(), + }; + + assert_eq!(test_run.id, 42); + assert_eq!(test_run.status, "error"); + assert!(test_run.log.contains("Connection timeout")); + } + + #[test] + fn test_run_serialization() { + let test_run = TestRun { + id: 123, + identity_key: "serialization_test".to_string(), + status: "pending".to_string(), + log: "".to_string(), + }; + + // Test that it can be serialized + let serialized = serde_json::to_string(&test_run).unwrap(); + assert!(serialized.contains("\"id\":123")); + assert!(serialized.contains("\"identity_key\":\"serialization_test\"")); + assert!(serialized.contains("\"status\":\"pending\"")); + assert!(serialized.contains("\"log\":\"\"")); + + // Test deserialization + let deserialized: TestRun = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized.id, test_run.id); + assert_eq!(deserialized.identity_key, test_run.identity_key); + assert_eq!(deserialized.status, test_run.status); + assert_eq!(deserialized.log, test_run.log); + } + + #[test] + fn test_run_with_long_log() { + let long_log = "Error: ".to_string() + &"x".repeat(1000); + let test_run = TestRun { + id: i32::MAX, + identity_key: "long_log_test".to_string(), + status: "failed".to_string(), + log: long_log.clone(), + }; + + assert_eq!(test_run.id, i32::MAX); + assert_eq!(test_run.log.len(), 1007); // "Error: " + 1000 x's + assert_eq!(test_run.log, long_log); + } + + #[test] + fn test_run_with_special_characters() { + let test_run = TestRun { + id: 0, + identity_key: "special_chars_∞_√_π".to_string(), + status: "unknown".to_string(), + log: "Test with\nnewlines\ttabs\rand \"quotes\"".to_string(), + }; + + assert_eq!(test_run.id, 0); + assert!(test_run.identity_key.contains('∞')); + assert!(test_run.log.contains('\n')); + assert!(test_run.log.contains('\t')); + assert!(test_run.log.contains('"')); + } + + #[test] + fn test_run_clone() { + let original = TestRun { + id: 999, + identity_key: "clone_test".to_string(), + status: "running".to_string(), + log: "In progress...".to_string(), + }; + + let cloned = original.clone(); + + assert_eq!(cloned.id, original.id); + assert_eq!(cloned.identity_key, original.identity_key); + assert_eq!(cloned.status, original.status); + assert_eq!(cloned.log, original.log); + } + + #[test] + fn test_run_edge_cases() { + // Test with negative ID (edge case) + let test_run = TestRun { + id: -1, + identity_key: "".to_string(), + status: "".to_string(), + log: "".to_string(), + }; + + assert_eq!(test_run.id, -1); + assert!(test_run.identity_key.is_empty()); + assert!(test_run.status.is_empty()); + assert!(test_run.log.is_empty()); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs index 6de0579741..8ed220c6a3 100644 --- a/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs +++ b/nym-node-status-api/nym-node-status-api/src/testruns/queue.rs @@ -1,34 +1,32 @@ use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::db::DbConnection; use crate::testruns::models::TestRun; +use crate::utils::now_utc; use anyhow::anyhow; -use chrono::DateTime; use futures_util::TryStreamExt; -use sqlx::pool::PoolConnection; -use sqlx::Sqlite; -use std::time::SystemTime; pub(crate) async fn try_queue_testrun( - conn: &mut PoolConnection, + conn: &mut DbConnection, identity_key: String, ip_address: String, ) -> anyhow::Result { - let timestamp = now_utc().timestamp(); - let timestamp_pretty = now_utc_as_rfc3339(); + let now = now_utc(); + let timestamp = now.unix_timestamp(); + let timestamp_pretty = now.to_string(); - let items = sqlx::query_as!( - GatewayInfoDto, + let items = crate::db::query_as::( r#"SELECT - id as "id!", - gateway_identity_key as "gateway_identity_key!", - self_described as "self_described?", - explorer_pretty_bond as "explorer_pretty_bond?" + id, + gateway_identity_key, + self_described, + explorer_pretty_bond FROM gateways WHERE gateway_identity_key = ? AND bonded = true ORDER BY gateway_identity_key LIMIT 1"#, - identity_key, ) + .bind(identity_key.clone()) // TODO dz should call .fetch_one // TODO dz replace this in other queries as well .fetch(conn.as_mut()) @@ -48,22 +46,21 @@ pub(crate) async fn try_queue_testrun( // // check if there is already a test run for this gateway // - let items = sqlx::query_as!( - TestRunDto, + let items = crate::db::query_as::( r#"SELECT - id as "id!", - gateway_id as "gateway_id!", - status as "status!", - created_utc as "created_utc!", - ip_address as "ip_address!", - log as "log!", + id, + gateway_id, + status, + created_utc, + ip_address, + log, last_assigned_utc FROM testruns WHERE gateway_id = ? AND status != 2 ORDER BY id DESC LIMIT 1"#, - gateway_id, ) + .bind(gateway_id) .fetch(conn.as_mut()) .try_collect::>() .await?; @@ -71,8 +68,8 @@ pub(crate) async fn try_queue_testrun( if !items.is_empty() { let testrun = items.first().unwrap(); return Ok(TestRun { - id: testrun.id as u32, - identity_key, + id: testrun.id as i32, + identity_key: identity_key.clone(), status: format!( "{}", TestRunStatus::from_repr(testrun.status as u8).unwrap() @@ -84,12 +81,10 @@ pub(crate) async fn try_queue_testrun( // // save test run // - let status = TestRunStatus::Queued as u32; - let log = format!( - "Test for {identity_key} requested at {} UTC\n\n", - timestamp_pretty - ); + let status = TestRunStatus::Queued as i32; + let log = format!("Test for {identity_key} requested at {timestamp_pretty} UTC\n\n"); + #[cfg(feature = "sqlite")] let id = sqlx::query!( "INSERT INTO testruns (gateway_id, status, ip_address, created_utc, log) VALUES (?, ?, ?, ?, ?)", gateway_id, @@ -98,22 +93,30 @@ pub(crate) async fn try_queue_testrun( timestamp, log, ) - .execute(conn.as_mut()) - .await? - .last_insert_rowid(); + .execute(conn.as_mut()) + .await? + .last_insert_rowid(); + + #[cfg(feature = "pg")] + let id = { + let record = sqlx::query!( + "INSERT INTO testruns (gateway_id, status, ip_address, created_utc, log) VALUES ($1, $2, $3, $4, $5) RETURNING id", + gateway_id as i32, + status, + ip_address, + timestamp, + log, + ) + .fetch_one(conn.as_mut()) + .await?; + record.id as i64 + }; Ok(TestRun { - id: id as u32, - identity_key, + #[allow(clippy::useless_conversion)] + id: id.try_into().unwrap(), + identity_key: identity_key.clone(), status: format!("{}", TestRunStatus::Queued), log, }) } - -pub fn now_utc() -> DateTime { - SystemTime::now().into() -} - -pub fn now_utc_as_rfc3339() -> String { - now_utc().to_rfc3339() -} diff --git a/nym-node-status-api/nym-node-status-api/src/utils.rs b/nym-node-status-api/nym-node-status-api/src/utils.rs index f315cee744..044aee6c9d 100644 --- a/nym-node-status-api/nym-node-status-api/src/utils.rs +++ b/nym-node-status-api/nym-node-status-api/src/utils.rs @@ -2,6 +2,7 @@ use cosmwasm_std::Decimal; use itertools::Itertools; use rand::prelude::SliceRandom; use rand::SeedableRng; +use tracing::error; // pub(crate) fn generate_node_name(identity: ed25519::PublicKey) -> String { pub(crate) fn generate_node_name(node_id: i64) -> String { @@ -21,9 +22,9 @@ pub(crate) fn generate_node_name(node_id: i64) -> String { #[allow(clippy::items_after_test_module)] #[cfg(test)] mod test { - use rand::Rng; - use super::*; + use rand::Rng; + use std::str::FromStr; #[test] fn generate_node_name_should_be_deterministic() { @@ -39,6 +40,69 @@ mod test { let node_name_same = generate_node_name(node_id); assert_eq!(node_name, node_name_same); } + + #[test] + fn test_decimal_to_i64() { + // Test with a simple decimal + let dec1 = Decimal::from_str("123.456").unwrap(); + assert_eq!(decimal_to_i64(dec1), 123); + + // Test with a decimal that has more than 6 decimal places + let dec2 = Decimal::from_str("123.456789").unwrap(); + assert_eq!(decimal_to_i64(dec2), 123); + + // Test with a decimal that rounds up + let dec3 = Decimal::from_str("123.9999999").unwrap(); + assert_eq!(decimal_to_i64(dec3), 124); + + // Test with a zero decimal + let dec4 = Decimal::zero(); + assert_eq!(decimal_to_i64(dec4), 0); + + // Test with a large decimal + let dec5 = Decimal::from_str("1234567890.123456").unwrap(); + assert_eq!(decimal_to_i64(dec5), 1234567890); + } + + #[test] + fn test_unix_timestamp_to_utc_rfc3339() { + // Test with a known timestamp + let ts1 = 1672531199; // 2022-12-31 23:59:59 UTC + assert_eq!(unix_timestamp_to_utc_rfc3339(ts1), "2022-12-31T23:59:59Z"); + + // Test with the Unix epoch + let ts2 = 0; + assert_eq!(unix_timestamp_to_utc_rfc3339(ts2), "1970-01-01T00:00:00Z"); + } + + #[test] + fn test_numerical_checked_cast() { + // Test successful cast + let val1: u32 = 123; + let res1: anyhow::Result = val1.cast_checked(); + assert_eq!(res1.unwrap(), 123u64); + + // Test failing cast + let val2: i64 = -1; + let res2: anyhow::Result = val2.cast_checked(); + assert!(res2.is_err()); + } +} + +pub(crate) fn now_utc() -> time::UtcDateTime { + time::UtcDateTime::now() +} + +pub(crate) fn unix_timestamp_to_utc_rfc3339(unix_timestamp: i64) -> String { + let timestamp = time::UtcDateTime::UNIX_EPOCH + time::Duration::seconds(unix_timestamp); + timestamp + .format(&time::format_description::well_known::Rfc3339) + // unwrap: time-rs guarantees that output will be valid according to spec + // https://time-rs.github.io/book/api/well-known-format-descriptions.html + .unwrap_or_else(|e| { + error!("Formatting {} as RFC3339 failed: {}", timestamp, e); + String::from("invalid_date") + }) } pub trait NumericalCheckedCast diff --git a/nym-node-status-api/nym-node-status-client/Cargo.toml b/nym-node-status-api/nym-node-status-client/Cargo.toml index a7dc5899fb..c5ee68d501 100644 --- a/nym-node-status-api/nym-node-status-client/Cargo.toml +++ b/nym-node-status-api/nym-node-status-client/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-client" -version = "0.1.0" +version = "0.2.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -15,11 +15,10 @@ readme.workspace = true [dependencies] anyhow = { workspace = true } -chrono = { workspace = true } bincode = { workspace = true } -nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] } -nym-http-api-client = { path = "../../common/http-api-client" } -reqwest = { workspace = true } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde", ] } +reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +time = { workspace = true } tracing = { workspace = true } diff --git a/nym-node-status-api/nym-node-status-client/src/api.rs b/nym-node-status-api/nym-node-status-client/src/api.rs index 46d2fb7cd4..c41b25bf89 100644 --- a/nym-node-status-api/nym-node-status-client/src/api.rs +++ b/nym-node-status-api/nym-node-status-client/src/api.rs @@ -15,4 +15,11 @@ impl ApiPaths { pub(super) fn submit_results(&self, testrun_id: impl Display) -> String { format!("{}/internal/testruns/{}", self.server_address, testrun_id) } + + pub(super) fn submit_results_v2(&self, testrun_id: impl Display) -> String { + format!( + "{}/internal/testruns/{}/v2", + self.server_address, testrun_id + ) + } } diff --git a/nym-node-status-api/nym-node-status-client/src/lib.rs b/nym-node-status-api/nym-node-status-client/src/lib.rs index 260b1b510d..e46897d8e4 100644 --- a/nym-node-status-api/nym-node-status-client/src/lib.rs +++ b/nym-node-status-api/nym-node-status-client/src/lib.rs @@ -1,8 +1,8 @@ -use crate::models::{get_testrun, submit_results, TestrunAssignment}; +use crate::models::{get_testrun, submit_results, submit_results_v2, TestrunAssignment}; use anyhow::bail; use api::ApiPaths; use nym_crypto::asymmetric::ed25519::{PrivateKey, Signature}; -use tracing::instrument; +use tracing::{instrument, warn}; mod api; pub mod auth; @@ -16,9 +16,20 @@ pub struct NsApiClient { impl NsApiClient { pub fn new(server_ip: &str, server_port: u16, auth_key: PrivateKey) -> Self { - let server_address = format!("{}:{}", server_ip, server_port); + let server_address = format!("{server_ip}:{server_port}"); let api = ApiPaths::new(server_address); - let client = reqwest::Client::new(); + let user_agent = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + let client = reqwest::Client::builder() + .user_agent(user_agent) + .build() + .inspect_err(|err| { + warn!( + "Failed to create client with user agent, falling back to default ({})", + err + ) + }) + // failing to set user agent shouldn't be a critical error + .unwrap_or_default(); Self { api, @@ -33,7 +44,7 @@ impl NsApiClient { let payload = get_testrun::Payload { agent_public_key: self.auth_key.public_key(), - timestamp: chrono::offset::Utc::now().timestamp(), + timestamp: time::UtcDateTime::now().unix_timestamp(), }; let signature = self.sign_message(&payload)?; let request = get_testrun::GetTestrunRequest { payload, signature }; @@ -94,6 +105,37 @@ impl NsApiClient { Ok(()) } + #[instrument(level = "debug", skip(self, probe_result))] + pub async fn submit_results_with_context( + &self, + testrun_id: i32, + probe_result: String, + assigned_at_utc: i64, + gateway_identity_key: String, + ) -> anyhow::Result<()> { + let target_url = self.api.submit_results_v2(testrun_id); + + let payload = submit_results_v2::Payload { + probe_result, + agent_public_key: self.auth_key.public_key(), + assigned_at_utc, + gateway_identity_key, + }; + let signature = self.sign_message(&payload)?; + let submit_results = submit_results_v2::SubmitResultsV2 { payload, signature }; + + let res = self + .client + .post(target_url) + .json(&submit_results) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted results with context: {})", res.status()); + Ok(()) + } + fn sign_message(&self, message: &T) -> anyhow::Result where T: serde::Serialize, diff --git a/nym-node-status-api/nym-node-status-client/src/models.rs b/nym-node-status-api/nym-node-status-client/src/models.rs index 05860259ba..c828c5371c 100644 --- a/nym-node-status-api/nym-node-status-client/src/models.rs +++ b/nym-node-status-api/nym-node-status-client/src/models.rs @@ -36,7 +36,7 @@ pub mod get_testrun { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct TestrunAssignment { - pub testrun_id: i64, + pub testrun_id: i32, pub assigned_at_utc: i64, pub gateway_identity_key: String, } @@ -74,3 +74,38 @@ pub mod submit_results { } } } + +pub mod submit_results_v2 { + use crate::auth::SignedRequest; + + use super::*; + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct Payload { + pub probe_result: String, + pub agent_public_key: PublicKey, + pub assigned_at_utc: i64, + pub gateway_identity_key: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct SubmitResultsV2 { + pub payload: Payload, + pub signature: Signature, + } + + impl SignedRequest for SubmitResultsV2 { + type Payload = Payload; + + fn public_key(&self) -> &PublicKey { + &self.payload.agent_public_key + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn payload(&self) -> &Self::Payload { + &self.payload + } + } +} diff --git a/explorer-nextjs/.gitignore b/nym-node-status-api/nym-node-status-ui/.gitignore similarity index 68% rename from explorer-nextjs/.gitignore rename to nym-node-status-api/nym-node-status-ui/.gitignore index fd3dbb571a..5ef6a52078 100644 --- a/explorer-nextjs/.gitignore +++ b/nym-node-status-api/nym-node-status-ui/.gitignore @@ -3,8 +3,12 @@ # dependencies /node_modules /.pnp -.pnp.js -.yarn/install-state.gz +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions # testing /coverage @@ -24,9 +28,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* -# local env files -.env*.local +# env files (can opt-in for committing if needed) +.env* # vercel .vercel diff --git a/nym-node-status-api/nym-node-status-ui/README.md b/nym-node-status-api/nym-node-status-ui/README.md new file mode 100644 index 0000000000..eb516bb158 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/README.md @@ -0,0 +1,18 @@ +# Node Status UI + +## Build + +```bash +pnpm build +``` + +## Local development + +First, run the development server: + +```bash +pnpm i +pnpm dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. diff --git a/nym-node-status-api/nym-node-status-ui/biome.json b/nym-node-status-api/nym-node-status-ui/biome.json new file mode 100644 index 0000000000..4c51b66ca6 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/biome.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [ + ".next" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "bracketSpacing": true, + "indentWidth": 2 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "error", + "useExhaustiveDependencies": "warn" + }, + "suspicious": { + "noExplicitAny": "off", + "noArrayIndexKey": "warn" + }, + "style": { + "useTemplate": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "overrides": [ + { + "include": ["client/**"], + "linter": { + "rules": { + "style": { + "noUselessElse": "off", + "noParameterAssign": "off", + "noNonNullAssertion": "off" + }, + "complexity": { + "noUselessSwitchCase": "off", + "noForEach": "off" + } + } + } + } + ] +} diff --git a/nym-node-status-api/nym-node-status-ui/next.config.ts b/nym-node-status-api/nym-node-status-ui/next.config.ts new file mode 100644 index 0000000000..e9ffa3083a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/nym-node-status-api/nym-node-status-ui/package.json b/nym-node-status-api/nym-node-status-ui/package.json new file mode 100644 index 0000000000..610a288446 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/package.json @@ -0,0 +1,38 @@ +{ + "name": "nym-node-status-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "biome check ./src", + "lint:fix": "biome check ./src --fix", + "generate:openapi-client": "npx @hey-api/openapi-ts -o src/client -p '@tanstack/react-query' -i https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^7.2.0", + "@mui/material": "^7.2.0", + "@mui/material-nextjs": "^7.2.0", + "@mui/x-charts": "^8.8.0", + "@mui/x-date-pickers": "^7.29.4", + "@tanstack/react-query": "^5.83.0", + "dayjs": "^1.11.13", + "material-react-table": "^3.2.1", + "next": "^15.4.1", + "openapi-typescript": "^7.5.2", + "react": "^19.0.0", + "react-country-flag": "^3.1.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@tanstack/react-query-devtools": "^5.83.0", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml new file mode 100644 index 0000000000..ab4a48acac --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml @@ -0,0 +1,2185 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': + specifier: ^11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/icons-material': + specifier: ^7.2.0 + version: 7.2.0(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/material': + specifier: ^7.2.0 + version: 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/material-nextjs': + specifier: ^7.2.0 + version: 7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + '@mui/x-charts': + specifier: ^8.8.0 + version: 8.8.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/x-date-pickers': + specifier: ^7.29.4 + version: 7.29.4(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-query': + specifier: ^5.83.0 + version: 5.83.0(react@19.0.0) + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + material-react-table: + specifier: ^3.2.1 + version: 3.2.1(e4be24f02074e47c0e16001efb50991b) + next: + specifier: ^15.4.1 + version: 15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + openapi-typescript: + specifier: ^7.5.2 + version: 7.5.2(typescript@5.7.3) + react: + specifier: ^19.0.0 + version: 19.0.0 + react-country-flag: + specifier: ^3.1.0 + version: 3.1.0(react@19.0.0) + react-dom: + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@tanstack/react-query-devtools': + specifier: ^5.83.0 + version: 5.83.0(@tanstack/react-query@5.83.0(react@19.0.0))(react@19.0.0) + '@types/node': + specifier: ^20 + version: 20.17.14 + '@types/react': + specifier: ^19 + version: 19.0.7 + '@types/react-dom': + specifier: ^19 + version: 19.0.3(@types/react@19.0.7) + typescript: + specifier: ^5 + version: 5.7.3 + +packages: + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.5': + resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.5': + resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.5': + resolution: {integrity: sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.5': + resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/runtime@1.4.4': + resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@mui/core-downloads-tracker@7.2.0': + resolution: {integrity: sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==} + + '@mui/icons-material@7.2.0': + resolution: {integrity: sha512-gRCspp3pfjHQyTmSOmYw7kUQTd9Udpdan4R8EnZvqPeoAtHnPzkvjBrBqzKaoAbbBp5bGF7BcD18zZJh4nwu0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^7.2.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/material-nextjs@7.2.0': + resolution: {integrity: sha512-/W2iKkjeOdaYBu5xNYi/w5HUX2C4HHefSMW7UgCvTKl90yy1puE7kmAgv/gxBghqhEE27cNWdevRrnvVhNRaUA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/cache': ^11.11.0 + '@emotion/react': ^11.11.4 + '@emotion/server': ^11.11.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + next: ^13.0.0 || ^14.0.0 || ^15.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/cache': + optional: true + '@emotion/server': + optional: true + '@types/react': + optional: true + + '@mui/material@7.2.0': + resolution: {integrity: sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^7.2.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@7.2.0': + resolution: {integrity: sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@7.2.0': + resolution: {integrity: sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@7.2.0': + resolution: {integrity: sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@7.4.4': + resolution: {integrity: sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@7.2.0': + resolution: {integrity: sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/x-charts-vendor@8.5.3': + resolution: {integrity: sha512-H05cb0c2qfRhWLPcwtiIU8BOcKTrMNvhgmRAvJJXpmlirOA1km8dUlR71VeUvJiCthhVIHKyFkPPzFYKgHAfng==} + + '@mui/x-charts@8.8.0': + resolution: {integrity: sha512-tqdwKoUpo8u+KZdEWO4C21Q0P3HOL/DadAZSMmTdtO1LDCO/m4S8UtHFpj2B0pZuikdiBJ5bz49I+nfBfK1Xng==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/x-date-pickers@7.29.4': + resolution: {integrity: sha512-wJ3tsqk/y6dp+mXGtT9czciAMEO5Zr3IIAHg9x6IL0Eqanqy0N3chbmQQZv3iq0m2qUpQDLvZ4utZBUTJdjNzw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + date-fns: ^2.25.0 || ^3.2.0 || ^4.0.0 + date-fns-jalali: ^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 || ^3.0.0 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + + '@mui/x-internal-gestures@0.2.1': + resolution: {integrity: sha512-7Po6F4/RdUrFyRwiwvh5ZNeY/bi8wavTCUe+stKAyMliKpgcYiEtH7ywTgroOEq0o56fIpyPzwC4+bbGwYFnvA==} + + '@mui/x-internals@7.29.0': + resolution: {integrity: sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@mui/x-internals@8.8.0': + resolution: {integrity: sha512-qTRK5oINkAjZ7sIHpSnESLNq1xtQUmmfmGscYUSEP0uHoYh6pKkNWH9+7yzggRHuTv+4011VBwN9s+efrk+xZg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@next/env@15.4.1': + resolution: {integrity: sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A==} + + '@next/swc-darwin-arm64@15.4.1': + resolution: {integrity: sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.4.1': + resolution: {integrity: sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.4.1': + resolution: {integrity: sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.4.1': + resolution: {integrity: sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.4.1': + resolution: {integrity: sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.4.1': + resolution: {integrity: sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.4.1': + resolution: {integrity: sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.4.1': + resolution: {integrity: sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.20.1': + resolution: {integrity: sha512-TYiTDtuItiv95YMsrRxyCs1HKLrDPtTvpaD3+kDKXBnFDeJuYKZ+eHXpCr6YeN4inxfVBs7DLhHsQcs9srddyQ==} + + '@redocly/openapi-core@1.27.2': + resolution: {integrity: sha512-qVrDc27DHpeO2NRCMeRdb4299nijKQE3BY0wrA+WUHlOLScorIi/y7JzammLk22IaTvjR9Mv9aTAdjE1aUwJnA==} + engines: {node: '>=14.19.0', npm: '>=7.0.0'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tanstack/match-sorter-utils@8.19.4': + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + + '@tanstack/query-core@5.83.0': + resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==} + + '@tanstack/query-devtools@5.81.2': + resolution: {integrity: sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==} + + '@tanstack/react-query-devtools@5.83.0': + resolution: {integrity: sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ==} + peerDependencies: + '@tanstack/react-query': ^5.83.0 + react: ^18 || ^19 + + '@tanstack/react-query@5.83.0': + resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.20.6': + resolution: {integrity: sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.11.2': + resolution: {integrity: sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/table-core@8.20.5': + resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.11.2': + resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/node@20.17.14': + resolution: {integrity: sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@19.0.3': + resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.0.7': + resolution: {integrity: sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==} + + agent-base@7.1.3: + resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} + engines: {node: '>= 14'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bezier-easing@2.1.0: + resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001695: + resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + highlight-words@2.0.0: + resolution: {integrity: sha512-If5n+IhSBRXTScE7wl16VPmd+44Vy7kof24EdqhjsZsDuHikpv1OCagVcJFpB4fS4UPUniedlWqrjIO8vWOsIQ==} + engines: {node: '>= 20', npm: '>= 9'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + index-to-position@0.1.2: + resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==} + engines: {node: '>=18'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + material-react-table@3.2.1: + resolution: {integrity: sha512-sQtTf7bETpkPN2Hm5BVtz89wrfXCVQguz6XlwMChSnfKFO5QCKAJJC5aSIKnUc3S0AvTz/k/ILi00FnnY1Gixw==} + engines: {node: '>=16'} + peerDependencies: + '@emotion/react': '>=11.13' + '@emotion/styled': '>=11.13' + '@mui/icons-material': '>=6' + '@mui/material': '>=6' + '@mui/x-date-pickers': '>=7.15' + react: '>=18.0' + react-dom: '>=18.0' + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@15.4.1: + resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + openapi-typescript@7.5.2: + resolution: {integrity: sha512-W/QXuQz0Fa3bGY6LKoqTCgrSX+xI/ST+E5RXo2WBmp3WwgXCWKDJPHv5GZmElF4yLCccnqYsakBDOJikHZYGRw==} + hasBin: true + peerDependencies: + typescript: ^5.x + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.1.0: + resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==} + engines: {node: '>=18'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + react-country-flag@3.1.0: + resolution: {integrity: sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} + peerDependencies: + react: ^19.0.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@19.1.0: + resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} + engines: {node: '>=0.10.0'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-color@9.4.0: + resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} + engines: {node: '>=12'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.33.0: + resolution: {integrity: sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==} + engines: {node: '>=16'} + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + +snapshots: + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.26.5': + dependencies: + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.26.5 + '@babel/types': 7.26.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.5': + dependencies: + '@babel/types': 7.26.5 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.5 + '@babel/types': 7.26.5 + + '@babel/traverse@7.26.5': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.5 + '@babel/parser': 7.26.5 + '@babel/template': 7.25.9 + '@babel/types': 7.26.5 + debug: 4.4.0(supports-color@9.4.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.5': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@emnapi/runtime@1.4.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.25.9 + '@babel/runtime': 7.26.0 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.3.1': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.0.0) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.7 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.0.0) + '@emotion/utils': 1.4.2 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.7 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.0.0)': + dependencies: + react: 19.0.0 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.4.4 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + + '@jridgewell/gen-mapping@0.3.8': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mui/core-downloads-tracker@7.2.0': {} + + '@mui/icons-material@7.2.0(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.7 + + '@mui/material-nextjs@7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + next: 15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@emotion/cache': 11.14.0 + '@types/react': 19.0.7 + + '@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/core-downloads-tracker': 7.2.0 + '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/types': 7.4.4(@types/react@19.0.7) + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@19.0.7) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-is: 19.1.0 + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@types/react': 19.0.7 + + '@mui/private-theming@7.2.0(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.7 + + '@mui/styled-engine@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + + '@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/private-theming': 7.2.0(@types/react@19.0.7)(react@19.0.0) + '@mui/styled-engine': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(react@19.0.0) + '@mui/types': 7.4.4(@types/react@19.0.7) + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@types/react': 19.0.7 + + '@mui/types@7.4.4(@types/react@19.0.7)': + dependencies: + '@babel/runtime': 7.27.6 + optionalDependencies: + '@types/react': 19.0.7 + + '@mui/utils@7.2.0(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/types': 7.4.4(@types/react@19.0.7) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.0.0 + react-is: 19.1.0 + optionalDependencies: + '@types/react': 19.0.7 + + '@mui/x-charts-vendor@8.5.3': + dependencies: + '@babel/runtime': 7.27.6 + '@types/d3-color': 3.1.3 + '@types/d3-delaunay': 6.0.4 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-color: 3.1.0 + d3-delaunay: 6.0.4 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + delaunator: 5.0.1 + robust-predicates: 3.0.2 + + '@mui/x-charts@8.8.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + '@mui/x-charts-vendor': 8.5.3 + '@mui/x-internal-gestures': 0.2.1 + '@mui/x-internals': 8.8.0(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + bezier-easing: 2.1.0 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + reselect: 5.1.1 + use-sync-external-store: 1.5.0(react@19.0.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + transitivePeerDependencies: + - '@types/react' + + '@mui/x-date-pickers@7.29.4(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + '@mui/x-internals': 7.29.0(@types/react@19.0.7)(react@19.0.0) + '@types/react-transition-group': 4.4.12(@types/react@19.0.7) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + dayjs: 1.11.13 + transitivePeerDependencies: + - '@types/react' + + '@mui/x-internal-gestures@0.2.1': + dependencies: + '@babel/runtime': 7.27.6 + + '@mui/x-internals@7.29.0(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + react: 19.0.0 + transitivePeerDependencies: + - '@types/react' + + '@mui/x-internals@8.8.0(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@mui/system': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/utils': 7.2.0(@types/react@19.0.7)(react@19.0.0) + react: 19.0.0 + reselect: 5.1.1 + transitivePeerDependencies: + - '@types/react' + + '@next/env@15.4.1': {} + + '@next/swc-darwin-arm64@15.4.1': + optional: true + + '@next/swc-darwin-x64@15.4.1': + optional: true + + '@next/swc-linux-arm64-gnu@15.4.1': + optional: true + + '@next/swc-linux-arm64-musl@15.4.1': + optional: true + + '@next/swc-linux-x64-gnu@15.4.1': + optional: true + + '@next/swc-linux-x64-musl@15.4.1': + optional: true + + '@next/swc-win32-arm64-msvc@15.4.1': + optional: true + + '@next/swc-win32-x64-msvc@15.4.1': + optional: true + + '@popperjs/core@2.11.8': {} + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.20.1': {} + + '@redocly/openapi-core@1.27.2(supports-color@9.4.0)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.20.1 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@9.4.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + node-fetch: 2.7.0 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - encoding + - supports-color + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tanstack/match-sorter-utils@8.19.4': + dependencies: + remove-accents: 0.5.0 + + '@tanstack/query-core@5.83.0': {} + + '@tanstack/query-devtools@5.81.2': {} + + '@tanstack/react-query-devtools@5.83.0(@tanstack/react-query@5.83.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/query-devtools': 5.81.2 + '@tanstack/react-query': 5.83.0(react@19.0.0) + react: 19.0.0 + + '@tanstack/react-query@5.83.0(react@19.0.0)': + dependencies: + '@tanstack/query-core': 5.83.0 + react: 19.0.0 + + '@tanstack/react-table@8.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/table-core': 8.20.5 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@tanstack/react-virtual@3.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/virtual-core': 3.11.2 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@tanstack/table-core@8.20.5': {} + + '@tanstack/virtual-core@3.11.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/node@20.17.14': + dependencies: + undici-types: 6.19.8 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@19.0.3(@types/react@19.0.7)': + dependencies: + '@types/react': 19.0.7 + + '@types/react-transition-group@4.4.12(@types/react@19.0.7)': + dependencies: + '@types/react': 19.0.7 + + '@types/react@19.0.7': + dependencies: + csstype: 3.1.3 + + agent-base@7.1.3: {} + + ansi-colors@4.1.3: {} + + argparse@2.0.1: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.26.0 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + + balanced-match@1.0.2: {} + + bezier-easing@2.1.0: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001695: {} + + change-case@5.4.4: {} + + client-only@0.0.1: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + optional: true + + color-name@1.1.4: + optional: true + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + colorette@1.4.0: {} + + convert-source-map@1.9.0: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + dayjs@1.11.13: {} + + debug@4.4.0(supports-color@9.4.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 9.4.0 + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + detect-libc@2.0.4: + optional: true + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.27.6 + csstype: 3.1.3 + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + escape-string-regexp@4.0.0: {} + + fast-deep-equal@3.1.3: {} + + find-root@1.1.0: {} + + function-bind@1.1.2: {} + + globals@11.12.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + highlight-words@2.0.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + https-proxy-agent@7.0.6(supports-color@9.4.0): + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + index-to-position@0.1.2: {} + + internmap@2.0.3: {} + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: + optional: true + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + lines-and-columns@1.2.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + material-react-table@3.2.1(e4be24f02074e47c0e16001efb50991b): + dependencies: + '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/icons-material': 7.2.0(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.7)(react@19.0.0) + '@mui/material': 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/x-date-pickers': 7.29.4(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/match-sorter-utils': 8.19.4 + '@tanstack/react-table': 8.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tanstack/react-virtual': 3.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + highlight-words: 2.0.0 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + ms@2.1.3: {} + + nanoid@3.3.8: {} + + next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@next/env': 15.4.1 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001695 + postcss: 8.4.31 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + styled-jsx: 5.1.6(react@19.0.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.4.1 + '@next/swc-darwin-x64': 15.4.1 + '@next/swc-linux-arm64-gnu': 15.4.1 + '@next/swc-linux-arm64-musl': 15.4.1 + '@next/swc-linux-x64-gnu': 15.4.1 + '@next/swc-linux-x64-musl': 15.4.1 + '@next/swc-win32-arm64-msvc': 15.4.1 + '@next/swc-win32-x64-msvc': 15.4.1 + sharp: 0.34.3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + object-assign@4.1.1: {} + + openapi-typescript@7.5.2(typescript@5.7.3): + dependencies: + '@redocly/openapi-core': 1.27.2(supports-color@9.4.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.1.0 + supports-color: 9.4.0 + typescript: 5.7.3 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - encoding + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-json@8.1.0: + dependencies: + '@babel/code-frame': 7.26.2 + index-to-position: 0.1.2 + type-fest: 4.33.0 + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + pluralize@8.0.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + react-country-flag@3.1.0(react@19.0.0): + dependencies: + react: 19.0.0 + + react-dom@19.0.0(react@19.0.0): + dependencies: + react: 19.0.0 + scheduler: 0.25.0 + + react-is@16.13.1: {} + + react-is@19.1.0: {} + + react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/runtime': 7.27.6 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + react@19.0.0: {} + + regenerator-runtime@0.14.1: {} + + remove-accents@0.5.0: {} + + require-from-string@2.0.2: {} + + reselect@5.1.1: {} + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + robust-predicates@3.0.2: {} + + scheduler@0.25.0: {} + + semver@7.7.2: + optional: true + + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + optional: true + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + styled-jsx@5.1.6(react@19.0.0): + dependencies: + client-only: 0.0.1 + react: 19.0.0 + + stylis@4.2.0: {} + + supports-color@9.4.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + type-fest@4.33.0: {} + + typescript@5.7.3: {} + + undici-types@6.19.8: {} + + uri-js-replace@1.0.1: {} + + use-sync-external-store@1.5.0(react@19.0.0): + dependencies: + react: 19.0.0 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + yaml-ast-parser@0.0.43: {} + + yaml@1.10.2: {} + + yargs-parser@21.1.1: {} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx b/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx new file mode 100644 index 0000000000..83d23fa86d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx @@ -0,0 +1,11 @@ +"use client"; + +export default function RootLayout(props: { children: React.ReactNode }) { + return ( + + + {props.children} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/page.tsx new file mode 100644 index 0000000000..b77b535926 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +export default function Home() { + return ( +
TODO
+ ); +} diff --git a/explorer-nextjs/tsconfig.json b/nym-node-status-api/nym-node-status-ui/tsconfig.json similarity index 51% rename from explorer-nextjs/tsconfig.json rename to nym-node-status-api/nym-node-status-ui/tsconfig.json index 701d0e6984..8ed7498522 100644 --- a/explorer-nextjs/tsconfig.json +++ b/nym-node-status-api/nym-node-status-ui/tsconfig.json @@ -1,16 +1,14 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, - "module": "CommonJS", + "module": "esnext", + "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", @@ -21,23 +19,9 @@ } ], "paths": { - "@/*": [ - "./*" - ], - "@assets/*": [ - "../assets/*" - ] - }, - "moduleResolution": "node", - "target": "ES2017" + "@/*": ["./src/*"] + } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] } diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 96fc4667be..0dceb6aedb 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.9.0" +version = "1.17.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -20,8 +20,10 @@ arc-swap = { workspace = true } bip39 = { workspace = true, features = ["zeroize"] } bs58.workspace = true bloomfilter = { workspace = true } -celes = { workspace = true } # country codes +celes = { workspace = true } +cfg-if = { workspace = true }# country codes colored = { workspace = true } +console-subscriber = { workspace = true, optional = true } csv = { workspace = true } clap = { workspace = true, features = ["cargo", "env"] } futures = { workspace = true } @@ -50,11 +52,15 @@ nym-bin-common = { path = "../common/bin-common", features = [ "basic_tracing", "output_format", ] } -nym-client-core-config-types = { path = "../common/client-core/config-types" } +nym-client-core-config-types = { path = "../common/client-core/config-types", features = [ + "disk-persistence", +] } nym-config = { path = "../common/config" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } +nym-noise = { path = "../common/nymnoise" } +nym-noise-keys = { path = "../common/nymnoise/keys" } nym-pemstore = { path = "../common/pemstore" } nym-sphinx-acknowledgements = { path = "../common/nymsphinx/acknowledgements" } nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" } @@ -63,6 +69,7 @@ nym-sphinx-types = { path = "../common/nymsphinx/types" } nym-sphinx-forwarding = { path = "../common/nymsphinx/forwarding" } nym-sphinx-routing = { path = "../common/nymsphinx/routing" } nym-sphinx-params = { path = "../common/nymsphinx/params" } +nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } @@ -83,13 +90,18 @@ tower-http = { workspace = true, features = ["fs"] } utoipa = { workspace = true, features = ["axum_extras", "time"] } utoipa-swagger-ui = { workspace = true, features = ["axum"] } -nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } -nym-node-requests = { path = "nym-node-requests", default-features = false, features = ["openapi"] } +nym-http-api-common = { path = "../common/http-api-common", features = [ + "utoipa", + "output", + "middleware", +] } +nym-node-requests = { path = "nym-node-requests", default-features = false, features = [ + "openapi", +] } nym-node-metrics = { path = "nym-node-metrics" } # nodes: nym-gateway = { path = "../gateway" } -nym-authenticator = { path = "../service-providers/authenticator" } nym-network-requester = { path = "../service-providers/network-requester" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } @@ -97,7 +109,7 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } # throughput tester to recreate lioness # we don't care about particular versions - just pull whatever is used by sphinx lioness = "*" -chacha = "*" +chacha = "0.3.0" arrayref = "*" blake2 = "=0.8.1" sha2 = { workspace = true } @@ -114,7 +126,10 @@ cargo_metadata = { workspace = true } [dev-dependencies] criterion = { workspace = true, features = ["async_tokio"] } +rand_chacha = { workspace = true } +[features] +tokio-console = ["console-subscriber"] [lints] workspace = true diff --git a/nym-node/nym-node-metrics/src/entry.rs b/nym-node/nym-node-metrics/src/entry.rs index ab8da08fd6..d236270b70 100644 --- a/nym-node/nym-node-metrics/src/entry.rs +++ b/nym-node/nym-node-metrics/src/entry.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_statistics_common::hash_identifier; +use nym_statistics_common::{hash_identifier, types::SessionType}; use std::time::Duration; use time::{Date, OffsetDateTime}; use tokio::sync::{RwLock, RwLockReadGuard}; @@ -16,7 +16,7 @@ impl EntryStats { *self.sessions.write().await = new } - pub async fn client_sessions(&self) -> RwLockReadGuard { + pub async fn client_sessions(&self) -> RwLockReadGuard<'_, ClientSessions> { self.sessions.read().await } } @@ -66,22 +66,10 @@ impl FinishedSession { } } -#[derive(PartialEq, Copy, Clone, strum::Display, strum::EnumString)] -pub enum SessionType { - Vpn, - Mixnet, - Unknown, -} - -impl SessionType { - pub fn from_string>(s: S) -> Self { - s.as_ref().parse().unwrap_or(Self::Unknown) - } -} - pub struct ActiveSession { pub start: OffsetDateTime, pub typ: SessionType, + pub remember: bool, } impl ActiveSession { @@ -89,12 +77,16 @@ impl ActiveSession { ActiveSession { start: start_time, typ: SessionType::Unknown, + remember: false, } } pub fn set_type(&mut self, typ: SessionType) { self.typ = typ; } + pub fn remember(&mut self) { + self.remember = true; + } pub fn end_at(self, stop_time: OffsetDateTime) -> Option { let session_duration = stop_time - self.start; diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 9e88943930..82119f47c8 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -18,6 +18,7 @@ schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } time = { workspace = true, features = ["serde", "formatting", "parsing"] } thiserror = { workspace = true } @@ -26,6 +27,7 @@ nym-crypto = { path = "../../common/crypto", features = [ "serde", ] } nym-exit-policy = { path = "../../common/exit-policy" } +nym-noise-keys = { path = "../../common/nymnoise/keys" } nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } # feature-specific dependencies: diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index c2ae5badb4..9c3e0a67d2 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -1,14 +1,15 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::api::v1::node::models::{LegacyHostInformation, LegacyHostInformationV2}; +use crate::api::v1::node::models::{ + LegacyHostInformationV1, LegacyHostInformationV2, LegacyHostInformationV3, +}; use crate::error::Error; use nym_crypto::asymmetric::ed25519; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::ops::Deref; -use utoipa::ToSchema; #[cfg(feature = "client")] pub mod client; @@ -20,7 +21,7 @@ pub use client::Client; // create the type alias manually if openapi is not enabled pub type SignedHostInformation = SignedData; -#[derive(ToSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct SignedDataHostInfo { // #[serde(flatten)] pub data: crate::api::v1::node::models::HostInformation, @@ -40,6 +41,7 @@ impl SignedData { T: Serialize, { let plaintext = serde_json::to_string(&data)?; + let signature = key.sign(plaintext).to_base58_string(); Ok(SignedData { data, signature }) } @@ -66,9 +68,29 @@ impl SignedHostInformation { return true; } + // TODO: @JS: to remove downgrade support in future release(s) + + let legacy_v3 = SignedData { + data: LegacyHostInformationV3::from(self.data.clone()), + signature: self.signature.clone(), + }; + + if legacy_v3.verify(&self.keys.ed25519_identity) { + return true; + } + // attempt to verify legacy signatures + let legacy_v3 = SignedData { + data: LegacyHostInformationV3::from(self.data.clone()), + signature: self.signature.clone(), + }; + + if legacy_v3.verify(&self.keys.ed25519_identity) { + return true; + } + let legacy_v2 = SignedData { - data: LegacyHostInformationV2::from(self.data.clone()), + data: LegacyHostInformationV2::from(legacy_v3.data), signature: self.signature.clone(), }; @@ -77,7 +99,7 @@ impl SignedHostInformation { } SignedData { - data: LegacyHostInformation::from(legacy_v2.data), + data: LegacyHostInformationV1::from(legacy_v2.data), signature: self.signature.clone(), } .verify(&self.keys.ed25519_identity) @@ -103,10 +125,14 @@ impl Display for ErrorResponse { } } +#[allow(deprecated)] #[cfg(test)] mod tests { + use super::*; + use crate::api::v1::node::models::{HostKeys, SphinxKey}; use nym_crypto::asymmetric::{ed25519, x25519}; + use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; use rand_chacha::rand_core::SeedableRng; #[test] @@ -114,15 +140,27 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); - let x25519_noise = x25519::KeyPair::new(&mut rng); + let x25519_sphinx2 = x25519::KeyPair::new(&mut rng); + let x25519_versioned_noise = VersionedNoiseKey { + supported_version: NoiseVersion::V1, + x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(), + }; + let current_rotation_id = 1234; + + // no pre-announced keys let host_info = crate::api::v1::node::models::HostInformation { ip_address: vec!["1.1.1.1".parse().unwrap()], hostname: Some("foomp.com".to_string()), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: None, + primary_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: None, }, }; @@ -136,7 +174,12 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: Some(*x25519_noise.public_key()), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: Some(x25519_versioned_noise), }, }; @@ -144,6 +187,146 @@ mod tests { SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap(); assert!(signed_info.verify(ed22519.public_key())); assert!(signed_info.verify_host_information()); + + // with pre-announced keys + let host_info = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: Some(SphinxKey { + rotation_id: current_rotation_id + 1, + public_key: *x25519_sphinx2.public_key(), + }), + x25519_versioned_noise: None, + }, + }; + + let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); + + let host_info_with_noise = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: Some(SphinxKey { + rotation_id: current_rotation_id + 1, + public_key: *x25519_sphinx2.public_key(), + }), + x25519_versioned_noise: Some(x25519_versioned_noise), + }, + }; + + let signed_info = + SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); + } + + #[test] + fn dummy_legacy_v3_signed_host_verification() { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let ed22519 = ed25519::KeyPair::new(&mut rng); + let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); + + let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV3 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV3 { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: None, + }, + }; + + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) + let current_struct = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: None, + }, + }; + + // signature on legacy data + let signature = SignedData::new(legacy_info_no_noise, ed22519.private_key()) + .unwrap() + .signature; + + // signed blob with the 'current' structure + let current_struct = SignedData { + data: current_struct, + signature, + }; + + assert!(!current_struct.verify(ed22519.public_key())); + assert!(current_struct.verify_host_information()); + + // //technically this variant should never happen + let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV3 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV3 { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) + let current_struct_noise = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: Some(VersionedNoiseKey { + supported_version: NoiseVersion::V1, + x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(), + }), + }, + }; + + // signature on legacy data + + let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key()) + .unwrap() + .signature; + + // signed blob with the 'current' structure + + let current_struct_noise = SignedData { + data: current_struct_noise, + signature: signature_noise, + }; + + assert!(!current_struct_noise.verify(ed22519.public_key())); + assert!(current_struct_noise.verify_host_information()) } #[test] @@ -173,23 +356,38 @@ mod tests { }, }; + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) let host_info_no_noise = crate::api::v1::node::models::HostInformation { ip_address: legacy_info_no_noise.ip_address.clone(), hostname: legacy_info_no_noise.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(), - x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(), - x25519_noise: None, + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: None, }, }; + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) let host_info_noise = crate::api::v1::node::models::HostInformation { ip_address: legacy_info_noise.ip_address.clone(), hostname: legacy_info_noise.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(), - x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(), - x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()), + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: Some(VersionedNoiseKey { + supported_version: NoiseVersion::V1, + x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(), + }), }, }; @@ -216,33 +414,38 @@ mod tests { assert!(!current_struct_no_noise.verify(ed22519.public_key())); assert!(current_struct_no_noise.verify_host_information()); - // if noise key is present, the signature is actually valid - assert!(current_struct_noise.verify(ed22519.public_key())); + assert!(!current_struct_noise.verify(ed22519.public_key())); assert!(current_struct_noise.verify_host_information()) } #[test] - fn dummy_legacy_signed_host_verification() { + fn dummy_legacy_v1_signed_host_verification() { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); - let legacy_info = crate::api::v1::node::models::LegacyHostInformation { + let legacy_info = crate::api::v1::node::models::LegacyHostInformationV1 { ip_address: vec!["1.1.1.1".parse().unwrap()], hostname: Some("foomp.com".to_string()), - keys: crate::api::v1::node::models::LegacyHostKeys { + keys: crate::api::v1::node::models::LegacyHostKeysV1 { ed25519: ed22519.public_key().to_base58_string(), x25519: x25519_sphinx.public_key().to_base58_string(), }, }; + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) let host_info = crate::api::v1::node::models::HostInformation { ip_address: legacy_info.ip_address.clone(), hostname: legacy_info.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(), - x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(), - x25519_noise: None, + x25519_sphinx: *x25519_sphinx.public_key(), + primary_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_versioned_noise: None, }, }; diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs index 24597847ca..259bd35efa 100644 --- a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs @@ -16,9 +16,16 @@ pub struct Gateway { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Wireguard { + #[deprecated(note = "use specific port instead (tunnel or metadata service)")] #[cfg_attr(feature = "openapi", schema(example = 51822, default = 51822))] pub port: u16, + #[cfg_attr(feature = "openapi", schema(example = 51822, default = 51822))] + pub tunnel_port: u16, + + #[cfg_attr(feature = "openapi", schema(example = 51830, default = 51830))] + pub metadata_port: u16, + pub public_key: String, } diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 5f047912fa..2d0a9fe16f 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -4,9 +4,9 @@ use celes::Country; use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; use nym_crypto::asymmetric::x25519::{ - self, - serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, + self, serde_helpers::bs58_x25519_pubkey, serde_helpers::option_bs58_x25519_pubkey, }; +use nym_noise_keys::VersionedNoiseKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::IpAddr; @@ -70,6 +70,13 @@ impl HostInformation { } } +#[derive(Serialize)] +pub struct LegacyHostInformationV3 { + pub ip_address: Vec, + pub hostname: Option, + pub keys: LegacyHostKeysV3, +} + #[derive(Serialize)] pub struct LegacyHostInformationV2 { pub ip_address: Vec, @@ -78,14 +85,24 @@ pub struct LegacyHostInformationV2 { } #[derive(Serialize)] -pub struct LegacyHostInformation { +pub struct LegacyHostInformationV1 { pub ip_address: Vec, pub hostname: Option, - pub keys: LegacyHostKeys, + pub keys: LegacyHostKeysV1, } -impl From for LegacyHostInformationV2 { +impl From for LegacyHostInformationV3 { fn from(value: HostInformation) -> Self { + LegacyHostInformationV3 { + ip_address: value.ip_address, + hostname: value.hostname, + keys: value.keys.into(), + } + } +} + +impl From for LegacyHostInformationV2 { + fn from(value: LegacyHostInformationV3) -> Self { LegacyHostInformationV2 { ip_address: value.ip_address, hostname: value.hostname, @@ -94,9 +111,9 @@ impl From for LegacyHostInformationV2 { } } -impl From for LegacyHostInformation { +impl From for LegacyHostInformationV1 { fn from(value: LegacyHostInformationV2) -> Self { - LegacyHostInformation { + LegacyHostInformationV1 { ip_address: value.ip_address, hostname: value.hostname, keys: value.keys.into(), @@ -106,27 +123,115 @@ impl From for LegacyHostInformation { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(from = "HostKeysDeHelper")] pub struct HostKeys { /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity. - #[serde(alias = "ed25519")] - #[serde(with = "bs58_ed25519_pubkey")] #[schemars(with = "String")] #[cfg_attr(feature = "openapi", schema(value_type = String))] + #[serde(with = "bs58_ed25519_pubkey")] pub ed25519_identity: ed25519::PublicKey, - /// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation. - /// Currently, it corresponds to either mixnode's or gateway's key. - #[serde(alias = "x25519")] - #[serde(with = "bs58_x25519_pubkey")] + #[deprecated(note = "use explicit primary_x25519_sphinx_key instead")] #[schemars(with = "String")] #[cfg_attr(feature = "openapi", schema(value_type = String))] + #[serde(with = "bs58_x25519_pubkey")] pub x25519_sphinx: x25519::PublicKey, + /// Current, active, x25519 sphinx key clients are expected to use when constructing packets + /// with this node in the route. + pub primary_x25519_sphinx_key: SphinxKey, + + /// Pre-announced x25519 sphinx key clients will use during the following key rotation + pub pre_announced_x25519_sphinx_key: Option, + /// Base58-encoded x25519 public key of this node used for the noise protocol. + #[serde(default)] + pub x25519_versioned_noise: Option, +} + +// we need the intermediate struct to help us with the new explicit sphinx key fields +#[allow(deprecated)] +impl From for HostKeys { + fn from(value: HostKeysDeHelper) -> Self { + let primary_x25519_sphinx_key = match value.primary_x25519_sphinx_key { + None => { + // legacy + SphinxKey::new_legacy(value.x25519_sphinx) + } + Some(primary_x25519_sphinx_key) => primary_x25519_sphinx_key, + }; + + HostKeys { + ed25519_identity: value.ed25519_identity, + x25519_sphinx: value.x25519_sphinx, + primary_x25519_sphinx_key, + pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key, + x25519_versioned_noise: value.x25519_versioned_noise, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct HostKeysDeHelper { + /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity. + #[serde(alias = "ed25519")] + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519_identity: ed25519::PublicKey, + + #[deprecated(note = "use explicit primary_x25519_sphinx_key instead")] + #[serde(alias = "x25519")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25519_sphinx: x25519::PublicKey, + + /// Current, active, x25519 sphinx key clients are expected to use when constructing packets + /// with this node in the route. + pub primary_x25519_sphinx_key: Option, + + /// Pre-announced x25519 sphinx key clients will use during the following key rotation + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option, + + /// Base58-encoded x25519 public key of this node used for the noise protocol. + #[serde(default)] + pub x25519_versioned_noise: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct SphinxKey { + pub rotation_id: u32, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub public_key: x25519::PublicKey, +} + +impl SphinxKey { + pub fn new_legacy(public_key: x25519::PublicKey) -> SphinxKey { + SphinxKey { + rotation_id: u32::MAX, + public_key, + } + } + + pub fn is_legacy(&self) -> bool { + self.rotation_id == u32::MAX + } +} + +#[derive(Serialize)] +pub struct LegacyHostKeysV3 { + #[serde(alias = "ed25519")] + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519_identity: ed25519::PublicKey, + + #[serde(alias = "x25519")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25519_sphinx: x25519::PublicKey, + #[serde(default)] #[serde(with = "option_bs58_x25519_pubkey")] - #[schemars(with = "Option")] - #[cfg_attr(feature = "openapi", schema(value_type = Option))] pub x25519_noise: Option, } @@ -138,13 +243,23 @@ pub struct LegacyHostKeysV2 { } #[derive(Serialize)] -pub struct LegacyHostKeys { +pub struct LegacyHostKeysV1 { pub ed25519: String, pub x25519: String, } -impl From for LegacyHostKeysV2 { +impl From for LegacyHostKeysV3 { fn from(value: HostKeys) -> Self { + LegacyHostKeysV3 { + ed25519_identity: value.ed25519_identity, + x25519_sphinx: value.primary_x25519_sphinx_key.public_key, + x25519_noise: value.x25519_versioned_noise.map(|k| k.x25519_pubkey), + } + } +} + +impl From for LegacyHostKeysV2 { + fn from(value: LegacyHostKeysV3) -> Self { LegacyHostKeysV2 { ed25519_identity: value.ed25519_identity.to_base58_string(), x25519_sphinx: value.x25519_sphinx.to_base58_string(), @@ -156,9 +271,9 @@ impl From for LegacyHostKeysV2 { } } -impl From for LegacyHostKeys { +impl From for LegacyHostKeysV1 { fn from(value: LegacyHostKeysV2) -> Self { - LegacyHostKeys { + LegacyHostKeysV1 { ed25519: value.ed25519_identity, x25519: value.x25519_sphinx, } @@ -267,3 +382,31 @@ pub struct AuxiliaryDetails { #[serde(default)] pub accepted_operator_terms_and_conditions: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_host_information_deserialisation() { + let legacy_raw = r#" + { + "data": { + "ip_address": [ + "194.182.184.55" + ], + "hostname": null, + "keys": { + "ed25519_identity": "2RMWm7PoadaoWpk3KhT2tcFFfA4oKUyC44KwmVvjxNDS", + "x25519_sphinx": "Awn4R2AHX91tYeiMJMxW3mFfoePuHWzZYUFdDQnydZCD", + "x25519_noise": null + } + }, + "signature": "5JcXh766JANhz3bu2hMBS8onTLihQn6vnGgduJg1qd8JAcPGPbXBwBTKmmQPYCVGeZYFHW4CMGhfHVBu2A1rE5f7" + } + "#; + + let res = serde_json::from_str::(legacy_raw); + assert!(res.is_ok()); + } +} diff --git a/nym-node/nym-node-requests/src/api/v1/node_load/models.rs b/nym-node/nym-node-requests/src/api/v1/node_load/models.rs index ecbca83eeb..b4b1f0ad84 100644 --- a/nym-node/nym-node-requests/src/api/v1/node_load/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node_load/models.rs @@ -3,7 +3,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString}; +use strum_macros::{Display, EnumString}; #[derive( Display, diff --git a/nym-node/src/cli/commands/debug/mod.rs b/nym-node/src/cli/commands/debug/mod.rs new file mode 100644 index 0000000000..9fc1175a80 --- /dev/null +++ b/nym-node/src/cli/commands/debug/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod reset_providers_dbs; diff --git a/nym-node/src/cli/commands/debug/reset_providers_dbs.rs b/nym-node/src/cli/commands/debug/reset_providers_dbs.rs new file mode 100644 index 0000000000..39130686ae --- /dev/null +++ b/nym-node/src/cli/commands/debug/reset_providers_dbs.rs @@ -0,0 +1,36 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::helpers::ConfigArgs; +use crate::config::upgrade_helpers::try_load_current_config; +use crate::node::helpers::load_ed25519_identity_public_key; +use crate::node::ServiceProvidersData; +use nym_network_requester::{CustomGatewayDetails, GatewayDetails, GatewayRegistration}; +use std::fs; + +#[derive(Debug, clap::Args)] +pub struct Args { + #[clap(flatten)] + pub(crate) config: ConfigArgs, +} + +pub async fn execute(args: Args) -> anyhow::Result<()> { + let config = try_load_current_config(args.config.config_path()).await?; + + let public_key = load_ed25519_identity_public_key( + &config.storage_paths.keys.public_ed25519_identity_key_file, + )?; + + let storage_paths = &config.service_providers.storage_paths; + for db_path in [ + &storage_paths.authenticator.gateway_registrations, + &storage_paths.ip_packet_router.gateway_registrations, + &storage_paths.network_requester.gateway_registrations, + ] { + fs::remove_file(db_path)?; + let gateway_details: GatewayRegistration = + GatewayDetails::Custom(CustomGatewayDetails::new(public_key)).into(); + ServiceProvidersData::initialise_client_gateway_storage(db_path, &gateway_details).await?; + } + Ok(()) +} diff --git a/nym-node/src/cli/commands/mod.rs b/nym-node/src/cli/commands/mod.rs index dea762daf9..6bab14a818 100644 --- a/nym-node/src/cli/commands/mod.rs +++ b/nym-node/src/cli/commands/mod.rs @@ -3,8 +3,10 @@ pub(crate) mod bonding_information; pub(super) mod build_info; +pub(super) mod debug; pub(super) mod migrate; pub(crate) mod node_details; +pub(crate) mod reset_sphinx_keys; pub(super) mod run; pub(super) mod sign; pub(crate) mod test_throughput; diff --git a/nym-node/src/cli/commands/reset_sphinx_keys.rs b/nym-node/src/cli/commands/reset_sphinx_keys.rs new file mode 100644 index 0000000000..379196b934 --- /dev/null +++ b/nym-node/src/cli/commands/reset_sphinx_keys.rs @@ -0,0 +1,97 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::helpers::ConfigArgs; +use crate::config::persistence::{ + DEFAULT_RD_BLOOMFILTER_FILE_EXT, DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT, +}; +use crate::config::upgrade_helpers::try_load_current_config; +use crate::node::helpers::get_current_rotation_id; +use crate::node::key_rotation::manager::SphinxKeyManager; +use nym_crypto::aes::cipher::crypto_common::rand_core::OsRng; +use std::fs; +use std::fs::read_dir; +use std::path::PathBuf; +use tracing::{info, warn}; + +#[derive(Debug, clap::Args)] +pub(crate) struct Args { + #[clap(flatten)] + pub(crate) config: ConfigArgs, +} + +fn clear_bloomfilters(dir: &PathBuf) -> anyhow::Result<()> { + let read_dir = read_dir(dir)?; + for entry in read_dir { + let entry = entry?; + let path = entry.path(); + let Some(extension) = path.extension() else { + continue; + }; + if extension == DEFAULT_RD_BLOOMFILTER_FILE_EXT + || extension == DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT + { + { + fs::remove_file(path)?; + } + } + } + + Ok(()) +} + +pub async fn execute(args: Args) -> anyhow::Result<()> { + let config = try_load_current_config(args.config.config_path()).await?; + + warn!("RESETTING SPHINX KEYS OF NODE {}", config.id); + + // 1. attempt to retrieve current rotation id + let current_rotation_id = + get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?; + + // 2. remove all bloomfilters + info!("clearing old replay protection bloomfilters..."); + clear_bloomfilters( + &config + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + )?; + + // 3. remove primary and secondary keys. also a temporary key if it existed + info!("removing old keys..."); + let tmp_location = config + .storage_paths + .keys + .primary_x25519_sphinx_key_file + .with_extension("tmp"); + if tmp_location.exists() { + fs::remove_file(tmp_location)?; + } + + if config + .storage_paths + .keys + .secondary_x25519_sphinx_key_file + .exists() + { + fs::remove_file(&config.storage_paths.keys.secondary_x25519_sphinx_key_file)?; + } + + // no need to explicitly remove primary key as the file will be overwritten + + // 4. recreate primary key according to current rotation id + let mut rng = OsRng; + + info!("generating new key for rotation {current_rotation_id}..."); + let _ = SphinxKeyManager::initialise_new( + &mut rng, + current_rotation_id, + &config.storage_paths.keys.primary_x25519_sphinx_key_file, + &config.storage_paths.keys.secondary_x25519_sphinx_key_file, + )?; + + info!("done!"); + Ok(()) +} diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 8b568150ed..703e47662b 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -78,6 +78,7 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> { } config }; + config.validate()?; if !config.modes.any_enabled() { warn!("this node is going to run without mixnode or gateway support! consider providing `mode` value"); diff --git a/nym-node/src/cli/commands/sign.rs b/nym-node/src/cli/commands/sign.rs index 22ad977756..a05db78c85 100644 --- a/nym-node/src/cli/commands/sign.rs +++ b/nym-node/src/cli/commands/sign.rs @@ -72,7 +72,7 @@ fn print_signed_contract_msg( pub async fn execute(args: Args) -> anyhow::Result<()> { let config = try_load_current_config(args.config.config_path()).await?; let identity_keypair = - load_ed25519_identity_keypair(config.storage_paths.keys.ed25519_identity_storage_paths())?; + load_ed25519_identity_keypair(&config.storage_paths.keys.ed25519_identity_storage_paths())?; // note: due to clap's ArgGroup, one (and only one) of those branches will be called if let Some(text) = args.text { diff --git a/nym-node/src/cli/commands/test_throughput.rs b/nym-node/src/cli/commands/test_throughput.rs index af7cc8ad42..57b6bb9d03 100644 --- a/nym-node/src/cli/commands/test_throughput.rs +++ b/nym-node/src/cli/commands/test_throughput.rs @@ -6,7 +6,6 @@ use crate::logging::granual_filtered_env; use crate::throughput_tester::test_mixing_throughput; use anyhow::bail; use humantime_serde::re::humantime; -use indicatif::ProgressStyle; use nym_bin_common::logging::default_tracing_fmt_layer; use std::env::temp_dir; use std::path::PathBuf; @@ -41,9 +40,6 @@ pub struct Args { fn init_test_logger() -> anyhow::Result<()> { let indicatif_layer = IndicatifLayer::new() - .with_progress_style(ProgressStyle::with_template( - "{span_child_prefix}{spinner} {span_fields} -- {span_name} {wide_msg}", - )?) .with_span_child_prefix_symbol("↳ ") .with_span_child_prefix_indent(" "); diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 3ff2388c45..cf6d18b683 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -276,13 +276,13 @@ pub(crate) struct WireguardArgs { )] pub(crate) wireguard_bind_address: Option, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. #[clap( long, env = NYMNODE_WG_ANNOUNCED_PORT_ARG )] - pub(crate) wireguard_announced_port: Option, + pub(crate) wireguard_tunnel_announced_port: Option, /// 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 @@ -311,8 +311,8 @@ impl WireguardArgs { section.bind_address = bind_address } - if let Some(announced_port) = self.wireguard_announced_port { - section.announced_port = announced_port + if let Some(announced_tunnel_port) = self.wireguard_tunnel_announced_port { + section.announced_tunnel_port = announced_tunnel_port } if let Some(private_network_prefix) = self.wireguard_private_network_prefix { diff --git a/nym-node/src/cli/mod.rs b/nym-node/src/cli/mod.rs index 40b05b4a84..ad72438f41 100644 --- a/nym-node/src/cli/mod.rs +++ b/nym-node/src/cli/mod.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::commands::{ - bonding_information, build_info, migrate, node_details, run, sign, test_throughput, + bonding_information, build_info, debug, migrate, node_details, reset_sphinx_keys, run, sign, + test_throughput, }; use crate::env::vars::{NYMNODE_CONFIG_ENV_FILE_ARG, NYMNODE_NO_BANNER_ARG}; -use crate::logging::setup_tracing_logger; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use nym_bin_common::bin_info; use std::future::Future; use std::sync::OnceLock; @@ -55,7 +55,7 @@ impl Cli { pub(crate) fn execute(self) -> anyhow::Result<()> { // NOTE: `test_throughput` sets up its own logger as it has to include additional layers if !matches!(self.command, Commands::TestThroughput(..)) { - setup_tracing_logger()?; + crate::logging::setup_tracing_logger()?; } match self.command { @@ -68,6 +68,14 @@ impl Cli { Commands::Migrate(args) => migrate::execute(*args)?, Commands::Sign(args) => { Self::execute_async(sign::execute(args))? }?, Commands::TestThroughput(args) => test_throughput::execute(args)?, + Commands::UnsafeResetSphinxKeys(args) => { + { Self::execute_async(reset_sphinx_keys::execute(args))? }? + } + Commands::Debug(debug) => match debug.command { + DebugCommands::ResetProvidersGatewayDbs(args) => { + { Self::execute_async(debug::reset_providers_dbs::execute(args))? }? + } + }, } Ok(()) } @@ -93,12 +101,31 @@ pub(crate) enum Commands { /// Use identity key of this node to sign provided message. Sign(sign::Args), + /// UNSAFE: reset existing sphinx keys and attempt to generate fresh one for the current network state + UnsafeResetSphinxKeys(reset_sphinx_keys::Args), + + /// Commands exposed for debug purposes, usually not meant to be used by operators + #[clap(hide = true)] + Debug(Debug), + /// Attempt to approximate the maximum mixnet throughput if nym-node /// was running on this machine in mixnet mode #[clap(hide = true)] TestThroughput(test_throughput::Args), } +#[derive(Debug, Args)] +pub(crate) struct Debug { + #[clap(subcommand)] + pub(crate) command: DebugCommands, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum DebugCommands { + /// Reset the internal GatewaysDetailsStores of all service providers in case they got corrupted + ResetProvidersGatewayDbs(debug::reset_providers_dbs::Args), +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 0dae4e17db..53769f9d80 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -7,6 +7,7 @@ use clap::crate_version; use nym_gateway::node::{ LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts, }; +use nym_gateway::nym_authenticator; // a temporary solution until further refactoring is made fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { @@ -182,7 +183,6 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { .authenticator .to_common_client_paths(), }, - logging: config.logging, }, custom_mixnet_path: None, }; @@ -202,7 +202,8 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { bind_address: config.wireguard.bind_address, private_ipv4: config.wireguard.private_ipv4, private_ipv6: config.wireguard.private_ipv6, - announced_port: config.wireguard.announced_port, + announced_tunnel_port: config.wireguard.announced_tunnel_port, + announced_metadata_port: config.wireguard.announced_metadata_port, private_network_prefix_v4: config.wireguard.private_network_prefix_v4, private_network_prefix_v6: config.wireguard.private_network_prefix_v6, storage_paths: config.wireguard.storage_paths.clone(), diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index cc7fd21886..e401de4e20 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -10,7 +10,7 @@ use human_repr::HumanCount; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{ mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, - DEFAULT_VERLOC_LISTENING_PORT, WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, + DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, }; use nym_config::defaults::{WG_TUN_DEVICE_NETMASK_V4, WG_TUN_DEVICE_NETMASK_V6}; @@ -21,6 +21,7 @@ use nym_config::{ must_get_home, parse_urls, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, }; +use nym_gateway::nym_authenticator; use serde::{Deserialize, Serialize}; use std::env; use std::fmt::{Display, Formatter}; @@ -426,6 +427,21 @@ impl Config { pub fn validate(&self) -> Result<(), NymNodeError> { self.mixnet.validate()?; + // it's not allowed to run mixnode mode alongside entry mode + if self.modes.mixnode && self.modes.entry { + return Err(NymNodeError::config_validation_failure( + "illegal modes configuration - node cannot run as a mixnode and an entry gateway", + )); + } + + // nor it's allowed to run mixnode mode alongside exit mode + // (use two separate checks for better error messages) + if self.modes.mixnode && self.modes.exit { + return Err(NymNodeError::config_validation_failure( + "illegal modes configuration - node cannot run as a mixnode and an exit gateway", + )); + } + Ok(()) } } @@ -529,6 +545,9 @@ pub struct Mixnet { /// Settings for controlling replay detection pub replay_protection: ReplayProtection, + #[serde(default)] + pub key_rotation: KeyRotation, + #[serde(default)] pub debug: MixnetDebug, } @@ -577,6 +596,13 @@ pub struct MixnetDebug { /// Maximum number of packets that can be stored waiting to get sent to a particular connection. pub maximum_connection_buffer_size: usize, + /// Specify whether any framed packets between nodes should use the legacy format (v7) + /// as opposed to the current (v8) one. + /// The legacy format has to be used until sufficient number of nodes on the network has upgraded and understand the new variant. + /// This will allow for optimisations to indicate which [sphinx] key is meant to be used when + /// processing received packets. + pub use_legacy_packet_encoding: bool, + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) pub unsafe_disable_noise: bool, } @@ -639,12 +665,6 @@ pub struct ReplayProtectionDebug { /// It's performed in case the traffic rates increase before the next bloomfilter update. pub bloomfilter_size_multiplier: f64, - // NOTE: this field is temporary until replay detection bloomfilter rotation is tied - // to key rotation - /// Specifies how often the bloomfilter is cleared - #[serde(with = "humantime_serde")] - pub bloomfilter_reset_rate: Duration, - /// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash #[serde(with = "humantime_serde")] pub bloomfilter_disk_flushing_rate: Duration, @@ -661,9 +681,6 @@ impl ReplayProtectionDebug { // 10^-5 pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5; - // 25h (key rotation will be happening every 24h + 1h of overlap) - pub const DEFAULT_REPLAY_DETECTION_BF_RESET_RATE: Duration = Duration::from_secs(25 * 60 * 60); - // we must have some reasonable balance between losing values and trashing the disk. // since on average HDD it would take ~30s to save a 2GB bloomfilter pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60); @@ -680,8 +697,12 @@ impl ReplayProtectionDebug { )); } + // ideally we would have pulled the exact information from the network, + // but making async calls really doesn't play around with this method + // so we do second best: assume 24h rotation with 1h overlap (which realistically won't ever change) + let items_in_filter = items_in_bloomfilter( - self.bloomfilter_reset_rate, + Duration::from_secs(25 * 60 * 60), self.initial_expected_packets_per_second, ); let bitmap_size = bitmap_size(self.false_positive_rate, items_in_filter); @@ -717,12 +738,37 @@ impl Default for ReplayProtectionDebug { bloomfilter_minimum_packets_per_second_size: Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE, bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER, - bloomfilter_reset_rate: Self::DEFAULT_REPLAY_DETECTION_BF_RESET_RATE, bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE, } } } +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotation { + pub debug: KeyRotationDebug, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationDebug { + /// Specifies how often the node should poll for any changes in the key rotation global state. + #[serde(with = "humantime_serde")] + pub rotation_state_poling_interval: Duration, +} + +impl KeyRotationDebug { + pub const DEFAULT_ROTATION_STATE_POLLING_INTERVAL: Duration = Duration::from_secs(4 * 60 * 60); +} + +impl Default for KeyRotationDebug { + fn default() -> Self { + KeyRotationDebug { + rotation_state_poling_interval: Self::DEFAULT_ROTATION_STATE_POLLING_INTERVAL, + } + } +} + impl MixnetDebug { // given that genuine clients are using mean delay of 50ms, // the probability of them delaying for over 10s is 10^-87 @@ -742,8 +788,9 @@ impl Default for MixnetDebug { packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, - // to be changed by @SW once the implementation is there - unsafe_disable_noise: true, + // TODO: update this in few releases... + use_legacy_packet_encoding: true, + unsafe_disable_noise: false, } } } @@ -773,6 +820,7 @@ impl Mixnet { nym_api_urls, nyxd_urls, replay_protection: ReplayProtection::new_default(data_dir), + key_rotation: Default::default(), debug: Default::default(), } } @@ -883,9 +931,13 @@ pub struct Wireguard { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. - pub announced_port: u16, + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the metadata endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. /// The maximum value for IPv4 is 32 @@ -903,10 +955,11 @@ impl Wireguard { pub fn new_default>(data_dir: P) -> Self { Wireguard { enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_PORT), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_TUNNEL_PORT), private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, - announced_port: WG_PORT, + announced_tunnel_port: WG_TUNNEL_PORT, + announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, storage_paths: persistence::WireguardPaths::new(data_dir), @@ -920,7 +973,8 @@ impl From for nym_wireguard_types::Config { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + announced_tunnel_port: value.announced_tunnel_port, + announced_metadata_port: value.announced_metadata_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } @@ -933,7 +987,7 @@ impl From for nym_authenticator::config::Authenticator { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + tunnel_announced_port: value.announced_tunnel_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index 5842ab3146..e67f2a83e9 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only mod old_config_v1; +mod old_config_v10; mod old_config_v2; mod old_config_v3; mod old_config_v4; @@ -9,8 +10,10 @@ mod old_config_v5; mod old_config_v6; mod old_config_v7; mod old_config_v8; +mod old_config_v9; pub use old_config_v1::try_upgrade_config_v1; +pub use old_config_v10::try_upgrade_config_v10; pub use old_config_v2::try_upgrade_config_v2; pub use old_config_v3::try_upgrade_config_v3; pub use old_config_v4::try_upgrade_config_v4; @@ -18,3 +21,4 @@ pub use old_config_v5::try_upgrade_config_v5; pub use old_config_v6::try_upgrade_config_v6; pub use old_config_v7::try_upgrade_config_v7; pub use old_config_v8::try_upgrade_config_v8; +pub use old_config_v9::try_upgrade_config_v9; diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs new file mode 100644 index 0000000000..a55b5b1113 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -0,0 +1,1695 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::{ + ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, +}; +use crate::config::persistence::{ + AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, + NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, +}; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::{ + gateway_tasks, service_providers, Config, GatewayTasksConfig, Host, Http, KeyRotation, + KeyRotationDebug, Mixnet, MixnetDebug, NodeModes, ReplayProtection, ReplayProtectionDebug, + ServiceProvidersConfig, Verloc, VerlocDebug, Wireguard, DEFAULT_HTTP_PORT, +}; +use crate::error::NymNodeError; +use celes::Country; +use clap::ValueEnum; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core_config_types::DebugConfig as ClientDebugConfig; +use nym_config::defaults::{DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT}; +use nym_config::helpers::{in6addr_any_init, inaddr_any}; +use nym_config::{ + defaults::TICKETBOOK_VALIDITY_DAYS, + read_config_from_toml_file, + serde_helpers::{de_maybe_port, de_maybe_stringified}, +}; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tracing::{debug, instrument}; +use url::Url; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardPathsV10 { + pub private_diffie_hellman_key_file: PathBuf, + pub public_diffie_hellman_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV10 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `[::]:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV10, +} + +// a temporary solution until all "types" are run at the same time +#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum NodeModeV10 { + #[default] + #[clap(alias = "mix")] + Mixnode, + + #[clap(alias = "entry", alias = "gateway")] + EntryGateway, + + // to not break existing behaviour, this means exit capabilities AND entry capabilities + #[clap(alias = "exit")] + ExitGateway, + + // will start only SP needed for exit capabilities WITHOUT entry routing + ExitProvidersOnly, +} + +impl From for NodeModes { + fn from(config: NodeModeV10) -> Self { + match config { + NodeModeV10::Mixnode => *NodeModes::default().with_mixnode(), + NodeModeV10::EntryGateway => *NodeModes::default().with_entry(), + // in old version exit implied entry + NodeModeV10::ExitGateway => *NodeModes::default().with_entry().with_exit(), + NodeModeV10::ExitProvidersOnly => *NodeModes::default().with_exit(), + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +pub struct NodeModesV10 { + /// Specifies whether this node can operate in a mixnode mode. + pub mixnode: bool, + + /// Specifies whether this node can operate in an entry mode. + pub entry: bool, + + /// Specifies whether this node can operate in an exit mode. + pub exit: bool, + // TODO: would it make sense to also put WG here for completion? +} + +// TODO: this is very much a WIP. we need proper ssl certificate support here +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HostV10 { + /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. + /// If no values are provided, when this node gets included in the network, + /// its ip addresses will be populated by whatever value is resolved by associated nym-api. + pub public_ips: Vec, + + /// Optional hostname of this node, for example nymtech.net. + // TODO: this is temporary. to be replaced by pulling the data directly from the certs. + #[serde(deserialize_with = "de_maybe_stringified")] + pub hostname: Option, + + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[serde(deserialize_with = "de_maybe_stringified")] + pub location: Option, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationDebugV10 { + /// Specifies how often the node should poll for any changes in the key rotation global state. + #[serde(with = "humantime_serde")] + pub rotation_state_poling_interval: Duration, +} + +impl KeyRotationDebugV10 { + pub const DEFAULT_ROTATION_STATE_POLLING_INTERVAL: Duration = Duration::from_secs(4 * 60 * 60); +} + +impl Default for KeyRotationDebugV10 { + fn default() -> Self { + KeyRotationDebugV10 { + rotation_state_poling_interval: Self::DEFAULT_ROTATION_STATE_POLLING_INTERVAL, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationV10 { + pub debug: KeyRotationDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetDebugV10 { + /// Specifies the duration of time this node is willing to delay a forward packet for. + #[serde(with = "humantime_serde")] + pub maximum_forward_packet_delay: Duration, + + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Specify whether any framed packets between nodes should use the legacy format (v7) + /// as opposed to the current (v8) one. + /// The legacy format has to be used until sufficient number of nodes on the network has upgraded and understand the new variant. + /// This will allow for optimisations to indicate which [sphinx] key is meant to be used when + /// processing received packets. + pub use_legacy_packet_encoding: bool, + + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) + pub unsafe_disable_noise: bool, +} + +impl MixnetDebugV10 { + // given that genuine clients are using mean delay of 50ms, + // the probability of them delaying for over 10s is 10^-87 + // which for all intents and purposes will never happen + pub(crate) const DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY: Duration = Duration::from_secs(10); + pub(crate) const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = + Duration::from_millis(10_000); + pub(crate) const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = + Duration::from_millis(300_000); + pub(crate) const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); + pub(crate) const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; +} + +impl Default for MixnetDebugV10 { + fn default() -> Self { + MixnetDebugV10 { + maximum_forward_packet_delay: Self::DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY, + packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, + maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // TODO: update this in few releases... + use_legacy_packet_encoding: true, + unsafe_disable_noise: false, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnetV10 { + /// Address this node will bind to for listening for mixnet packets + /// default: `[::]:1789` + pub bind_address: SocketAddr, + + /// 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. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + + /// Addresses to nym APIs from which the node gets the view of the network. + pub nym_api_urls: Vec, + + /// Addresses to nyxd which the node uses to interact with the nyx chain. + pub nyxd_urls: Vec, + + /// Settings for controlling replay detection + pub replay_protection: ReplayProtectionV10, + + #[serde(default)] + pub key_rotation: KeyRotationV10, + + #[serde(default)] + pub debug: MixnetDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct ReplayProtectionV10 { + /// Paths for current bloomfilters + pub storage_paths: ReplayProtectionPathsV10, + + #[serde(default)] + pub debug: ReplayProtectionDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ReplayProtectionPathsV10 { + /// Path to the directory storing currently used bloomfilter(s). + pub current_bloomfilters_directory: PathBuf, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct ReplayProtectionDebugV10 { + /// Specifies whether this node should **NOT** use replay protection + pub unsafe_disabled: bool, + + /// How long the processing task is willing to skip mutex acquisition before it will block the thread + /// until it actually obtains it + pub maximum_replay_detection_deferral: Duration, + + /// How many packets the processing task is willing to queue before it will block the thread + /// until it obtains the mutex + pub maximum_replay_detection_pending_packets: usize, + + /// Probability of false positives, fraction between 0 and 1 or a number indicating 1-in-p + pub false_positive_rate: f64, + + /// Defines initial expected number of packets this node will process a second, + /// so that an initial bloomfilter could be established. + /// As the node is running and BF are cleared, the value will be adjusted dynamically + pub initial_expected_packets_per_second: usize, + + /// Defines minimum expected number of packets this node will process a second + /// when used for calculating the BF size after reset. + /// This is to avoid degenerate cases where node receives 0 packets (because say it's misconfigured) + /// and it constructs an empty bloomfilter. + pub bloomfilter_minimum_packets_per_second_size: usize, + + /// Specifies the amount the bloomfilter size is going to get multiplied by after each reset. + /// It's performed in case the traffic rates increase before the next bloomfilter update. + pub bloomfilter_size_multiplier: f64, + + /// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash + #[serde(with = "humantime_serde")] + pub bloomfilter_disk_flushing_rate: Duration, +} + +impl ReplayProtectionDebugV10 { + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL: Duration = Duration::from_millis(50); + + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS: usize = 100; + + // 12% (completely arbitrary) + pub const DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER: f64 = 1.12; + + // 10^-5 + pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5; + + // we must have some reasonable balance between losing values and trashing the disk. + // since on average HDD it would take ~30s to save a 2GB bloomfilter + pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60); + + // this value will have to be adjusted in the future + pub const DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND: usize = 2000; + + pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; +} + +impl Default for ReplayProtectionDebugV10 { + fn default() -> Self { + ReplayProtectionDebugV10 { + unsafe_disabled: false, + maximum_replay_detection_deferral: Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL, + maximum_replay_detection_pending_packets: + Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS, + false_positive_rate: Self::DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE, + initial_expected_packets_per_second: Self::DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND, + bloomfilter_minimum_packets_per_second_size: + Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE, + bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER, + bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV10 { + /// Path to file containing ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing the primary x25519 sphinx private key. + pub primary_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing the secondary x25519 sphinx private key. + pub secondary_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 noise private key. + pub private_x25519_noise_key_file: PathBuf, + + /// Path to file containing x25519 noise public key. + pub public_x25519_noise_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NymNodePathsV10 { + pub keys: KeysPathsV10, + + /// Path to a file containing basic node description: human-readable name, website, details, etc. + pub description: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HttpV10 { + /// Socket address this node will use for binding its http API. + /// default: `[::]:8080` + pub bind_address: SocketAddr, + + /// Path to assets directory of custom landing page of this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub landing_page_assets_path: Option, + + /// An optional bearer token for accessing certain http endpoints. + /// Currently only used for obtaining mixnode's stats. + #[serde(default)] + pub access_token: Option, + + /// Specify whether basic system information should be exposed. + /// default: true + pub expose_system_info: bool, + + /// Specify whether basic system hardware information should be exposed. + /// This option is superseded by `expose_system_info` + /// default: true + pub expose_system_hardware: bool, + + /// Specify whether detailed system crypto hardware information should be exposed. + /// This option is superseded by `expose_system_hardware` + /// default: true + pub expose_crypto_hardware: bool, + + /// Specify the cache ttl of the node load. + /// default: 30s + #[serde(with = "humantime_serde")] + pub node_load_cache_ttl: Duration, +} + +impl HttpV10 { + pub const DEFAULT_NODE_LOAD_CACHE_TTL: Duration = Duration::from_secs(30); +} + +impl Default for HttpV10 { + fn default() -> Self { + HttpV10 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT), + landing_page_assets_path: None, + access_token: None, + expose_system_info: true, + expose_system_hardware: true, + expose_crypto_hardware: true, + node_load_cache_ttl: Self::DEFAULT_NODE_LOAD_CACHE_TTL, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodePathsV10 {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV10 { + /// Delay between each subsequent node statistics being logged to the console + #[serde(with = "humantime_serde")] + pub node_stats_logging_delay: Duration, + + /// Delay between each subsequent node statistics being updated + #[serde(with = "humantime_serde")] + pub node_stats_updating_delay: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocDebugV10 { + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the connection to get established. + #[serde(with = "humantime_serde")] + pub connection_timeout: Duration, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + #[serde(with = "humantime_serde")] + pub packet_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + #[serde(with = "humantime_serde")] + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + #[serde(with = "humantime_serde")] + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + #[serde(with = "humantime_serde")] + pub retry_timeout: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocV10 { + /// Socket address this node will use for binding its verloc API. + /// default: `[::]:1790` + pub bind_address: SocketAddr, + + /// 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. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + + #[serde(default)] + pub debug: VerlocDebugV10, +} + +impl VerlocV10 { + pub const DEFAULT_VERLOC_PORT: u16 = DEFAULT_VERLOC_LISTENING_PORT; +} + +impl Default for VerlocV10 { + fn default() -> Self { + VerlocV10 { + bind_address: SocketAddr::new(in6addr_any_init(), Self::DEFAULT_VERLOC_PORT), + announce_port: None, + debug: Default::default(), + } + } +} + +impl VerlocDebugV10 { + const DEFAULT_PACKETS_PER_NODE: usize = 100; + const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); + const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); + const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); + const DEFAULT_BATCH_SIZE: usize = 50; + const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); + const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); +} + +impl Default for VerlocDebugV10 { + fn default() -> Self { + VerlocDebugV10 { + packets_per_node: Self::DEFAULT_PACKETS_PER_NODE, + connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT, + packet_timeout: Self::DEFAULT_PACKET_TIMEOUT, + delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE, + testing_interval: Self::DEFAULT_TESTING_INTERVAL, + retry_timeout: Self::DEFAULT_RETRY_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodeConfigV10 { + pub storage_paths: MixnodePathsV10, + + pub verloc: VerlocV10, + + #[serde(default)] + pub debug: DebugV10, +} + +impl DebugV10 { + const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); + const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); +} + +impl Default for DebugV10 { + fn default() -> Self { + DebugV10 { + node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY, + node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayPathsV10 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys and available client bandwidths. + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebugV10 { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebugV10 { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + + // use min(4/5 of max validity, validity - 1), but making sure it's no greater than 1 day + // ASSUMPTION: our validity period is AT LEAST 2 days + // + // this could have been a constant, but it's more readable as a function + pub const fn default_maximum_time_between_redemption() -> Duration { + let desired_secs = TICKETBOOK_VALIDITY_DAYS * (86400 * 4) / 5; + let desired_secs_alt = (TICKETBOOK_VALIDITY_DAYS - 1) * 86400; + + // can't use `min` in const context + let target_secs = if desired_secs < desired_secs_alt { + desired_secs + } else { + desired_secs_alt + }; + + assert!( + target_secs > 86400, + "the maximum time between redemption can't be lower than 1 day!" + ); + Duration::from_secs(target_secs as u64) + } +} + +impl Default for ZkNymTicketHandlerDebugV10 { + fn default() -> Self { + ZkNymTicketHandlerDebugV10 { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::default_maximum_time_between_redemption(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + pub zk_nym_tickets: ZkNymTicketHandlerDebugV10, +} + +impl EntryGatewayConfigDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for EntryGatewayConfigDebugV10 { + fn default() -> Self { + EntryGatewayConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigV10 { + pub storage_paths: EntryGatewayPathsV10, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: EntryGatewayConfigDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NetworkRequesterPathsV10 { + /// Path to file containing network requester ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct IpPacketRouterPathsV10 { + /// Path to file containing ip packet router ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AuthenticatorPathsV10 { + /// Path to file containing authenticator ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayPathsV10 { + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV10, + + pub ip_packet_router: IpPacketRouterPathsV10, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct AuthenticatorV10 { + #[serde(default)] + pub debug: AuthenticatorDebugV10, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct AuthenticatorDebugV10 { + /// Specifies whether authenticator service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run + /// the authenticator. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for AuthenticatorDebugV10 { + fn default() -> Self { + AuthenticatorDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AuthenticatorV10 { + fn default() -> Self { + AuthenticatorV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebugV10 { + /// Specifies whether ip packet routing service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for IpPacketRouterDebugV10 { + fn default() -> Self { + IpPacketRouterDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouterV10 { + #[serde(default)] + pub debug: IpPacketRouterDebugV10, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouterV10 { + fn default() -> Self { + IpPacketRouterV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebugV10 { + /// Specifies whether network requester service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for NetworkRequesterDebugV10 { + fn default() -> Self { + NetworkRequesterDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterV10 { + #[serde(default)] + pub debug: NetworkRequesterDebugV10, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV10 { + fn default() -> Self { + NetworkRequesterV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ExitGatewayDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ExitGatewayDebugV10 { + fn default() -> Self { + ExitGatewayDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayConfigV10 { + pub storage_paths: ExitGatewayPathsV10, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV10, + + pub ip_packet_router: IpPacketRouterV10, + + #[serde(default)] + pub debug: ExitGatewayDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksPathsV10 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct StaleMessageDebugV10 { + /// Specifies how often the clean-up task should check for stale data. + #[serde(with = "humantime_serde")] + pub cleaner_run_interval: Duration, + + /// Specifies maximum age of stored messages before they are removed from the storage + #[serde(with = "humantime_serde")] + pub max_age: Duration, +} + +impl StaleMessageDebugV10 { + const DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_STALE_MESSAGES_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); +} + +impl Default for StaleMessageDebugV10 { + fn default() -> Self { + StaleMessageDebugV10 { + cleaner_run_interval: Self::DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL, + max_age: Self::DEFAULT_STALE_MESSAGES_MAX_AGE, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ClientBandwidthDebugV10 { + /// Defines maximum delay between client bandwidth information being flushed to the persistent storage. + pub max_flushing_rate: Duration, + + /// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage. + pub max_delta_flushing_amount: i64, +} + +impl ClientBandwidthDebugV10 { + const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); + const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB +} + +impl Default for ClientBandwidthDebugV10 { + fn default() -> Self { + ClientBandwidthDebugV10 { + max_flushing_rate: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE, + max_delta_flushing_amount: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct GatewayTasksConfigDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + + /// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies + /// of the services providers + pub minimum_mix_performance: u8, + + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + #[serde(alias = "maximum_auth_request_age")] + pub max_request_timestamp_skew: Duration, + + pub stale_messages: StaleMessageDebugV10, + + pub client_bandwidth: ClientBandwidthDebugV10, + + pub zk_nym_tickets: ZkNymTicketHandlerDebugV10, +} + +impl GatewayTasksConfigDebugV10 { + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; +} + +impl Default for GatewayTasksConfigDebugV10 { + fn default() -> Self { + GatewayTasksConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + max_request_timestamp_skew: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW, + minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, + stale_messages: Default::default(), + client_bandwidth: Default::default(), + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksConfigV10 { + pub storage_paths: GatewayTasksPathsV10, + + /// Indicates whether this gateway is accepting only zk-nym credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub ws_bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: GatewayTasksConfigDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersPathsV10 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV10, + + pub ip_packet_router: IpPacketRouterPathsV10, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigDebugV10 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ServiceProvidersConfigDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ServiceProvidersConfigDebugV10 { + fn default() -> Self { + ServiceProvidersConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigV10 { + pub storage_paths: ServiceProvidersPathsV10, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV10, + + pub ip_packet_router: IpPacketRouterV10, + + pub authenticator: AuthenticatorV10, + + #[serde(default)] + pub debug: ServiceProvidersConfigDebugV10, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsConfigV10 { + #[serde(default)] + pub debug: MetricsDebugV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsDebugV10 { + /// Specify whether running statistics of this node should be logged to the console. + pub log_stats_to_console: bool, + + /// Specify the rate of which the metrics aggregator should call the `on_update` methods of all its registered handlers. + #[serde(with = "humantime_serde")] + pub aggregator_update_rate: Duration, + + /// Specify the target rate of clearing old stale mixnet metrics. + #[serde(with = "humantime_serde")] + pub stale_mixnet_metrics_cleaner_rate: Duration, + + /// Specify the target rate of updating global prometheus counters. + #[serde(with = "humantime_serde")] + pub global_prometheus_counters_update_rate: Duration, + + /// Specify the target rate of updating egress packets pending delivery counter. + #[serde(with = "humantime_serde")] + pub pending_egress_packets_update_rate: Duration, + + /// Specify the rate of updating clients sessions + #[serde(with = "humantime_serde")] + pub clients_sessions_update_rate: Duration, + + /// If console logging is enabled, specify the interval at which that happens + #[serde(with = "humantime_serde")] + pub console_logging_update_interval: Duration, + + /// Specify the update rate of running stats for the legacy `/metrics/mixing` endpoint + #[serde(with = "humantime_serde")] + pub legacy_mixing_metrics_update_rate: Duration, +} + +impl MetricsDebugV10 { + const DEFAULT_CONSOLE_LOGGING_INTERVAL: Duration = Duration::from_millis(60_000); + const DEFAULT_LEGACY_MIXING_UPDATE_RATE: Duration = Duration::from_millis(30_000); + const DEFAULT_AGGREGATOR_UPDATE_RATE: Duration = Duration::from_secs(5); + const DEFAULT_STALE_MIXNET_METRICS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const DEFAULT_CLIENT_SESSIONS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL: Duration = Duration::from_secs(30); + const DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE: Duration = Duration::from_secs(30); +} + +impl Default for MetricsDebugV10 { + fn default() -> Self { + MetricsDebugV10 { + log_stats_to_console: true, + console_logging_update_interval: Self::DEFAULT_CONSOLE_LOGGING_INTERVAL, + legacy_mixing_metrics_update_rate: Self::DEFAULT_LEGACY_MIXING_UPDATE_RATE, + aggregator_update_rate: Self::DEFAULT_AGGREGATOR_UPDATE_RATE, + stale_mixnet_metrics_cleaner_rate: Self::DEFAULT_STALE_MIXNET_METRICS_UPDATE_RATE, + global_prometheus_counters_update_rate: + Self::GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL, + pending_egress_packets_update_rate: Self::DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE, + clients_sessions_update_rate: Self::DEFAULT_CLIENT_SESSIONS_UPDATE_RATE, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV10 { + // well, we need to implement something here at some point... +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV10 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV10, + + pub host: HostV10, + + pub mixnet: MixnetV10, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV10, + + #[serde(default)] + pub http: HttpV10, + + #[serde(default)] + pub verloc: VerlocV10, + + pub wireguard: WireguardV10, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV10, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV10, + + #[serde(default)] + pub metrics: MetricsConfigV10, + + #[serde(default)] + pub logging: LoggingSettingsV10, + + #[serde(default)] + pub debug: DebugV10, +} + +impl ConfigV10 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV10 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v10>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v10 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV10::read_from_path(&path)? + }; + + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModes { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + replay_protection: ReplayProtection { + storage_paths: ReplayProtectionPaths { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebug { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: KeyRotation { + debug: KeyRotationDebug { + rotation_state_poling_interval: old_cfg + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + }, + }, + debug: MixnetDebug { + maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, + }, + }, + storage_paths: NymNodePaths { + keys: KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + primary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .primary_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + secondary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .secondary_x25519_sphinx_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: Verloc { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.verloc.debug.retry_timeout, + }, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_tunnel_port: old_cfg.wireguard.announced_port, + announced_metadata_port: WG_METADATA_PORT, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + gateway_tasks: GatewayTasksConfig { + storage_paths: GatewayTasksPaths { + clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebug { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebug { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettings {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/old_configs/old_config_v3.rs b/nym-node/src/config/old_configs/old_config_v3.rs index 6796f1aa8c..8df7a5a96a 100644 --- a/nym-node/src/config/old_configs/old_config_v3.rs +++ b/nym-node/src/config/old_configs/old_config_v3.rs @@ -203,22 +203,6 @@ pub struct KeysPathsV3 { } impl KeysPathsV3 { - pub fn new>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - - KeysPathsV3 { - private_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), - public_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), - private_x25519_sphinx_key_file: data_dir - .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), - public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), - private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), - public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), - } - } - pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { nym_pemstore::KeyPairPath::new( &self.private_ed25519_identity_key_file, diff --git a/nym-node/src/config/old_configs/old_config_v4.rs b/nym-node/src/config/old_configs/old_config_v4.rs index fb6d3aaa30..98912cbb9e 100644 --- a/nym-node/src/config/old_configs/old_config_v4.rs +++ b/nym-node/src/config/old_configs/old_config_v4.rs @@ -212,45 +212,6 @@ pub struct KeysPathsV4 { pub public_x25519_noise_key_file: PathBuf, } -impl KeysPathsV4 { - pub fn new>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - - KeysPathsV4 { - private_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), - public_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), - private_x25519_sphinx_key_file: data_dir - .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), - public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), - private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), - public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), - } - } - - pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_ed25519_identity_key_file, - &self.public_ed25519_identity_key_file, - ) - } - - pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_sphinx_key_file, - &self.public_x25519_sphinx_key_file, - ) - } - - pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_noise_key_file, - &self.public_x25519_noise_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct NymNodePathsV4 { diff --git a/nym-node/src/config/old_configs/old_config_v5.rs b/nym-node/src/config/old_configs/old_config_v5.rs index 0984802358..5dbacdaedb 100644 --- a/nym-node/src/config/old_configs/old_config_v5.rs +++ b/nym-node/src/config/old_configs/old_config_v5.rs @@ -212,45 +212,6 @@ pub struct KeysPathsV5 { pub public_x25519_noise_key_file: PathBuf, } -impl KeysPathsV5 { - pub fn new>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - - KeysPathsV5 { - private_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), - public_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), - private_x25519_sphinx_key_file: data_dir - .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), - public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), - private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), - public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), - } - } - - pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_ed25519_identity_key_file, - &self.public_ed25519_identity_key_file, - ) - } - - pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_sphinx_key_file, - &self.public_x25519_sphinx_key_file, - ) - } - - pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_noise_key_file, - &self.public_x25519_noise_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct NymNodePathsV5 { diff --git a/nym-node/src/config/old_configs/old_config_v6.rs b/nym-node/src/config/old_configs/old_config_v6.rs index 7a3eb97295..ebf3cc5b44 100644 --- a/nym-node/src/config/old_configs/old_config_v6.rs +++ b/nym-node/src/config/old_configs/old_config_v6.rs @@ -232,45 +232,6 @@ pub struct KeysPathsV6 { pub public_x25519_noise_key_file: PathBuf, } -impl KeysPathsV6 { - pub fn new>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - - KeysPathsV6 { - private_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), - public_ed25519_identity_key_file: data_dir - .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), - private_x25519_sphinx_key_file: data_dir - .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), - public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), - private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), - public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), - } - } - - pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_ed25519_identity_key_file, - &self.public_ed25519_identity_key_file, - ) - } - - pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_sphinx_key_file, - &self.public_x25519_sphinx_key_file, - ) - } - - pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_noise_key_file, - &self.public_x25519_noise_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct NymNodePathsV6 { diff --git a/nym-node/src/config/old_configs/old_config_v8.rs b/nym-node/src/config/old_configs/old_config_v8.rs index f4fc89503e..64f8f057bb 100644 --- a/nym-node/src/config/old_configs/old_config_v8.rs +++ b/nym-node/src/config/old_configs/old_config_v8.rs @@ -1,12 +1,14 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; -use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, -}; -use crate::config::service_providers::{ - IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +use crate::config::old_configs::old_config_v9::{ + AuthenticatorDebugV9, AuthenticatorPathsV9, AuthenticatorV9, ClientBandwidthDebugV9, ConfigV9, + GatewayTasksConfigDebugV9, GatewayTasksConfigV9, GatewayTasksPathsV9, HostV9, HttpV9, + IpPacketRouterDebugV9, IpPacketRouterPathsV9, IpPacketRouterV9, KeysPathsV9, LoggingSettingsV9, + MixnetDebugV9, MixnetV9, NetworkRequesterDebugV9, NetworkRequesterPathsV9, NetworkRequesterV9, + NodeModesV9, NymNodePathsV9, ReplayProtectionV9, ServiceProvidersConfigDebugV9, + ServiceProvidersConfigV9, ServiceProvidersPathsV9, StaleMessageDebugV9, VerlocDebugV9, + VerlocV9, WireguardPathsV9, WireguardV9, ZkNymTicketHandlerDebugV9, }; use crate::config::*; use crate::error::NymNodeError; @@ -20,7 +22,6 @@ use nym_config::{ serde_helpers::{de_maybe_port, de_maybe_stringified}, }; use nym_config::{parse_urls, read_config_from_toml_file}; -use persistence::*; use serde::{Deserialize, Serialize}; use std::env; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -1189,7 +1190,7 @@ impl ConfigV8 { pub async fn try_upgrade_config_v8>( path: P, prev_config: Option, -) -> Result { +) -> Result { debug!("attempting to load v8 config..."); let old_cfg = if let Some(prev_config) = prev_config { @@ -1205,26 +1206,26 @@ pub async fn try_upgrade_config_v8>( .parent() .ok_or(NymNodeError::DataDirDerivationFailure)?; - let cfg = Config { + let cfg = ConfigV9 { save_path: old_cfg.save_path, id: old_cfg.id, - modes: NodeModes { + modes: NodeModesV9 { mixnode: old_cfg.modes.mixnode, entry: old_cfg.modes.entry, exit: old_cfg.modes.exit, }, - host: Host { + host: HostV9 { public_ips: old_cfg.host.public_ips, hostname: old_cfg.host.hostname, location: old_cfg.host.location, }, - mixnet: Mixnet { + mixnet: MixnetV9 { bind_address: old_cfg.mixnet.bind_address, announce_port: old_cfg.mixnet.announce_port, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, - replay_protection: ReplayProtection::new_default(data_dir), - debug: MixnetDebug { + replay_protection: ReplayProtectionV9::new_default(data_dir), + debug: MixnetDebugV9 { maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, packet_forwarding_initial_backoff: old_cfg .mixnet @@ -1239,8 +1240,8 @@ pub async fn try_upgrade_config_v8>( unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, }, }, - storage_paths: NymNodePaths { - keys: KeysPaths { + storage_paths: NymNodePathsV9 { + keys: KeysPathsV9 { private_ed25519_identity_key_file: old_cfg .storage_paths .keys @@ -1268,7 +1269,7 @@ pub async fn try_upgrade_config_v8>( }, description: old_cfg.storage_paths.description, }, - http: Http { + http: HttpV9 { bind_address: old_cfg.http.bind_address, landing_page_assets_path: old_cfg.http.landing_page_assets_path, access_token: old_cfg.http.access_token, @@ -1277,10 +1278,10 @@ pub async fn try_upgrade_config_v8>( expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, }, - verloc: Verloc { + verloc: VerlocV9 { bind_address: old_cfg.verloc.bind_address, announce_port: old_cfg.verloc.announce_port, - debug: VerlocDebug { + debug: VerlocDebugV9 { packets_per_node: old_cfg.verloc.debug.packets_per_node, connection_timeout: old_cfg.verloc.debug.connection_timeout, packet_timeout: old_cfg.verloc.debug.packet_timeout, @@ -1290,7 +1291,7 @@ pub async fn try_upgrade_config_v8>( retry_timeout: old_cfg.verloc.debug.retry_timeout, }, }, - wireguard: Wireguard { + wireguard: WireguardV9 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4: old_cfg.wireguard.private_ipv4, @@ -1298,7 +1299,7 @@ pub async fn try_upgrade_config_v8>( announced_port: old_cfg.wireguard.announced_port, private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, - storage_paths: WireguardPaths { + storage_paths: WireguardPathsV9 { private_diffie_hellman_key_file: old_cfg .wireguard .storage_paths @@ -1309,8 +1310,8 @@ pub async fn try_upgrade_config_v8>( .public_diffie_hellman_key_file, }, }, - gateway_tasks: GatewayTasksConfig { - storage_paths: GatewayTasksPaths { + gateway_tasks: GatewayTasksConfigV9 { + storage_paths: GatewayTasksPathsV9 { clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, @@ -1319,12 +1320,12 @@ pub async fn try_upgrade_config_v8>( ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, - debug: gateway_tasks::Debug { + debug: GatewayTasksConfigDebugV9 { message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, - stale_messages: StaleMessageDebug { + stale_messages: StaleMessageDebugV9 { cleaner_run_interval: old_cfg .gateway_tasks .debug @@ -1332,7 +1333,7 @@ pub async fn try_upgrade_config_v8>( .cleaner_run_interval, max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, }, - client_bandwidth: ClientBandwidthDebug { + client_bandwidth: ClientBandwidthDebugV9 { max_flushing_rate: old_cfg .gateway_tasks .debug @@ -1344,7 +1345,7 @@ pub async fn try_upgrade_config_v8>( .client_bandwidth .max_delta_flushing_amount, }, - zk_nym_tickets: ZkNymTicketHandlerDebug { + zk_nym_tickets: ZkNymTicketHandlerDebugV9 { revocation_bandwidth_penalty: old_cfg .gateway_tasks .debug @@ -1369,11 +1370,11 @@ pub async fn try_upgrade_config_v8>( }, }, }, - service_providers: ServiceProvidersConfig { - storage_paths: ServiceProvidersPaths { + service_providers: ServiceProvidersConfigV9 { + storage_paths: ServiceProvidersPathsV9 { clients_storage: old_cfg.service_providers.storage_paths.clients_storage, stats_storage: old_cfg.service_providers.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { + network_requester: NetworkRequesterPathsV9 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1410,7 +1411,7 @@ pub async fn try_upgrade_config_v8>( .network_requester .gateway_registrations, }, - ip_packet_router: IpPacketRouterPaths { + ip_packet_router: IpPacketRouterPathsV9 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1447,7 +1448,7 @@ pub async fn try_upgrade_config_v8>( .ip_packet_router .gateway_registrations, }, - authenticator: AuthenticatorPaths { + authenticator: AuthenticatorPathsV9 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1487,8 +1488,8 @@ pub async fn try_upgrade_config_v8>( }, open_proxy: old_cfg.service_providers.open_proxy, upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { + network_requester: NetworkRequesterV9 { + debug: NetworkRequesterDebugV9 { enabled: old_cfg.service_providers.network_requester.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1502,8 +1503,8 @@ pub async fn try_upgrade_config_v8>( .client_debug, }, }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { + ip_packet_router: IpPacketRouterV9 { + debug: IpPacketRouterDebugV9 { enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1517,8 +1518,8 @@ pub async fn try_upgrade_config_v8>( .client_debug, }, }, - authenticator: Authenticator { - debug: AuthenticatorDebug { + authenticator: AuthenticatorV9 { + debug: AuthenticatorDebugV9 { enabled: old_cfg.service_providers.authenticator.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1528,12 +1529,12 @@ pub async fn try_upgrade_config_v8>( client_debug: old_cfg.service_providers.authenticator.debug.client_debug, }, }, - debug: service_providers::Debug { + debug: ServiceProvidersConfigDebugV9 { message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, }, }, metrics: Default::default(), - logging: LoggingSettings {}, + logging: LoggingSettingsV9 {}, debug: Default::default(), }; Ok(cfg) diff --git a/nym-node/src/config/old_configs/old_config_v9.rs b/nym-node/src/config/old_configs/old_config_v9.rs new file mode 100644 index 0000000000..839a54b679 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v9.rs @@ -0,0 +1,1712 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::old_configs::old_config_v10::{ + AuthenticatorDebugV10, AuthenticatorPathsV10, AuthenticatorV10, ClientBandwidthDebugV10, + ConfigV10, GatewayTasksConfigDebugV10, GatewayTasksConfigV10, GatewayTasksPathsV10, HostV10, + HttpV10, IpPacketRouterDebugV10, IpPacketRouterPathsV10, IpPacketRouterV10, KeysPathsV10, + LoggingSettingsV10, MixnetDebugV10, MixnetV10, NetworkRequesterDebugV10, + NetworkRequesterPathsV10, NetworkRequesterV10, NodeModesV10, NymNodePathsV10, + ReplayProtectionDebugV10, ReplayProtectionPathsV10, ReplayProtectionV10, + ServiceProvidersConfigDebugV10, ServiceProvidersConfigV10, ServiceProvidersPathsV10, + StaleMessageDebugV10, VerlocDebugV10, VerlocV10, WireguardPathsV10, WireguardV10, + ZkNymTicketHandlerDebugV10, +}; +use crate::config::persistence::{ + DEFAULT_PRIMARY_X25519_SPHINX_KEY_FILENAME, DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME, +}; +use crate::config::{NodeModes, DEFAULT_HTTP_PORT}; +use crate::error::{KeyIOFailure, NymNodeError}; +use crate::node::helpers::{get_current_rotation_id, load_key, store_key}; +use crate::node::key_rotation::key::SphinxPrivateKey; +use celes::Country; +use clap::ValueEnum; +use nym_client_core_config_types::DebugConfig as ClientDebugConfig; +use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; +use nym_config::helpers::{in6addr_any_init, inaddr_any}; +use nym_config::{ + defaults::TICKETBOOK_VALIDITY_DAYS, + read_config_from_toml_file, + serde_helpers::{de_maybe_port, de_maybe_stringified}, +}; +use nym_crypto::asymmetric::x25519; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tracing::{debug, instrument}; +use url::Url; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardPathsV9 { + pub private_diffie_hellman_key_file: PathBuf, + pub public_diffie_hellman_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV9 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `[::]:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV9, +} + +// a temporary solution until all "types" are run at the same time +#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum NodeModeV9 { + #[default] + #[clap(alias = "mix")] + Mixnode, + + #[clap(alias = "entry", alias = "gateway")] + EntryGateway, + + // to not break existing behaviour, this means exit capabilities AND entry capabilities + #[clap(alias = "exit")] + ExitGateway, + + // will start only SP needed for exit capabilities WITHOUT entry routing + ExitProvidersOnly, +} + +impl From for NodeModes { + fn from(config: NodeModeV9) -> Self { + match config { + NodeModeV9::Mixnode => *NodeModes::default().with_mixnode(), + NodeModeV9::EntryGateway => *NodeModes::default().with_entry(), + // in old version exit implied entry + NodeModeV9::ExitGateway => *NodeModes::default().with_entry().with_exit(), + NodeModeV9::ExitProvidersOnly => *NodeModes::default().with_exit(), + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +pub struct NodeModesV9 { + /// Specifies whether this node can operate in a mixnode mode. + pub mixnode: bool, + + /// Specifies whether this node can operate in an entry mode. + pub entry: bool, + + /// Specifies whether this node can operate in an exit mode. + pub exit: bool, + // TODO: would it make sense to also put WG here for completion? +} + +// TODO: this is very much a WIP. we need proper ssl certificate support here +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HostV9 { + /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. + /// If no values are provided, when this node gets included in the network, + /// its ip addresses will be populated by whatever value is resolved by associated nym-api. + pub public_ips: Vec, + + /// Optional hostname of this node, for example nymtech.net. + // TODO: this is temporary. to be replaced by pulling the data directly from the certs. + #[serde(deserialize_with = "de_maybe_stringified")] + pub hostname: Option, + + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[serde(deserialize_with = "de_maybe_stringified")] + pub location: Option, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetDebugV9 { + /// Specifies the duration of time this node is willing to delay a forward packet for. + #[serde(with = "humantime_serde")] + pub maximum_forward_packet_delay: Duration, + + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) + pub unsafe_disable_noise: bool, +} + +impl MixnetDebugV9 { + // given that genuine clients are using mean delay of 50ms, + // the probability of them delaying for over 10s is 10^-87 + // which for all intents and purposes will never happen + pub(crate) const DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY: Duration = Duration::from_secs(10); + pub(crate) const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = + Duration::from_millis(10_000); + pub(crate) const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = + Duration::from_millis(300_000); + pub(crate) const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); + pub(crate) const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; +} + +impl Default for MixnetDebugV9 { + fn default() -> Self { + MixnetDebugV9 { + maximum_forward_packet_delay: Self::DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY, + packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, + maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // to be changed by @SW once the implementation is there + unsafe_disable_noise: true, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnetV9 { + /// Address this node will bind to for listening for mixnet packets + /// default: `[::]:1789` + pub bind_address: SocketAddr, + + /// 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. + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option, + + /// Addresses to nym APIs from which the node gets the view of the network. + pub nym_api_urls: Vec, + + /// Addresses to nyxd which the node uses to interact with the nyx chain. + pub nyxd_urls: Vec, + + /// Settings for controlling replay detection + pub replay_protection: ReplayProtectionV9, + + #[serde(default)] + pub debug: MixnetDebugV9, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct ReplayProtectionV9 { + /// Paths for current bloomfilters + pub storage_paths: ReplayProtectionPathsV9, + + #[serde(default)] + pub debug: ReplayProtectionDebugV9, +} + +impl ReplayProtectionV9 { + pub fn new_default>(data_dir: P) -> Self { + ReplayProtectionV9 { + storage_paths: ReplayProtectionPathsV9::new(data_dir), + debug: Default::default(), + } + } +} + +pub const DEFAULT_RD_BLOOMFILTER_SUBDIR: &str = "replay-detection"; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ReplayProtectionPathsV9 { + /// Path to the directory storing currently used bloomfilter(s). + pub current_bloomfilters_directory: PathBuf, +} + +impl ReplayProtectionPathsV9 { + pub fn new>(data_dir: P) -> Self { + ReplayProtectionPathsV9 { + current_bloomfilters_directory: data_dir.as_ref().join(DEFAULT_RD_BLOOMFILTER_SUBDIR), + } + } +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct ReplayProtectionDebugV9 { + /// Specifies whether this node should **NOT** use replay protection + pub unsafe_disabled: bool, + + /// How long the processing task is willing to skip mutex acquisition before it will block the thread + /// until it actually obtains it + pub maximum_replay_detection_deferral: Duration, + + /// How many packets the processing task is willing to queue before it will block the thread + /// until it obtains the mutex + pub maximum_replay_detection_pending_packets: usize, + + /// Probability of false positives, fraction between 0 and 1 or a number indicating 1-in-p + pub false_positive_rate: f64, + + /// Defines initial expected number of packets this node will process a second, + /// so that an initial bloomfilter could be established. + /// As the node is running and BF are cleared, the value will be adjusted dynamically + pub initial_expected_packets_per_second: usize, + + /// Defines minimum expected number of packets this node will process a second + /// when used for calculating the BF size after reset. + /// This is to avoid degenerate cases where node receives 0 packets (because say it's misconfigured) + /// and it constructs an empty bloomfilter. + pub bloomfilter_minimum_packets_per_second_size: usize, + + /// Specifies the amount the bloomfilter size is going to get multiplied by after each reset. + /// It's performed in case the traffic rates increase before the next bloomfilter update. + pub bloomfilter_size_multiplier: f64, + + // NOTE: this field is temporary until replay detection bloomfilter rotation is tied + // to key rotation + /// Specifies how often the bloomfilter is cleared + #[serde(with = "humantime_serde")] + pub bloomfilter_reset_rate: Duration, + + /// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash + #[serde(with = "humantime_serde")] + pub bloomfilter_disk_flushing_rate: Duration, +} + +impl ReplayProtectionDebugV9 { + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL: Duration = Duration::from_millis(50); + + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS: usize = 100; + + // 12% (completely arbitrary) + pub const DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER: f64 = 1.12; + + // 10^-5 + pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5; + + // 25h (key rotation will be happening every 24h + 1h of overlap) + pub const DEFAULT_REPLAY_DETECTION_BF_RESET_RATE: Duration = Duration::from_secs(25 * 60 * 60); + + // we must have some reasonable balance between losing values and trashing the disk. + // since on average HDD it would take ~30s to save a 2GB bloomfilter + pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60); + + // this value will have to be adjusted in the future + pub const DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND: usize = 2000; + + pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; +} + +impl Default for ReplayProtectionDebugV9 { + fn default() -> Self { + ReplayProtectionDebugV9 { + unsafe_disabled: false, + maximum_replay_detection_deferral: Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL, + maximum_replay_detection_pending_packets: + Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS, + false_positive_rate: Self::DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE, + initial_expected_packets_per_second: Self::DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND, + bloomfilter_minimum_packets_per_second_size: + Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE, + bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER, + bloomfilter_reset_rate: Self::DEFAULT_REPLAY_DETECTION_BF_RESET_RATE, + bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV9 { + /// Path to file containing ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing x25519 sphinx private key. + pub private_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 sphinx public key. + pub public_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 noise private key. + pub private_x25519_noise_key_file: PathBuf, + + /// Path to file containing x25519 noise public key. + pub public_x25519_noise_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NymNodePathsV9 { + pub keys: KeysPathsV9, + + /// Path to a file containing basic node description: human-readable name, website, details, etc. + pub description: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HttpV9 { + /// Socket address this node will use for binding its http API. + /// default: `[::]:8080` + pub bind_address: SocketAddr, + + /// Path to assets directory of custom landing page of this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub landing_page_assets_path: Option, + + /// An optional bearer token for accessing certain http endpoints. + /// Currently only used for obtaining mixnode's stats. + #[serde(default)] + pub access_token: Option, + + /// Specify whether basic system information should be exposed. + /// default: true + pub expose_system_info: bool, + + /// Specify whether basic system hardware information should be exposed. + /// This option is superseded by `expose_system_info` + /// default: true + pub expose_system_hardware: bool, + + /// Specify whether detailed system crypto hardware information should be exposed. + /// This option is superseded by `expose_system_hardware` + /// default: true + pub expose_crypto_hardware: bool, + + /// Specify the cache ttl of the node load. + /// default: 30s + #[serde(with = "humantime_serde")] + pub node_load_cache_ttl: Duration, +} + +impl HttpV9 { + pub const DEFAULT_NODE_LOAD_CACHE_TTL: Duration = Duration::from_secs(30); +} + +impl Default for HttpV9 { + fn default() -> Self { + HttpV9 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT), + landing_page_assets_path: None, + access_token: None, + expose_system_info: true, + expose_system_hardware: true, + expose_crypto_hardware: true, + node_load_cache_ttl: Self::DEFAULT_NODE_LOAD_CACHE_TTL, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodePathsV9 {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV9 { + /// Delay between each subsequent node statistics being logged to the console + #[serde(with = "humantime_serde")] + pub node_stats_logging_delay: Duration, + + /// Delay between each subsequent node statistics being updated + #[serde(with = "humantime_serde")] + pub node_stats_updating_delay: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocDebugV9 { + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the connection to get established. + #[serde(with = "humantime_serde")] + pub connection_timeout: Duration, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + #[serde(with = "humantime_serde")] + pub packet_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + #[serde(with = "humantime_serde")] + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + #[serde(with = "humantime_serde")] + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + #[serde(with = "humantime_serde")] + pub retry_timeout: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocV9 { + /// Socket address this node will use for binding its verloc API. + /// default: `[::]:1790` + pub bind_address: SocketAddr, + + /// 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. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + + #[serde(default)] + pub debug: VerlocDebugV9, +} + +impl VerlocV9 { + pub const DEFAULT_VERLOC_PORT: u16 = DEFAULT_VERLOC_LISTENING_PORT; +} + +impl Default for VerlocV9 { + fn default() -> Self { + VerlocV9 { + bind_address: SocketAddr::new(in6addr_any_init(), Self::DEFAULT_VERLOC_PORT), + announce_port: None, + debug: Default::default(), + } + } +} + +impl VerlocDebugV9 { + const DEFAULT_PACKETS_PER_NODE: usize = 100; + const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); + const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); + const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); + const DEFAULT_BATCH_SIZE: usize = 50; + const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); + const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); +} + +impl Default for VerlocDebugV9 { + fn default() -> Self { + VerlocDebugV9 { + packets_per_node: Self::DEFAULT_PACKETS_PER_NODE, + connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT, + packet_timeout: Self::DEFAULT_PACKET_TIMEOUT, + delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE, + testing_interval: Self::DEFAULT_TESTING_INTERVAL, + retry_timeout: Self::DEFAULT_RETRY_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodeConfigV9 { + pub storage_paths: MixnodePathsV9, + + pub verloc: VerlocV9, + + #[serde(default)] + pub debug: DebugV9, +} + +impl DebugV9 { + const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); + const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); +} + +impl Default for DebugV9 { + fn default() -> Self { + DebugV9 { + node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY, + node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayPathsV9 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys and available client bandwidths. + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, + + pub authenticator: AuthenticatorPathsV9, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebugV9 { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebugV9 { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + + // use min(4/5 of max validity, validity - 1), but making sure it's no greater than 1 day + // ASSUMPTION: our validity period is AT LEAST 2 days + // + // this could have been a constant, but it's more readable as a function + pub const fn default_maximum_time_between_redemption() -> Duration { + let desired_secs = TICKETBOOK_VALIDITY_DAYS * (86400 * 4) / 5; + let desired_secs_alt = (TICKETBOOK_VALIDITY_DAYS - 1) * 86400; + + // can't use `min` in const context + let target_secs = if desired_secs < desired_secs_alt { + desired_secs + } else { + desired_secs_alt + }; + + assert!( + target_secs > 86400, + "the maximum time between redemption can't be lower than 1 day!" + ); + Duration::from_secs(target_secs as u64) + } +} + +impl Default for ZkNymTicketHandlerDebugV9 { + fn default() -> Self { + ZkNymTicketHandlerDebugV9 { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::default_maximum_time_between_redemption(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigDebugV9 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + pub zk_nym_tickets: ZkNymTicketHandlerDebugV9, +} + +impl EntryGatewayConfigDebugV9 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for EntryGatewayConfigDebugV9 { + fn default() -> Self { + EntryGatewayConfigDebugV9 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigV9 { + pub storage_paths: EntryGatewayPathsV9, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: EntryGatewayConfigDebugV9, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NetworkRequesterPathsV9 { + /// Path to file containing network requester ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct IpPacketRouterPathsV9 { + /// Path to file containing ip packet router ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AuthenticatorPathsV9 { + /// Path to file containing authenticator ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayPathsV9 { + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV9, + + pub ip_packet_router: IpPacketRouterPathsV9, + + pub authenticator: AuthenticatorPathsV9, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct AuthenticatorV9 { + #[serde(default)] + pub debug: AuthenticatorDebugV9, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct AuthenticatorDebugV9 { + /// Specifies whether authenticator service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run + /// the authenticator. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for AuthenticatorDebugV9 { + fn default() -> Self { + AuthenticatorDebugV9 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AuthenticatorV9 { + fn default() -> Self { + AuthenticatorV9 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebugV9 { + /// Specifies whether ip packet routing service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for IpPacketRouterDebugV9 { + fn default() -> Self { + IpPacketRouterDebugV9 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouterV9 { + #[serde(default)] + pub debug: IpPacketRouterDebugV9, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouterV9 { + fn default() -> Self { + IpPacketRouterV9 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebugV9 { + /// Specifies whether network requester service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for NetworkRequesterDebugV9 { + fn default() -> Self { + NetworkRequesterDebugV9 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterV9 { + #[serde(default)] + pub debug: NetworkRequesterDebugV9, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV9 { + fn default() -> Self { + NetworkRequesterV9 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayDebugV9 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ExitGatewayDebugV9 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ExitGatewayDebugV9 { + fn default() -> Self { + ExitGatewayDebugV9 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayConfigV9 { + pub storage_paths: ExitGatewayPathsV9, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV9, + + pub ip_packet_router: IpPacketRouterV9, + + #[serde(default)] + pub debug: ExitGatewayDebugV9, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksPathsV9 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct StaleMessageDebugV9 { + /// Specifies how often the clean-up task should check for stale data. + #[serde(with = "humantime_serde")] + pub cleaner_run_interval: Duration, + + /// Specifies maximum age of stored messages before they are removed from the storage + #[serde(with = "humantime_serde")] + pub max_age: Duration, +} + +impl StaleMessageDebugV9 { + const DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_STALE_MESSAGES_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); +} + +impl Default for StaleMessageDebugV9 { + fn default() -> Self { + StaleMessageDebugV9 { + cleaner_run_interval: Self::DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL, + max_age: Self::DEFAULT_STALE_MESSAGES_MAX_AGE, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ClientBandwidthDebugV9 { + /// Defines maximum delay between client bandwidth information being flushed to the persistent storage. + pub max_flushing_rate: Duration, + + /// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage. + pub max_delta_flushing_amount: i64, +} + +impl ClientBandwidthDebugV9 { + const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); + const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB +} + +impl Default for ClientBandwidthDebugV9 { + fn default() -> Self { + ClientBandwidthDebugV9 { + max_flushing_rate: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE, + max_delta_flushing_amount: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct GatewayTasksConfigDebugV9 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + + /// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies + /// of the services providers + pub minimum_mix_performance: u8, + + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + #[serde(alias = "maximum_auth_request_age")] + pub max_request_timestamp_skew: Duration, + + pub stale_messages: StaleMessageDebugV9, + + pub client_bandwidth: ClientBandwidthDebugV9, + + pub zk_nym_tickets: ZkNymTicketHandlerDebugV9, +} + +impl GatewayTasksConfigDebugV9 { + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; +} + +impl Default for GatewayTasksConfigDebugV9 { + fn default() -> Self { + GatewayTasksConfigDebugV9 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + max_request_timestamp_skew: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW, + minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, + stale_messages: Default::default(), + client_bandwidth: Default::default(), + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksConfigV9 { + pub storage_paths: GatewayTasksPathsV9, + + /// Indicates whether this gateway is accepting only zk-nym credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub ws_bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: GatewayTasksConfigDebugV9, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersPathsV9 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys, available client bandwidths and wireguard peers. + pub clients_storage: PathBuf, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV9, + + pub ip_packet_router: IpPacketRouterPathsV9, + + pub authenticator: AuthenticatorPathsV9, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigDebugV9 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ServiceProvidersConfigDebugV9 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ServiceProvidersConfigDebugV9 { + fn default() -> Self { + ServiceProvidersConfigDebugV9 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigV9 { + pub storage_paths: ServiceProvidersPathsV9, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV9, + + pub ip_packet_router: IpPacketRouterV9, + + pub authenticator: AuthenticatorV9, + + #[serde(default)] + pub debug: ServiceProvidersConfigDebugV9, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsConfigV9 { + #[serde(default)] + pub debug: MetricsDebugV9, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsDebugV9 { + /// Specify whether running statistics of this node should be logged to the console. + pub log_stats_to_console: bool, + + /// Specify the rate of which the metrics aggregator should call the `on_update` methods of all its registered handlers. + #[serde(with = "humantime_serde")] + pub aggregator_update_rate: Duration, + + /// Specify the target rate of clearing old stale mixnet metrics. + #[serde(with = "humantime_serde")] + pub stale_mixnet_metrics_cleaner_rate: Duration, + + /// Specify the target rate of updating global prometheus counters. + #[serde(with = "humantime_serde")] + pub global_prometheus_counters_update_rate: Duration, + + /// Specify the target rate of updating egress packets pending delivery counter. + #[serde(with = "humantime_serde")] + pub pending_egress_packets_update_rate: Duration, + + /// Specify the rate of updating clients sessions + #[serde(with = "humantime_serde")] + pub clients_sessions_update_rate: Duration, + + /// If console logging is enabled, specify the interval at which that happens + #[serde(with = "humantime_serde")] + pub console_logging_update_interval: Duration, + + /// Specify the update rate of running stats for the legacy `/metrics/mixing` endpoint + #[serde(with = "humantime_serde")] + pub legacy_mixing_metrics_update_rate: Duration, +} + +impl MetricsDebugV9 { + const DEFAULT_CONSOLE_LOGGING_INTERVAL: Duration = Duration::from_millis(60_000); + const DEFAULT_LEGACY_MIXING_UPDATE_RATE: Duration = Duration::from_millis(30_000); + const DEFAULT_AGGREGATOR_UPDATE_RATE: Duration = Duration::from_secs(5); + const DEFAULT_STALE_MIXNET_METRICS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const DEFAULT_CLIENT_SESSIONS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL: Duration = Duration::from_secs(30); + const DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE: Duration = Duration::from_secs(30); +} + +impl Default for MetricsDebugV9 { + fn default() -> Self { + MetricsDebugV9 { + log_stats_to_console: true, + console_logging_update_interval: Self::DEFAULT_CONSOLE_LOGGING_INTERVAL, + legacy_mixing_metrics_update_rate: Self::DEFAULT_LEGACY_MIXING_UPDATE_RATE, + aggregator_update_rate: Self::DEFAULT_AGGREGATOR_UPDATE_RATE, + stale_mixnet_metrics_cleaner_rate: Self::DEFAULT_STALE_MIXNET_METRICS_UPDATE_RATE, + global_prometheus_counters_update_rate: + Self::GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL, + pending_egress_packets_update_rate: Self::DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE, + clients_sessions_update_rate: Self::DEFAULT_CLIENT_SESSIONS_UPDATE_RATE, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV9 { + // well, we need to implement something here at some point... +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV9 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV9, + + pub host: HostV9, + + pub mixnet: MixnetV9, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV9, + + #[serde(default)] + pub http: HttpV9, + + #[serde(default)] + pub verloc: VerlocV9, + + pub wireguard: WireguardV9, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV9, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV9, + + #[serde(default)] + pub metrics: MetricsConfigV9, + + #[serde(default)] + pub logging: LoggingSettingsV9, + + #[serde(default)] + pub debug: DebugV9, +} + +impl ConfigV9 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV9 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +async fn upgrade_sphinx_key(old_cfg: &ConfigV9) -> Result<(PathBuf, PathBuf), NymNodeError> { + // we mark the current sphinx key as the primary and attach the current rotation id + let rotation_id = + get_current_rotation_id(&old_cfg.mixnet.nym_api_urls, &old_cfg.mixnet.nyxd_urls).await?; + + let current_sphinx_key_path = &old_cfg.storage_paths.keys.private_x25519_sphinx_key_file; + let current_pubkey_path = &old_cfg.storage_paths.keys.public_x25519_sphinx_key_file; + + let current_sphinx_key: x25519::PrivateKey = + load_key(current_sphinx_key_path, "sphinx private key")?; + + let keys_dir = current_sphinx_key_path + .parent() + .ok_or(NymNodeError::DataDirDerivationFailure)?; + + let primary_key_path = keys_dir.join(DEFAULT_PRIMARY_X25519_SPHINX_KEY_FILENAME); + let secondary_key_path = keys_dir.join(DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME); + + let primary_key = SphinxPrivateKey::import(current_sphinx_key, rotation_id); + store_key(&primary_key, &primary_key_path, "sphinx private key")?; + + // no point in keeping the old sphinx files + fs::remove_file(current_sphinx_key_path).map_err(|err| KeyIOFailure::KeyRemovalFailure { + key: "sphinx private key".to_string(), + path: current_sphinx_key_path.clone(), + err, + })?; + fs::remove_file(current_pubkey_path).map_err(|err| KeyIOFailure::KeyRemovalFailure { + key: "sphinx public key".to_string(), + path: current_pubkey_path.clone(), + err, + })?; + + Ok((primary_key_path, secondary_key_path)) +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v9>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v9 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV9::read_from_path(&path)? + }; + + let (primary_x25519_sphinx_key_file, secondary_x25519_sphinx_key_file) = + upgrade_sphinx_key(&old_cfg).await?; + + let cfg = ConfigV10 { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModesV10 { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + host: HostV10 { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: MixnetV10 { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + replay_protection: ReplayProtectionV10 { + storage_paths: ReplayProtectionPathsV10 { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebugV10 { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: Default::default(), + debug: MixnetDebugV10 { + maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + ..Default::default() + }, + }, + storage_paths: NymNodePathsV10 { + keys: KeysPathsV10 { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + primary_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + secondary_x25519_sphinx_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: HttpV10 { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: VerlocV10 { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebugV10 { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.verloc.debug.retry_timeout, + }, + }, + wireguard: WireguardV10 { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_port: old_cfg.wireguard.announced_port, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + storage_paths: WireguardPathsV10 { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + gateway_tasks: GatewayTasksConfigV10 { + storage_paths: GatewayTasksPathsV10 { + clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + debug: GatewayTasksConfigDebugV10 { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebugV10 { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebugV10 { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebugV10 { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + }, + }, + service_providers: ServiceProvidersConfigV10 { + storage_paths: ServiceProvidersPathsV10 { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPathsV10 { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPathsV10 { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPathsV10 { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequesterV10 { + debug: NetworkRequesterDebugV10 { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouterV10 { + debug: IpPacketRouterDebugV10 { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: AuthenticatorV10 { + debug: AuthenticatorDebugV10 { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: ServiceProvidersConfigDebugV10 { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettingsV10 {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/persistence.rs b/nym-node/src/config/persistence.rs index 9de7f868cc..087bcd5b76 100644 --- a/nym-node/src/config/persistence.rs +++ b/nym-node/src/config/persistence.rs @@ -13,8 +13,8 @@ use zeroize::Zeroizing; // Global: pub const DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME: &str = "ed25519_identity"; pub const DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME: &str = "ed25519_identity.pub"; -pub const DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME: &str = "x25519_sphinx"; -pub const DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME: &str = "x25519_sphinx.pub"; +pub const DEFAULT_PRIMARY_X25519_SPHINX_KEY_FILENAME: &str = "x25519_sphinx_primary"; +pub const DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME: &str = "x25519_sphinx_secondary"; pub const DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME: &str = "x25519_noise"; pub const DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME: &str = "x25519_noise.pub"; pub const DEFAULT_NYMNODE_DESCRIPTION_FILENAME: &str = "description.toml"; @@ -59,7 +59,6 @@ pub const DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME: &str = "x25519_wg_dh.pub"; pub const DEFAULT_RD_BLOOMFILTER_SUBDIR: &str = "replay-detection"; pub const DEFAULT_RD_BLOOMFILTER_FILE_EXT: &str = "bloom"; pub const DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT: &str = "flush"; -pub const CURRENT_RD_BLOOMFILTER_FILENAME: &str = "current"; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -82,7 +81,6 @@ impl NymNodePaths { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] -#[serde(deny_unknown_fields)] pub struct KeysPaths { /// Path to file containing ed25519 identity private key. pub private_ed25519_identity_key_file: PathBuf, @@ -90,11 +88,11 @@ pub struct KeysPaths { /// Path to file containing ed25519 identity public key. pub public_ed25519_identity_key_file: PathBuf, - /// Path to file containing x25519 sphinx private key. - pub private_x25519_sphinx_key_file: PathBuf, + /// Path to file containing the primary x25519 sphinx private key. + pub primary_x25519_sphinx_key_file: PathBuf, - /// Path to file containing x25519 sphinx public key. - pub public_x25519_sphinx_key_file: PathBuf, + /// Path to file containing the secondary x25519 sphinx private key. + pub secondary_x25519_sphinx_key_file: PathBuf, /// Path to file containing x25519 noise private key. pub private_x25519_noise_key_file: PathBuf, @@ -112,9 +110,10 @@ impl KeysPaths { .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), public_ed25519_identity_key_file: data_dir .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), - private_x25519_sphinx_key_file: data_dir - .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), - public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), + primary_x25519_sphinx_key_file: data_dir + .join(DEFAULT_PRIMARY_X25519_SPHINX_KEY_FILENAME), + secondary_x25519_sphinx_key_file: data_dir + .join(DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME), private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), } @@ -127,13 +126,6 @@ impl KeysPaths { ) } - pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_x25519_sphinx_key_file, - &self.public_x25519_sphinx_key_file, - ) - } - pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath { nym_pemstore::KeyPairPath::new( &self.private_x25519_noise_key_file, @@ -504,20 +496,6 @@ pub struct ReplayProtectionPaths { pub current_bloomfilters_directory: PathBuf, } -impl ReplayProtectionPaths { - pub fn current_bloomfilter_filepath(&self) -> PathBuf { - self.current_bloomfilters_directory - .join(CURRENT_RD_BLOOMFILTER_FILENAME) - .with_extension(DEFAULT_RD_BLOOMFILTER_FILE_EXT) - } - - pub fn current_bloomfilter_being_flushed_filepath(&self) -> PathBuf { - self.current_bloomfilters_directory - .join(CURRENT_RD_BLOOMFILTER_FILENAME) - .with_extension(DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT) - } -} - impl ReplayProtectionPaths { pub fn new>(data_dir: P) -> Self { ReplayProtectionPaths { diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index 96f66b246b..bf8ed72710 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -84,11 +84,11 @@ private_ed25519_identity_key_file = '{{ storage_paths.keys.private_ed25519_ident # Path to file containing ed25519 identity public key. public_ed25519_identity_key_file = '{{ storage_paths.keys.public_ed25519_identity_key_file }}' -# Path to file containing x25519 sphinx private key. -private_x25519_sphinx_key_file = '{{ storage_paths.keys.private_x25519_sphinx_key_file }}' +# Path to file containing the primary x25519 sphinx private key. +primary_x25519_sphinx_key_file = '{{ storage_paths.keys.primary_x25519_sphinx_key_file }}' -# Path to file containing x25519 sphinx public key. -public_x25519_sphinx_key_file = '{{ storage_paths.keys.public_x25519_sphinx_key_file }}' +# Path to file containing the secondary x25519 sphinx private key. +secondary_x25519_sphinx_key_file = '{{ storage_paths.keys.secondary_x25519_sphinx_key_file }}' # Path to file containing x25519 noise private key. private_x25519_noise_key_file = '{{ storage_paths.keys.private_x25519_noise_key_file }}' @@ -145,7 +145,11 @@ private_ipv6 = '{{ wireguard.private_ipv6 }}' # Port announced to external clients wishing to connect to the wireguard interface. # Useful in the instances where the node is behind a proxy. -announced_port = {{ wireguard.announced_port }} +announced_tunnel_port = {{ wireguard.announced_tunnel_port }} + +# Port announced to external clients wishing to connect to the metadata service. +# Useful in the instances where the node is behind a proxy. +announced_metadata_port = {{ wireguard.announced_metadata_port }} # The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. # The maximum value for IPv4 is 32 diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index 11a12afb76..bdb10ec8b7 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -7,7 +7,6 @@ use crate::error::NymNodeError; use std::path::Path; use tracing::debug; -// currently there are no upgrades async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v1(path, None).await.ok(); let cfg = try_upgrade_config_v2(path, cfg).await.ok(); @@ -16,10 +15,12 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v5(path, cfg).await.ok(); let cfg = try_upgrade_config_v6(path, cfg).await.ok(); let cfg = try_upgrade_config_v7(path, cfg).await.ok(); - match try_upgrade_config_v8(path, cfg).await { + let cfg = try_upgrade_config_v8(path, cfg).await.ok(); + let cfg = try_upgrade_config_v9(path, cfg).await.ok(); + match try_upgrade_config_v10(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { - tracing::error!("Failed to finish upgrade - {e}"); + tracing::error!("Failed to finish upgrade: {e}"); Err(NymNodeError::FailedUpgrade) } } diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index 519f927e69..37bf7eb59a 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -5,6 +5,7 @@ use crate::node::http::error::NymNodeHttpError; use crate::wireguard::error::WireguardError; use nym_http_api_client::HttpClientError; use nym_ip_packet_router::error::ClientCoreError; +use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::ValidatorClientError; use std::io; use std::net::IpAddr; @@ -45,6 +46,32 @@ pub enum KeyIOFailure { #[source] err: io::Error, }, + + #[error("failed to move {key} key from '{}' to '{}': {err}", source.display(), destination.display())] + KeyMoveFailure { + key: String, + source: PathBuf, + destination: PathBuf, + #[source] + err: io::Error, + }, + + #[error("failed to copy {key} key from '{}' to '{}': {err}", source.display(), destination.display())] + KeyCopyFailure { + key: String, + source: PathBuf, + destination: PathBuf, + #[source] + err: io::Error, + }, + + #[error("failed to remove {key} key from '{}': {err}", path.display())] + KeyRemovalFailure { + key: String, + path: PathBuf, + #[source] + err: io::Error, + }, } #[derive(Debug, Error)] @@ -58,6 +85,9 @@ pub enum NymNodeError { source: io::Error, }, + #[error("received shutdown signal while attempting to complete the action")] + ShutdownReceived, + #[error("could not find an existing config file at '{}' and fresh node initialisation has been disabled", config_path.display())] ForbiddenInitialisation { config_path: PathBuf }, @@ -115,6 +145,23 @@ pub enum NymNodeError { #[error("this node hasn't set any valid public addresses to announce. Please modify [host.public_ips] section of your config")] NoPublicIps, + #[error("there are no available nym api endpoints")] + NoNymApiUrls, + + #[error("failed to resolve nym-api query - no nodes returned a valid response")] + NymApisExhausted, + + #[error("the current epoch appears to be stuck")] + StuckEpoch, + + // this should never happen in normal usage, but it's better to throw it than to panic + // in case of bugs + #[error("sphinx keys have already been consumed to spawn the node tasks")] + ConsumedSphinxKeys, + + #[error("failed to resolve chain query: {0}")] + NyxdFailure(#[from] NyxdError), + #[error("this node attempted to announce an invalid public address: {address}. Please modify [host.public_ips] section of your config. Alternatively, if you wanted to use it in the local setting, run the node with the '--local' flag.")] InvalidPublicIp { address: IpAddr }, @@ -157,14 +204,17 @@ pub enum NymNodeError { #[error("failed to save/load the bloomfilter: {source} using path: {}", path.display())] BloomfilterIoFailure { source: io::Error, path: PathBuf }, + #[error("failed to deserialise bloomfilter metadata")] + BloomfilterMetadataDeserialisationFailure, + #[error(transparent)] - GatewayFailure(#[from] nym_gateway::GatewayError), + GatewayFailure(Box), #[error(transparent)] GatewayTasksStartupFailure(Box), #[error(transparent)] - EntryGatewayFailure(#[from] EntryGatewayError), + EntryGatewayFailure(Box), #[error(transparent)] ServiceProvidersFailure(#[from] ServiceProvidersError), @@ -177,6 +227,18 @@ pub enum NymNodeError { FailedUpgrade, } +impl From for NymNodeError { + fn from(error: EntryGatewayError) -> Self { + NymNodeError::EntryGatewayFailure(Box::new(error)) + } +} + +impl From for NymNodeError { + fn from(error: nym_gateway::GatewayError) -> Self { + NymNodeError::GatewayFailure(Box::new(error)) + } +} + impl NymNodeError { pub fn config_validation_failure>(error: S) -> Self { NymNodeError::ConfigValidationFailure { @@ -219,7 +281,13 @@ pub enum EntryGatewayError { }, #[error("entry gateway failure: {0}")] - External(#[from] nym_gateway::GatewayError), + External(Box), +} + +impl From for EntryGatewayError { + fn from(error: nym_gateway::GatewayError) -> Self { + EntryGatewayError::External(Box::new(error)) + } } #[derive(Debug, Error)] diff --git a/nym-node/src/logging.rs b/nym-node/src/logging.rs index 332e274944..e1812059be 100644 --- a/nym-node/src/logging.rs +++ b/nym-node/src/logging.rs @@ -5,8 +5,9 @@ use nym_bin_common::logging::{default_tracing_env_filter, default_tracing_fmt_la use tracing_subscriber::filter::Directive; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Layer}; -pub(crate) fn granual_filtered_env() -> anyhow::Result { +pub(crate) fn granual_filtered_env() -> anyhow::Result { fn directive_checked(directive: impl Into) -> anyhow::Result { directive.into().parse().map_err(From::from) } @@ -16,19 +17,28 @@ pub(crate) fn granual_filtered_env() -> anyhow::Result anyhow::Result { - Ok(tracing_subscriber::registry() - .with(default_tracing_fmt_layer(std::io::stderr)) - .with(granual_filtered_env()?)) -} - pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { - build_tracing_logger()?.init(); + let stderr_layer = + default_tracing_fmt_layer(std::io::stderr).with_filter(granual_filtered_env()?); + + cfg_if::cfg_if! {if #[cfg(feature = "tokio-console")] { + // instrument tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time + let console_layer = console_subscriber::spawn(); + + tracing_subscriber::registry() + .with(console_layer) + .with(stderr_layer) + .init(); + } else { + tracing_subscriber::registry() + .with(stderr_layer) + .init(); + }} Ok(()) } diff --git a/nym-node/src/node/helpers.rs b/nym-node/src/node/helpers.rs index aa9e3525e4..bdb00df7b1 100644 --- a/nym-node/src/node/helpers.rs +++ b/nym-node/src/node/helpers.rs @@ -3,13 +3,42 @@ use crate::config::NodeModes; use crate::error::{KeyIOFailure, NymNodeError}; +use crate::node::key_rotation::key::{SphinxPrivateKey, SphinxPublicKey}; +use crate::node::nym_apis_client::NymApisClient; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_node_requests::api::v1::node::models::NodeDescription; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use nym_pemstore::KeyPairPath; +use nym_task::ShutdownToken; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; +use nym_validator_client::QueryHttpRpcNyxdClient; use serde::Serialize; use std::fmt::{Display, Formatter}; use std::path::Path; +use tracing::warn; +use url::Url; + +#[derive(Debug, Serialize)] +pub(crate) struct DisplaySphinxKey { + public_key: String, + rotation_id: u32, +} + +impl From<&SphinxPrivateKey> for DisplaySphinxKey { + fn from(value: &SphinxPrivateKey) -> Self { + let pubkey: SphinxPublicKey = value.into(); + DisplaySphinxKey { + public_key: pubkey.inner.to_base58_string(), + rotation_id: pubkey.rotation_id, + } + } +} + +impl Display for DisplaySphinxKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} (rotation: {})", self.public_key, self.rotation_id) + } +} #[derive(Debug, Serialize)] pub(crate) struct DisplayDetails { @@ -18,7 +47,8 @@ pub(crate) struct DisplayDetails { pub(crate) description: NodeDescription, pub(crate) ed25519_identity_key: String, - pub(crate) x25519_sphinx_key: String, + pub(crate) x25519_primary_sphinx_key: DisplaySphinxKey, + pub(crate) x25519_secondary_sphinx_key: Option, pub(crate) x25519_noise_key: String, pub(crate) x25519_wireguard_key: String, @@ -39,7 +69,14 @@ impl Display for DisplayDetails { )?; writeln!(f, "details: '{}'", self.description.details)?; writeln!(f, "ed25519 identity: {}", self.ed25519_identity_key)?; - writeln!(f, "x25519 sphinx: {}", self.x25519_sphinx_key)?; + writeln!( + f, + "x25519 primary sphinx: {}", + self.x25519_primary_sphinx_key + )?; + if let Some(secondary) = &self.x25519_secondary_sphinx_key { + writeln!(f, "x25519 primary sphinx: {secondary}")?; + } writeln!(f, "x25519 noise: {}", self.x25519_noise_key)?; writeln!( f, @@ -61,24 +98,24 @@ impl Display for DisplayDetails { } pub(crate) fn load_keypair( - paths: KeyPairPath, + paths: &KeyPairPath, name: impl Into, ) -> Result { - nym_pemstore::load_keypair(&paths).map_err(|err| KeyIOFailure::KeyPairLoadFailure { + nym_pemstore::load_keypair(paths).map_err(|err| KeyIOFailure::KeyPairLoadFailure { keys: name.into(), - paths, + paths: paths.clone(), err, }) } pub(crate) fn store_keypair( keys: &T, - paths: KeyPairPath, + paths: &KeyPairPath, name: impl Into, ) -> Result<(), KeyIOFailure> { - nym_pemstore::store_keypair(keys, &paths).map_err(|err| KeyIOFailure::KeyPairStoreFailure { + nym_pemstore::store_keypair(keys, paths).map_err(|err| KeyIOFailure::KeyPairStoreFailure { keys: name.into(), - paths, + paths: paths.clone(), err, }) } @@ -108,7 +145,7 @@ where } pub(crate) fn load_ed25519_identity_keypair( - paths: KeyPairPath, + paths: &KeyPairPath, ) -> Result { Ok(load_keypair(paths, "ed25519-identity")?) } @@ -120,41 +157,54 @@ pub(crate) fn load_ed25519_identity_public_key>( Ok(load_key(path, "ed25519-identity-public-key")?) } -pub(crate) fn load_x25519_sphinx_keypair( - paths: KeyPairPath, -) -> Result { - Ok(load_keypair(paths, "x25519-sphinx")?) -} - pub(crate) fn load_x25519_noise_keypair( - paths: KeyPairPath, + paths: &KeyPairPath, ) -> Result { Ok(load_keypair(paths, "x25519-noise")?) } pub(crate) fn load_x25519_wireguard_keypair( - paths: KeyPairPath, + paths: &KeyPairPath, ) -> Result { Ok(load_keypair(paths, "x25519-wireguard")?) } pub(crate) fn store_ed25519_identity_keypair( keys: &ed25519::KeyPair, - paths: KeyPairPath, + paths: &KeyPairPath, ) -> Result<(), NymNodeError> { Ok(store_keypair(keys, paths, "ed25519-identity")?) } -pub(crate) fn store_x25519_sphinx_keypair( - keys: &x25519::KeyPair, - paths: KeyPairPath, -) -> Result<(), NymNodeError> { - Ok(store_keypair(keys, paths, "x25519-sphinx")?) -} - pub(crate) fn store_x25519_noise_keypair( keys: &x25519::KeyPair, - paths: KeyPairPath, + paths: &KeyPairPath, ) -> Result<(), NymNodeError> { Ok(store_keypair(keys, paths, "x25519-noise")?) } + +pub(crate) async fn get_current_rotation_id( + nym_apis: &[Url], + fallback_nyxd: &[Url], +) -> Result { + let apis_client = NymApisClient::new(nym_apis, ShutdownToken::ephemeral())?; + if let Ok(rotation_info) = apis_client.get_key_rotation_info().await.map(|r| r.details) { + if rotation_info.is_epoch_stuck() { + return Err(NymNodeError::StuckEpoch); + } + let current_epoch = rotation_info.current_absolute_epoch_id; + return Ok(rotation_info + .key_rotation_state + .key_rotation_id(current_epoch)); + } + warn!("failed to retrieve key rotation id from nym apis. falling back to contract query"); + + for nyxd_url in fallback_nyxd { + let client = QueryHttpRpcNyxdClient::connect_to_default_env(nyxd_url.as_str())?; + if let Ok(res) = client.get_key_rotation_id().await { + return Ok(res.rotation_id); + } + } + + Err(NymNodeError::NymApisExhausted) +} diff --git a/nym-node/src/node/http/helpers/mod.rs b/nym-node/src/node/http/helpers/mod.rs index a819bdd062..6375f304e4 100644 --- a/nym-node/src/node/http/helpers/mod.rs +++ b/nym-node/src/node/http/helpers/mod.rs @@ -1,38 +1,4 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::Config; -use crate::error::NymNodeError; -use crate::node::http::api::api_requests; -use crate::node::http::error::NymNodeHttpError; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_node_requests::api::SignedHostInformation; - pub mod system_info; - -pub(crate) fn sign_host_details( - config: &Config, - x22519_sphinx: &x25519::PublicKey, - x25519_noise: &x25519::PublicKey, - ed22519_identity: &ed25519::KeyPair, -) -> Result { - let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { - None - } else { - Some(*x25519_noise) - }; - - let host_info = api_requests::v1::node::models::HostInformation { - ip_address: config.host.public_ips.clone(), - hostname: config.host.hostname.clone(), - keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: *ed22519_identity.public_key(), - x25519_sphinx: *x22519_sphinx, - x25519_noise, - }, - }; - - let signed_info = SignedHostInformation::new(host_info, ed22519_identity.private_key()) - .map_err(NymNodeHttpError::from)?; - Ok(signed_info) -} diff --git a/nym-node/src/node/http/router/api/v1/node/host_information.rs b/nym-node/src/node/http/router/api/v1/node/host_information.rs index 76eb5ddfa0..7bb036ea1f 100644 --- a/nym-node/src/node/http/router/api/v1/node/host_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/host_information.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use axum::extract::Query; +use crate::node::http::api::api_requests; +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::{v1::node::models::SignedHostInformation, SignedDataHostInfo}; @@ -20,11 +22,54 @@ use nym_node_requests::api::{v1::node::models::SignedHostInformation, SignedData params(OutputParams) )] pub(crate) async fn host_information( - host_information: SignedHostInformation, Query(output): Query, + State(state): State, ) -> HostInformationResponse { let output = output.output.unwrap_or_default(); - output.to_response(host_information) + + let primary_key = state.x25519_sphinx_keys.primary(); + let pre_announced = match state.x25519_sphinx_keys.secondary() { + None => None, + Some(secondary_key) => { + if secondary_key.rotation_id() == primary_key.rotation_id() + 1 { + Some(api_requests::v1::node::models::SphinxKey { + rotation_id: secondary_key.rotation_id(), + public_key: secondary_key.x25519_pubkey(), + }) + } else { + None + } + } + }; + + let primary_pubkey = primary_key.x25519_pubkey(); + + #[allow(deprecated)] + let host_info = api_requests::v1::node::models::HostInformation { + ip_address: state.static_information.ip_addresses.clone(), + hostname: state.static_information.hostname.clone(), + keys: api_requests::v1::node::models::HostKeys { + ed25519_identity: *state.static_information.ed25519_identity_keys.public_key(), + x25519_sphinx: primary_pubkey, + primary_x25519_sphinx_key: api_requests::v1::node::models::SphinxKey { + rotation_id: primary_key.rotation_id(), + public_key: primary_pubkey, + }, + x25519_versioned_noise: state.static_information.x25519_versioned_noise_key, + pre_announced_x25519_sphinx_key: pre_announced, + }, + }; + + // SAFETY: the only way for this call to fail is if serialisation of HostInformation fails. + // however, that conversion is stable and infallible + #[allow(clippy::unwrap_used)] + let signed_info = SignedHostInformation::new( + host_info, + state.static_information.ed25519_identity_keys.private_key(), + ) + .unwrap(); + + output.to_response(signed_info) } pub type HostInformationResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/node/mod.rs b/nym-node/src/node/http/router/api/v1/node/mod.rs index f62b17ab3b..3b115602b7 100644 --- a/nym-node/src/node/http/router/api/v1/node/mod.rs +++ b/nym-node/src/node/http/router/api/v1/node/mod.rs @@ -7,6 +7,7 @@ use crate::node::http::api::v1::node::description::description; use crate::node::http::api::v1::node::hardware::host_system; use crate::node::http::api::v1::node::host_information::host_information; use crate::node::http::api::v1::node::roles::roles; +use crate::node::http::state::AppState; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::node::models; @@ -22,14 +23,13 @@ pub mod roles; #[derive(Debug, Clone)] pub struct Config { pub build_information: models::BinaryBuildInformationOwned, - pub host_information: models::SignedHostInformation, pub system_info: Option, pub roles: models::NodeRoles, pub description: models::NodeDescription, pub auxiliary_details: models::AuxiliaryDetails, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(config: Config) -> Router { Router::new() .route( v1::BUILD_INFO, @@ -45,13 +45,7 @@ pub(super) fn routes(config: Config) -> Router move |query| roles(node_roles, query) }), ) - .route( - v1::HOST_INFO, - get({ - let host_info = config.host_information; - move |query| host_information(host_info, query) - }), - ) + .route(v1::HOST_INFO, get(host_information)) .route( v1::SYSTEM_INFO, get({ diff --git a/nym-node/src/node/http/router/mod.rs b/nym-node/src/node/http/router/mod.rs index 9750b1c0c1..c296afcea9 100644 --- a/nym-node/src/node/http/router/mod.rs +++ b/nym-node/src/node/http/router/mod.rs @@ -16,7 +16,6 @@ use nym_node_requests::api::v1::mixnode::models::Mixnode; use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use nym_node_requests::api::v1::network_requester::models::NetworkRequester; use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, HostSystem, NodeDescription}; -use nym_node_requests::api::SignedHostInformation; use nym_node_requests::routes; use std::net::SocketAddr; use std::path::Path; @@ -34,14 +33,13 @@ pub struct HttpServerConfig { } impl HttpServerConfig { - pub fn new(host_information: SignedHostInformation) -> Self { + pub fn new() -> Self { HttpServerConfig { landing: Default::default(), api: api::Config { v1_config: api::v1::Config { node: api::v1::node::Config { build_information: bin_info_owned!(), - host_information, system_info: None, roles: Default::default(), description: Default::default(), @@ -118,6 +116,7 @@ impl HttpServerConfig { self } + #[must_use] pub fn with_prometheus_bearer_token(mut self, bearer_token: Option) -> Self { self.api.v1_config.metrics.bearer_token = bearer_token.map(|b| Arc::new(Zeroizing::new(b))); self @@ -159,7 +158,7 @@ impl NymNodeRouter { ) .nest(routes::LANDING_PAGE, landing_page::routes(config.landing)) .nest(routes::API, api::routes(config.api)) - .layer(axum::middleware::from_fn(logging::logger)) + .layer(axum::middleware::from_fn(logging::log_request_info)) .with_state(state), } } diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index 33dcf4ea9f..9cd3348cb6 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -1,20 +1,36 @@ -// Copyright 2023-2024 - Nym Technologies SA +// Copyright 2023-2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::state::load::CachedNodeLoad; use crate::node::http::state::metrics::MetricsAppState; +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use nym_crypto::asymmetric::ed25519; use nym_node_metrics::NymNodeMetrics; +use nym_noise_keys::VersionedNoiseKey; use nym_verloc::measurements::SharedVerlocStats; +use std::net::IpAddr; +use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; pub mod load; pub mod metrics; +pub(crate) struct StaticNodeInformation { + pub(crate) ed25519_identity_keys: Arc, + pub(crate) x25519_versioned_noise_key: Option, + pub(crate) ip_addresses: Vec, + pub(crate) hostname: Option, +} + #[derive(Clone)] -pub struct AppState { +pub(crate) struct AppState { pub(crate) startup_time: Instant, + pub(crate) static_information: Arc, + + pub(crate) x25519_sphinx_keys: ActiveSphinxKeys, + pub(crate) cached_load: CachedNodeLoad, pub(crate) metrics: MetricsAppState, @@ -22,12 +38,17 @@ pub struct AppState { impl AppState { #[allow(clippy::new_without_default)] - pub fn new( + pub(crate) fn new( + static_information: StaticNodeInformation, + x25519_sphinx_keys: ActiveSphinxKeys, metrics: NymNodeMetrics, verloc: SharedVerlocStats, load_cache_ttl: Duration, ) -> Self { AppState { + static_information: Arc::new(static_information), + x25519_sphinx_keys, + // is it 100% accurate? // no. // does it have to be? diff --git a/nym-node/src/node/key_rotation/active_keys.rs b/nym-node/src/node/key_rotation/active_keys.rs new file mode 100644 index 0000000000..d018db4e88 --- /dev/null +++ b/nym-node/src/node/key_rotation/active_keys.rs @@ -0,0 +1,160 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::key_rotation::key::SphinxPrivateKey; +use arc_swap::{ArcSwap, ArcSwapOption, Guard}; +use std::ops::Deref; +use std::sync::Arc; +use tracing::error; + +#[derive(Clone)] +pub(crate) struct ActiveSphinxKeys { + inner: Arc, +} + +struct ActiveSphinxKeysInner { + /// Key that's currently used as the default when processing packets with no explicit rotation information + primary_key: ArcSwap, + + /// Optionally, a secondary key associated with this node. depending on the context it could either be + /// the pre-announced key for the following rotation or a key from the previous rotation for the overlap period + secondary_key: ArcSwapOption, +} + +impl ActiveSphinxKeys { + pub(crate) fn new_fresh(primary: SphinxPrivateKey) -> Self { + ActiveSphinxKeys { + inner: Arc::new(ActiveSphinxKeysInner { + primary_key: ArcSwap::from_pointee(primary), + secondary_key: Default::default(), + }), + } + } + + pub(crate) fn new_loaded( + primary: SphinxPrivateKey, + secondary: Option, + ) -> Self { + ActiveSphinxKeys { + inner: Arc::new(ActiveSphinxKeysInner { + primary_key: ArcSwap::from_pointee(primary), + secondary_key: ArcSwapOption::from_pointee(secondary), + }), + } + } + + pub(crate) fn even(&self) -> Option { + let primary = self.inner.primary_key.load(); + if primary.is_even_rotation() { + return Some(SphinxKeyGuard::Primary(primary)); + } + self.secondary() + } + + pub(crate) fn odd(&self) -> Option { + let primary = self.inner.primary_key.load(); + if !primary.is_even_rotation() { + return Some(SphinxKeyGuard::Primary(primary)); + } + self.secondary() + } + + pub(crate) fn primary(&self) -> SphinxKeyGuard { + SphinxKeyGuard::Primary(self.inner.primary_key.load()) + } + + pub(crate) fn secondary(&self) -> Option { + let guard = self.inner.secondary_key.load(); + if guard.is_none() { + return None; + } + + Some(SphinxKeyGuard::Secondary(SecondaryKeyGuard { guard })) + } + + pub(crate) fn set_secondary(&self, new_key: SphinxPrivateKey) { + self.inner.secondary_key.store(Some(Arc::new(new_key))) + } + + pub(crate) fn primary_key_rotation_id(&self) -> u32 { + self.inner.primary_key.load().rotation_id() + } + + pub(crate) fn secondary_key_rotation_id(&self) -> Option { + self.inner + .secondary_key + .load() + .as_ref() + .map(|k| k.rotation_id()) + } + + // set the secondary (pre-announced key) as the primary + // and the current primary as the secondary (for the overlap epoch) + pub(crate) fn rotate(&self, expected_new_rotation: u32) -> bool { + let Some(pre_announced) = self.inner.secondary_key.load_full() else { + error!("sphinx key inconsistency - attempted to perform key rotation without having pre-announced new key"); + return false; + }; + + if pre_announced.rotation_id() != expected_new_rotation { + error!("sphinx key inconsistency - pre-announced key rotation id != primary + 1"); + return false; + } + + let old_primary = self.inner.primary_key.swap(pre_announced); + self.inner.secondary_key.store(Some(old_primary)); + true + } + + pub(crate) fn deactivate_secondary(&self) { + self.inner.secondary_key.store(None); + } +} + +pub(crate) enum SphinxKeyGuard { + // Primary(Guard>), + Primary(Guard>), + Secondary(SecondaryKeyGuard), +} + +impl Deref for SphinxKeyGuard { + type Target = SphinxPrivateKey; + + fn deref(&self) -> &Self::Target { + match self { + SphinxKeyGuard::Primary(g) => g.deref(), + SphinxKeyGuard::Secondary(g) => g.deref(), + } + } +} + +// enum SecondaryKey { +// PreAnnounced(SphinxPrivateKey), +// PreviousOverlap(SphinxPrivateKey), +// } + +// impl Deref for SecondaryKey { +// type Target = SphinxPrivateKey; +// +// fn deref(&self) -> &Self::Target { +// match self { +// SecondaryKey::PreAnnounced(key) => &key, +// SecondaryKey::PreviousOverlap(key) => &key, +// } +// } +// } + +pub(crate) struct SecondaryKeyGuard { + guard: Guard>>, + // guard: Guard>>, +} + +impl Deref for SecondaryKeyGuard { + type Target = SphinxPrivateKey; + + fn deref(&self) -> &Self::Target { + // SAFETY: the guard is ONLY constructed when the key is 'Some' + #[allow(clippy::unwrap_used)] + self.guard.as_ref().unwrap() + } +} diff --git a/nym-node/src/node/key_rotation/controller.rs b/nym-node/src/node/key_rotation/controller.rs new file mode 100644 index 0000000000..0aa8aa5fa5 --- /dev/null +++ b/nym-node/src/node/key_rotation/controller.rs @@ -0,0 +1,381 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::node::key_rotation::manager::SphinxKeyManager; +use crate::node::nym_apis_client::NymApisClient; +use crate::node::replay_protection::manager::ReplayProtectionBloomfiltersManager; +use futures::pin_mut; +use nym_task::ShutdownToken; +use nym_validator_client::models::{KeyRotationDetails, KeyRotationInfoResponse, KeyRotationState}; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::time::{interval, sleep, Instant}; +use tracing::{debug, error, info, trace, warn}; + +pub(crate) struct RotationConfig { + epoch_duration: Duration, + rotation_state: KeyRotationState, +} + +impl RotationConfig { + fn rotation_lifetime(&self) -> Duration { + (self.rotation_state.validity_epochs + 1) * self.epoch_duration + } +} + +impl From for RotationConfig { + fn from(value: KeyRotationInfoResponse) -> Self { + RotationConfig { + epoch_duration: value.details.epoch_duration, + rotation_state: value.details.key_rotation_state, + } + } +} + +pub(crate) struct KeyRotationController { + // regular polling rate to catch any changes in the system config. they shouldn't happen too often + // so the requests can be sent quite infrequently + regular_polling_interval: Duration, + + rotation_config: RotationConfig, + replay_protection_manager: ReplayProtectionBloomfiltersManager, + client: NymApisClient, + managed_keys: SphinxKeyManager, + shutdown_token: ShutdownToken, +} + +struct NextAction { + typ: KeyRotationActionState, + deadline: OffsetDateTime, +} + +impl NextAction { + fn new(typ: KeyRotationActionState, deadline: OffsetDateTime) -> Self { + NextAction { typ, deadline } + } + + fn until_deadline(&self) -> Duration { + let now = OffsetDateTime::now_utc(); + Duration::try_from(self.deadline - now).unwrap_or_else(|_| { + // deadline is already in the past + Duration::from_nanos(0) + }) + } + + fn wait(duration: Duration) -> NextAction { + NextAction::new( + KeyRotationActionState::Wait, + OffsetDateTime::now_utc() + duration, + ) + } + + fn pre_announce(rotation_id: u32, deadline: OffsetDateTime) -> Self { + NextAction::new( + KeyRotationActionState::PreAnnounce { rotation_id }, + deadline, + ) + } + + fn swap_default(expected_new_rotation: u32, deadline: OffsetDateTime) -> Self { + NextAction::new( + KeyRotationActionState::SwapDefault { + expected_new_rotation, + }, + deadline, + ) + } + + fn purge_secondary(deadline: OffsetDateTime) -> Self { + NextAction::new(KeyRotationActionState::PurgeOld, deadline) + } +} + +#[derive(Debug, Clone, Copy)] +enum KeyRotationActionState { + // generate and pre-announce new key to the nym-api(s) + PreAnnounce { rotation_id: u32 }, + + // perform the following exchange + // primary -> secondary + // pre_announced -> primary + SwapDefault { expected_new_rotation: u32 }, + + // remove the old overlap key and purge associated data like the replay detection bloomfilter + PurgeOld, + + // a no-op action that has only a single purpose - wait (used to handle slight desyncs) + Wait, +} + +impl KeyRotationController { + pub(crate) fn new( + config: &Config, + rotation_config: RotationConfig, + client: NymApisClient, + replay_protection_manager: ReplayProtectionBloomfiltersManager, + managed_keys: SphinxKeyManager, + shutdown_token: ShutdownToken, + ) -> Self { + KeyRotationController { + regular_polling_interval: config + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + rotation_config, + replay_protection_manager, + client, + managed_keys, + shutdown_token, + } + } + + async fn try_determine_next_action(&self) -> NextAction { + let now = OffsetDateTime::now_utc(); + let Some(key_rotation_info) = self.try_get_key_rotation_info().await else { + warn!("failed to retrieve key rotation information"); + return NextAction::wait(Duration::from_secs(240)); + }; + + // check if we think the epoch is stuck (we're already 20% or more into following epoch with no advancement) + if key_rotation_info.is_epoch_stuck() { + warn!("the epoch is stuck - can't progress with key rotation"); + return NextAction::wait(Duration::from_secs(240)); + } + + // >>>>> START: determine if we called this method pre-maturely due to clock skew + + // current rotation id as determined by the current epoch id + let current_rotation_id = key_rotation_info.current_key_rotation_id(); + + // expected rotation id as determined by the current TIME + // used to determined epoch stalling or clocks being slightly out of sync + let expected_current_rotation_id = key_rotation_info.expected_current_rotation_id(); + + if current_rotation_id != expected_current_rotation_id { + warn!("the current rotation is {current_rotation_id} whilst we expected {expected_current_rotation_id}"); + // if we got here, it means epoch is most likely NOT stuck (we're within the threshold) + // so probably we prematurely called this method before nym-api(s) got to advancing + // the epoch and thus the rotation, so wait a bit instead. + return NextAction::wait(Duration::from_secs(30)); + } + // >>>>> END: determine if we called this method pre-maturely due to clock skew + + // if we're less than 30s until next rotation, we probably started our binary in a rather + // unfortunate time, just wait until the next rotation rather than do all the work only to throw it + // away immediately + let Some(until_next_rotation) = key_rotation_info.until_next_rotation() else { + debug!("key rotation is overdue - waiting..."); + return NextAction::wait(Duration::from_secs(30)); + }; + if until_next_rotation < Duration::from_secs(30) { + debug!("less than 30s until next rotation - waiting until then"); + return NextAction::wait(Duration::from_secs(30)); + } + + let current_epoch = key_rotation_info.current_absolute_epoch_id; + + // epoch id of when the current rotation has started + let current_rotation_start_epoch = key_rotation_info.current_rotation_starting_epoch_id(); + + // epoch id of when the new rotation id is meant to start + let next_rotation_start_epoch = key_rotation_info.next_rotation_starting_epoch_id(); + + let secondary_key_rotation_id = self.managed_keys.keys.secondary_key_rotation_id(); + let primary_key_rotation_id = self.managed_keys.keys.primary_key_rotation_id(); + + debug!( + "current rotation: {current_rotation_id}, primary: {}, secondary: {secondary_key_rotation_id:?}", + self.managed_keys.keys.primary_key_rotation_id() + ); + + let rotates_next_epoch = next_rotation_start_epoch == current_epoch + 1; + let next_rotation_id = current_rotation_id + 1; + + let Some(secondary_key_rotation_id) = secondary_key_rotation_id else { + debug!("we don't have a secondary key"); + // figure out if we already have appropriate key (like we crashed or this is the first time node is running) + // or whether we have to regenerate anything or, which is the most likely case, we're waiting to + // pre-announce new key for the following rotation + + if primary_key_rotation_id != current_rotation_id { + warn!("current primary key does not correspond to the current rotation - immediately pre-announcing new key (rotates next epoch: {rotates_next_epoch}"); + // we don't have a secondary key and our current key is already outdated - + // preannounce a key for either this or the next rotation + // (and next time this method is called, it will be promoted to primary) + return if rotates_next_epoch { + NextAction::pre_announce(next_rotation_id, now) + } else { + NextAction::pre_announce(current_rotation_id, now) + }; + } + + // we have a primary key corresponding to the current rotation, so we just have to pre-announce + // a key for the next rotation an epoch before the rotation + let deadline = key_rotation_info.epoch_start_time(next_rotation_start_epoch - 1); + debug!( + "going to pre-announce secondary key for rotation {next_rotation_id} on {deadline}" + ); + return NextAction::pre_announce(next_rotation_id, deadline); + }; + + // the current secondary key corresponds to the next rotation, i.e. this is the pre-announced key + if secondary_key_rotation_id == next_rotation_id { + debug!("secondary key is for the NEXT rotation - we need to swap into it"); + + let deadline = key_rotation_info.epoch_start_time(next_rotation_start_epoch); + return NextAction::swap_default(next_rotation_id, deadline); + } + + if secondary_key_rotation_id == current_rotation_id { + debug!("secondary key is for the CURRENT rotation - we need to swap into it"); + + return NextAction::swap_default(current_rotation_id, now); + } + + if secondary_key_rotation_id < current_rotation_id { + let deadline = if secondary_key_rotation_id == current_rotation_id - 1 { + debug!("secondary key is from the PREVIOUS rotations - we need to purge it"); + // we purge the key after the end of overlap period, i.e. during the 2nd epoch of a rotation + key_rotation_info.epoch_start_time(current_rotation_start_epoch + 1) + } else { + debug!("secondary key is from AN OLD rotation - we need to purge it"); + // the key is from some old rotation, we were probably offline for some time - we need to pre-announce new key + // for the upcoming rotation, so start off by purging this key immediately + now + }; + + return NextAction::purge_secondary(deadline); + } + + // at this point all branches should have been covered, i.e. missing secondary key, + // secondary key == next rotation + // secondary key == current rotation + // secondary key < current rotation + // the only, theoretical, branch is if secondary key was from few rotations in the future, + // but this would require some weird chain shenanigans + error!("this code branch should have been unreachable - please report if you see this error with the following information:\ + primary_key_rotation = {primary_key_rotation_id}, + secondary_key_rotation = {secondary_key_rotation_id}, + current_rotation = {current_rotation_id}, + next_rotation = {next_rotation_id}, + raw_response = {key_rotation_info:?}"); + + NextAction::wait(Duration::from_secs(240)) + } + + async fn try_get_key_rotation_info(&self) -> Option { + let Ok(rotation_info) = self.client.get_key_rotation_info().await else { + warn!("failed to retrieve key rotation information from ANY nym-api - we might miss configuration changes"); + return None; + }; + + Some(rotation_info.details) + } + + async fn pre_announce_new_key(&self, rotation_id: u32) { + info!("pre-announcing new key for rotation {rotation_id}"); + if let Err(err) = self.managed_keys.generate_key_for_new_rotation(rotation_id) { + error!("failed to generate and store new sphinx key: {err}"); + return; + }; + + if self + .replay_protection_manager + .allocate_pre_announced(rotation_id, self.rotation_config.rotation_lifetime()) + .is_err() + { + // mutex poisoning - we have to exit + self.shutdown_token.cancel(); + } + + // no need to send the information explicitly to nym-apis, as they're scheduled to refresh + // self-described endpoints of all nodes before the key rotation epoch rolls over + } + + fn swap_default_key(&self, expected_new_rotation: u32) { + info!("attempting to swap the primary key to the previously generated one"); + if let Err(err) = self.managed_keys.rotate_keys(expected_new_rotation) { + error!("failed to perform sphinx key swap: {err}") + }; + if self + .replay_protection_manager + .promote_pre_announced() + .is_err() + { + // mutex poisoning - we have to exit + self.shutdown_token.cancel(); + } + } + + fn purge_old_rotation_data(&self) { + info!("purging data associated with the old sphinx key"); + if let Err(err) = self.managed_keys.remove_overlap_key() { + error!("failed to remove old sphinx key: {err}"); + }; + if self.replay_protection_manager.purge_secondary().is_err() { + // mutex poisoning - we have to exit + self.shutdown_token.cancel(); + } + } + + async fn execute_next_action(&self, action: KeyRotationActionState) { + match action { + KeyRotationActionState::PreAnnounce { rotation_id } => { + self.pre_announce_new_key(rotation_id).await + } + KeyRotationActionState::SwapDefault { + expected_new_rotation, + } => self.swap_default_key(expected_new_rotation), + KeyRotationActionState::PurgeOld => { + self.purge_old_rotation_data(); + } + KeyRotationActionState::Wait => {} + } + } + + pub(crate) async fn run(&self) { + info!("starting sphinx key rotation controller"); + + let mut polling_interval = interval(self.regular_polling_interval); + polling_interval.reset(); + + let mut next_action = self.try_determine_next_action().await; + debug!( + "next key rotation action to take: {:?} at {}", + next_action.typ, next_action.deadline + ); + let state_update_future = sleep(next_action.until_deadline()); + pin_mut!(state_update_future); + + while !self.shutdown_token.is_cancelled() { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("KeyRotationController: Received shutdown"); + break; + } + _ = polling_interval.tick() => {} + _ = &mut state_update_future => { + self.execute_next_action(next_action.typ).await + } + } + + next_action = self.try_determine_next_action().await; + debug!( + "next key rotation action to take: {:?} at {}", + next_action.typ, next_action.deadline + ); + state_update_future + .as_mut() + .reset(Instant::now() + next_action.until_deadline()); + } + + trace!("KeyRotationController: exiting") + } + + pub(crate) fn start(self) { + tokio::spawn(async move { self.run().await }); + } +} diff --git a/nym-node/src/node/key_rotation/key.rs b/nym-node/src/node/key_rotation/key.rs new file mode 100644 index 0000000000..7d31455ae5 --- /dev/null +++ b/nym-node/src/node/key_rotation/key.rs @@ -0,0 +1,137 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore}; +use nym_crypto::asymmetric::x25519; +use nym_pemstore::traits::PemStorableKey; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum MalformedSphinxKey { + #[error("inner x25519 key is malformed: {0}")] + X25519Failure(#[from] x25519::KeyRecoveryError), + + #[error("did not receive sufficient number of bytes to recover the key")] + Incomplete, +} + +pub(crate) struct SphinxPrivateKey { + rotation_id: u32, + inner: x25519::PrivateKey, +} + +impl SphinxPrivateKey { + pub(crate) fn new(rng: &mut R, rotation_id: u32) -> Self { + SphinxPrivateKey { + rotation_id, + inner: x25519::PrivateKey::new(rng), + } + } + + pub(crate) fn import(key: x25519::PrivateKey, rotation_id: u32) -> Self { + SphinxPrivateKey { + rotation_id, + inner: key, + } + } + + pub(crate) fn x25519_pubkey(&self) -> x25519::PublicKey { + self.inner.public_key() + } + + pub(crate) fn inner(&self) -> &x25519::PrivateKey { + &self.inner + } + + pub(crate) fn is_even_rotation(&self) -> bool { + self.rotation_id & 1 == 0 + } + + pub(crate) fn rotation_id(&self) -> u32 { + self.rotation_id + } +} + +impl From<&SphinxPrivateKey> for SphinxPublicKey { + fn from(value: &SphinxPrivateKey) -> Self { + SphinxPublicKey { + rotation_id: value.rotation_id, + inner: (&value.inner).into(), + } + } +} + +impl AsRef for SphinxPrivateKey { + fn as_ref(&self) -> &x25519::PrivateKey { + &self.inner + } +} + +pub(crate) struct SphinxPublicKey { + pub(crate) rotation_id: u32, + pub(crate) inner: x25519::PublicKey, +} + +impl AsRef for SphinxPublicKey { + fn as_ref(&self) -> &x25519::PublicKey { + &self.inner + } +} + +impl PemStorableKey for SphinxPrivateKey { + type Error = MalformedSphinxKey; + + fn pem_type() -> &'static str { + // it's fine (and actually desired) to attach 'SPHINX' here, as this is not a valid X25519 key by itself. + // this is because it also contains the encoded rotation id + "X25519 SPHINX PRIVATE KEY" + } + + fn to_bytes(&self) -> Vec { + self.rotation_id + .to_be_bytes() + .into_iter() + .chain(self.inner.to_bytes()) + .collect() + } + + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != x25519::PRIVATE_KEY_SIZE + 4 { + return Err(MalformedSphinxKey::Incomplete); + } + // SAFETY: we just checked we have sufficient bytes available + #[allow(clippy::unwrap_used)] + let rotation_id = u32::from_be_bytes(bytes[..4].try_into().unwrap()); + + Ok(SphinxPrivateKey { + rotation_id, + inner: x25519::PrivateKey::from_bytes(&bytes[4..])?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; + + #[test] + fn private_key_bytes_convertion() { + // Set up a deterministic RNG. + let seed = [42u8; 32]; + let mut rng = ChaCha20Rng::from_seed(seed); + + let key = SphinxPrivateKey { + rotation_id: 42, + inner: x25519::PrivateKey::new(&mut rng), + }; + + let bytes = key.to_bytes(); + assert_eq!(bytes.len(), 36); // 32 bytes for x25519 key and 4 bytes for rotation id + let recovered_key = SphinxPrivateKey::from_bytes(bytes.as_slice()).unwrap(); + + assert_eq!(recovered_key.rotation_id, 42); + assert_eq!(recovered_key.inner.to_bytes(), key.inner.to_bytes()); + } +} diff --git a/nym-node/src/node/key_rotation/manager.rs b/nym-node/src/node/key_rotation/manager.rs new file mode 100644 index 0000000000..0a4c0df27d --- /dev/null +++ b/nym-node/src/node/key_rotation/manager.rs @@ -0,0 +1,187 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::{KeyIOFailure, NymNodeError}; +use crate::node::helpers::{load_key, store_key}; +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use crate::node::key_rotation::key::{SphinxPrivateKey, SphinxPublicKey}; +use rand::rngs::OsRng; +use rand::{CryptoRng, RngCore}; +use std::fs; +use std::path::{Path, PathBuf}; +use tracing::{trace, warn}; + +pub(crate) struct SphinxKeyManager { + pub(crate) keys: ActiveSphinxKeys, + + primary_key_path: PathBuf, + secondary_key_path: PathBuf, +} + +impl SphinxKeyManager { + // only called by newly initialised nym-nodes + pub(crate) fn initialise_new( + rng: &mut R, + current_rotation_id: u32, + primary_key_path: P, + secondary_key_path: P, + ) -> Result + where + R: RngCore + CryptoRng, + P: AsRef, + { + let primary = SphinxPrivateKey::new(rng, current_rotation_id); + trace!("attempting to store primary x25519 sphinx key"); + + let primary_key_path = primary_key_path.as_ref(); + store_key(&primary, primary_key_path, "x25519 sphinx")?; + + Ok(SphinxKeyManager { + keys: ActiveSphinxKeys::new_fresh(primary), + primary_key_path: primary_key_path.to_path_buf(), + secondary_key_path: secondary_key_path.as_ref().to_path_buf(), + }) + } + + // moves the primary key to the secondary file + // and vice verse, i.e. secondary to the primary + fn swap_key_files>( + primary_path: P, + secondary_path: P, + ) -> Result<(), NymNodeError> { + let tmp_path = primary_path.as_ref().with_extension("tmp"); + + // 1. COPY: primary -> temp + fs::copy(primary_path.as_ref(), &tmp_path).map_err(|err| KeyIOFailure::KeyCopyFailure { + key: "old x25519 sphinx primary".to_string(), + source: primary_path.as_ref().to_path_buf(), + destination: tmp_path.clone(), + err, + })?; + + // 2. MOVE: secondary -> primary + fs::rename(secondary_path.as_ref(), primary_path.as_ref()).map_err(|err| { + KeyIOFailure::KeyMoveFailure { + key: "x25519 sphinx secondary".to_string(), + source: secondary_path.as_ref().to_path_buf(), + destination: primary_path.as_ref().to_path_buf(), + err, + } + })?; + + // 3. MOVE temp -> secondary + fs::rename(&tmp_path, secondary_path.as_ref()).map_err(|err| { + KeyIOFailure::KeyMoveFailure { + key: "old x25519 sphinx primary".to_string(), + source: tmp_path.clone(), + destination: primary_path.as_ref().to_path_buf(), + err, + } + })?; + + Ok(()) + } + + pub(crate) fn generate_key_for_new_rotation( + &self, + expected_rotation: u32, + ) -> Result { + let mut rng = OsRng; + let new = SphinxPrivateKey::new(&mut rng, expected_rotation); + let pub_key = (&new).into(); + store_key( + &new, + &self.secondary_key_path, + "x22519 (pre-announced) sphinx", + )?; + + self.keys.set_secondary(new); + Ok(pub_key) + } + + pub(crate) fn rotate_keys(&self, expected_new_rotation: u32) -> Result<(), NymNodeError> { + if !self.keys.rotate(expected_new_rotation) { + self.generate_key_for_new_rotation(expected_new_rotation)?; + self.keys.rotate(expected_new_rotation); + } + Self::swap_key_files(&self.primary_key_path, &self.secondary_key_path) + } + + pub(crate) fn remove_overlap_key(&self) -> Result<(), NymNodeError> { + self.keys.deactivate_secondary(); + fs::remove_file(&self.secondary_key_path).map_err(|err| { + KeyIOFailure::KeyRemovalFailure { + key: "old x25519 sphinx secondary".to_string(), + path: self.secondary_key_path.clone(), + err, + } + })?; + Ok(()) + } + + pub(crate) fn try_load_or_regenerate>( + current_rotation_id: u32, + primary_key_path: P, + secondary_key_path: P, + ) -> Result { + // if the temporary key exists, it means we crashed in the middle of rotating the key. + // rather than trying to figure out which exact step failed, just delete it and it will be redone + // (we still have the two keys, they just might be in the wrong order) + let tmp_location = primary_key_path.as_ref().with_extension("tmp"); + if tmp_location.exists() { + warn!("we seem to have crashed in the middle of rotating the sphinx key"); + fs::remove_file(&tmp_location).map_err(|err| KeyIOFailure::KeyRemovalFailure { + key: "old x25519 sphinx (temp location)".to_string(), + path: tmp_location, + err, + })?; + } + + // primary key should always be present + let mut primary: SphinxPrivateKey = + load_key(primary_key_path.as_ref(), "x25519 sphinx primary")?; + + let mut secondary: Option = if secondary_key_path.as_ref().exists() { + Some(load_key( + secondary_key_path.as_ref(), + "x25519 sphinx secondary", + )?) + } else { + None + }; + + let primary_id = primary.rotation_id(); + let secondary_id = secondary.as_ref().map(|k| k.rotation_id()); + + // 1. check for failed (or missed) rotation, i.e. secondary > primary AND current_rotation > primary + if let Some(secondary_id) = secondary_id { + if secondary_id > primary_id && current_rotation_id > primary_id { + Self::swap_key_files(primary_key_path.as_ref(), secondary_key_path.as_ref())?; + // SAFETY: we just checked secondary exists + #[allow(clippy::unwrap_used)] + let tmp = secondary.take().unwrap(); + secondary = Some(primary); + primary = tmp; + } + } + + // if upon loading it turns out that the node has been inactive for a long time, + // immediately rotate keys (but leave 1h grace period for current primary, i.e. set it as secondary) + if primary.rotation_id() != current_rotation_id { + warn!("this node has been inactive for more than a key rotation duration. the current primary key was generated for rotation {} while the current rotation is {current_rotation_id}. new key will be generated now.", primary.rotation_id()); + let this = SphinxKeyManager { + keys: ActiveSphinxKeys::new_loaded(primary, None), + primary_key_path: primary_key_path.as_ref().to_path_buf(), + secondary_key_path: secondary_key_path.as_ref().to_path_buf(), + }; + this.generate_key_for_new_rotation(current_rotation_id)?; + return Ok(this); + } + + Ok(SphinxKeyManager { + keys: ActiveSphinxKeys::new_loaded(primary, secondary), + primary_key_path: primary_key_path.as_ref().to_path_buf(), + secondary_key_path: secondary_key_path.as_ref().to_path_buf(), + }) + } +} diff --git a/nym-node/src/node/key_rotation/mod.rs b/nym-node/src/node/key_rotation/mod.rs new file mode 100644 index 0000000000..9e89bc4d28 --- /dev/null +++ b/nym-node/src/node/key_rotation/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod active_keys; +pub(crate) mod controller; +pub(crate) mod key; +pub(crate) mod manager; diff --git a/nym-node/src/node/metrics/handler/client_sessions.rs b/nym-node/src/node/metrics/handler/client_sessions.rs index 2f2326c541..19530440b3 100644 --- a/nym-node/src/node/metrics/handler/client_sessions.rs +++ b/nym-node/src/node/metrics/handler/client_sessions.rs @@ -7,13 +7,13 @@ use crate::node::metrics::handler::{ use async_trait::async_trait; use nym_gateway::node::PersistentStatsStorage; use nym_gateway_stats_storage::error::StatsStorageError; -use nym_gateway_stats_storage::models::{TicketType, ToSessionType}; use nym_node_metrics::entry::{ActiveSession, ClientSessions, FinishedSession}; use nym_node_metrics::events::GatewaySessionEvent; use nym_node_metrics::prometheus_wrapper::PrometheusMetric::EntryClientSessionsDurations; use nym_node_metrics::prometheus_wrapper::PROMETHEUS_METRICS; use nym_node_metrics::NymNodeMetrics; use nym_sphinx_types::DestinationAddressBytes; +use nym_statistics_common::types::SessionType; use time::{Date, Duration, OffsetDateTime}; use tracing::error; use tracing::log::trace; @@ -39,9 +39,6 @@ impl GatewaySessionStatsHandler { start_time: OffsetDateTime, client: DestinationAddressBytes, ) -> Result<(), StatsStorageError> { - self.storage - .insert_unique_user(self.current_day, client.as_base58_string()) - .await?; self.storage .insert_active_session(client, ActiveSession::new(start_time)) .await?; @@ -54,43 +51,39 @@ impl GatewaySessionStatsHandler { client: DestinationAddressBytes, ) -> Result<(), StatsStorageError> { if let Some(session) = self.storage.get_active_session(client).await? { - if let Some(finished_session) = session.end_at(stop_time) { - PROMETHEUS_METRICS.observe_histogram( - EntryClientSessionsDurations { - typ: finished_session.typ.to_string(), - }, - finished_session.duration.as_secs_f64(), - ); - - self.storage - .insert_finished_session(self.current_day, finished_session) - .await?; - self.storage.delete_active_session(client).await?; + if session.remember { + if let Some(finished_session) = session.end_at(stop_time) { + PROMETHEUS_METRICS.observe_histogram( + EntryClientSessionsDurations { + typ: finished_session.typ.to_string(), + }, + finished_session.duration.as_secs_f64(), + ); + self.storage + .insert_unique_user(self.current_day, client.as_base58_string()) + .await?; + self.storage + .insert_finished_session(self.current_day, finished_session) + .await?; + } } + self.storage.delete_active_session(client).await?; } Ok(()) } - async fn handle_ecash_ticket( + async fn handle_session_remember( &mut self, - ticket_type: TicketType, client: DestinationAddressBytes, + session_type: SessionType, ) -> Result<(), StatsStorageError> { + self.storage.remember_active_session(client).await?; self.storage - .update_active_session_type(client, ticket_type.to_session_type()) + .update_active_session_type(client, session_type) .await?; Ok(()) } - async fn handle_session_delete( - &mut self, - client: DestinationAddressBytes, - ) -> Result<(), StatsStorageError> { - self.storage.delete_active_session(client).await?; - self.storage.delete_unique_user(client).await?; - Ok(()) - } - async fn handle_session_event( &mut self, event: GatewaySessionEvent, @@ -104,15 +97,11 @@ impl GatewaySessionStatsHandler { self.handle_session_stop(stop_time, client).await } - GatewaySessionEvent::EcashTicket { - ticket_type, + // As long as remember is sent before stop, everything should work as expected + GatewaySessionEvent::SessionRemember { + session_type, client, - } => self.handle_ecash_ticket(ticket_type, client).await, - - // As long as delete is sent before stop, everything should work as expected - GatewaySessionEvent::SessionDelete { client } => { - self.handle_session_delete(client).await - } + } => self.handle_session_remember(client, session_type).await, } } @@ -172,6 +161,7 @@ impl GatewaySessionStatsHandler { //publish yesterday's data if any self.update_shared_stats(yesterday).await?; //store "active" sessions as duration 0 + //it's not guaranteed that these are to be remembered, but we can easily spot them for active_session in self.storage.get_all_active_sessions().await? { self.storage .insert_finished_session( diff --git a/nym-node/src/node/mixnet/handler.rs b/nym-node/src/node/mixnet/handler.rs index 1a6cf87ad3..aca227455a 100644 --- a/nym-node/src/node/mixnet/handler.rs +++ b/nym-node/src/node/mixnet/handler.rs @@ -3,14 +3,18 @@ use crate::node::mixnet::shared::SharedData; use futures::StreamExt; +use nym_noise::connection::Connection; +use nym_noise::upgrade_noise_responder; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_framing::codec::NymCodec; use nym_sphinx_framing::packet::FramedNymPacket; use nym_sphinx_framing::processing::{ process_framed_packet, MixProcessingResult, MixProcessingResultData, PacketProcessingError, - PartiallyUnwrappedPacket, ProcessedFinalHop, + PartiallyUnwrappedPacket, PartialyUnwrappedPacketWithKeyRotation, ProcessedFinalHop, }; +use nym_sphinx_params::SphinxKeyRotation; use nym_sphinx_types::{Delay, REPLAY_TAG_SIZE}; +use std::collections::HashMap; use std::mem; use std::net::SocketAddr; use tokio::net::TcpStream; @@ -19,41 +23,50 @@ use tokio_util::codec::Framed; use tracing::{debug, error, instrument, trace, warn}; struct PendingReplayCheckPackets { - packets: Vec, + // map of rotation id used for packet creation to the packets + packets: HashMap>, last_acquired_mutex: Instant, } impl PendingReplayCheckPackets { fn new() -> PendingReplayCheckPackets { PendingReplayCheckPackets { - packets: vec![], + packets: Default::default(), last_acquired_mutex: Instant::now(), } } - fn reset(&mut self, now: Instant) -> Vec { + fn reset(&mut self, now: Instant) -> HashMap> { self.last_acquired_mutex = now; mem::take(&mut self.packets) } - fn push(&mut self, now: Instant, packet: PartiallyUnwrappedPacket) { + fn push(&mut self, now: Instant, packet: PartialyUnwrappedPacketWithKeyRotation) { if self.packets.is_empty() { self.last_acquired_mutex = now; } - self.packets.push(packet); + self.packets + .entry(packet.used_key_rotation) + .or_default() + .push(packet.packet) } - fn replay_tags(&self) -> Vec<&[u8; REPLAY_TAG_SIZE]> { - let mut replay_tags = Vec::with_capacity(self.packets.len()); - for packet in &self.packets { - let Some(replay_tag) = packet.replay_tag() else { - error!( - "corrupted batch of {} packets - replay tag was missing", - self.packets.len() - ); - return Vec::new(); - }; - replay_tags.push(replay_tag); + fn replay_tags(&self) -> HashMap> { + let mut replay_tags = HashMap::with_capacity(self.packets.len()); + 'outer: for (rotation_id, packets) in &self.packets { + let mut rotation_replay_tags = Vec::with_capacity(packets.len()); + for packet in packets { + let Some(replay_tag) = packet.replay_tag() else { + error!( + "corrupted batch of {} packets - replay tag was missing", + self.packets.len() + ); + replay_tags.insert(*rotation_id, Vec::new()); + continue 'outer; + }; + rotation_replay_tags.push(replay_tag); + } + replay_tags.insert(*rotation_id, rotation_replay_tags); } replay_tags } @@ -61,7 +74,6 @@ impl PendingReplayCheckPackets { pub(crate) struct ConnectionHandler { shared: SharedData, - mixnet_connection: Framed, remote_address: SocketAddr, // packets pending for replay detection @@ -78,11 +90,7 @@ impl Drop for ConnectionHandler { } impl ConnectionHandler { - pub(crate) fn new( - shared: &SharedData, - tcp_stream: TcpStream, - remote_address: SocketAddr, - ) -> Self { + pub(crate) fn new(shared: &SharedData, remote_address: SocketAddr) -> Self { let shutdown = shared.shutdown.child_token(remote_address.to_string()); shared.metrics.network.new_active_ingress_mixnet_client(); @@ -93,11 +101,11 @@ impl ConnectionHandler { replay_protection_filter: shared.replay_protection_filter.clone(), mixnet_forwarder: shared.mixnet_forwarder.clone(), final_hop: shared.final_hop.clone(), + noise_config: shared.noise_config.clone(), metrics: shared.metrics.clone(), shutdown, }, remote_address, - mixnet_connection: Framed::new(tcp_stream, NymCodec), pending_packets: PendingReplayCheckPackets::new(), } } @@ -212,6 +220,56 @@ impl ConnectionHandler { time_threshold && count_threshold } + fn try_partially_unwrap_packet( + &self, + packet: FramedNymPacket, + ) -> Result { + // based on the received sphinx key rotation information, + // attempt to choose appropriate key for processing the packet + match packet.header().key_rotation { + SphinxKeyRotation::Unknown => { + let primary = self.shared.sphinx_keys.primary(); + let primary_rotation = primary.rotation_id(); + + // we have to try both keys, start with the primary as it has higher likelihood of being correct + // if let Ok(partially_unwrapped) = PartiallyUnwrappedPacket::new() + match PartiallyUnwrappedPacket::new(packet, primary.inner().as_ref()) { + Ok(unwrapped_packet) => { + Ok(unwrapped_packet.with_key_rotation(primary_rotation)) + } + Err((packet, err)) => { + if let Some(secondary) = self.shared.sphinx_keys.secondary() { + let secondary_rotation = secondary.rotation_id(); + PartiallyUnwrappedPacket::new(packet, secondary.inner().as_ref()) + .map_err(|(_, err)| err) + .map(|p| p.with_key_rotation(secondary_rotation)) + } else { + Err(err) + } + } + } + } + SphinxKeyRotation::OddRotation => { + let Some(odd_key) = self.shared.sphinx_keys.odd() else { + return Err(PacketProcessingError::ExpiredKey); + }; + let odd_rotation = odd_key.rotation_id(); + PartiallyUnwrappedPacket::new(packet, odd_key.inner().as_ref()) + .map_err(|(_, err)| err) + .map(|p| p.with_key_rotation(odd_rotation)) + } + SphinxKeyRotation::EvenRotation => { + let Some(even_key) = self.shared.sphinx_keys.even() else { + return Err(PacketProcessingError::ExpiredKey); + }; + let even_rotation = even_key.rotation_id(); + PartiallyUnwrappedPacket::new(packet, even_key.inner().as_ref()) + .map_err(|(_, err)| err) + .map(|p| p.with_key_rotation(even_rotation)) + } + } + } + async fn handle_received_packet_with_replay_detection( &mut self, now: Instant, @@ -219,10 +277,7 @@ impl ConnectionHandler { ) { // 1. derive and expand shared secret // also check the header integrity - let partially_unwrapped = match PartiallyUnwrappedPacket::new( - packet, - self.shared.sphinx_keys.private_key().as_ref(), - ) { + let partially_unwrapped = match self.try_partially_unwrap_packet(packet) { Ok(unwrapped) => unwrapped, Err(err) => { trace!("failed to process received mix packet: {err}"); @@ -277,17 +332,24 @@ impl ConnectionHandler { async fn handle_post_replay_detection_packets( &self, now: Instant, - packets: Vec, - replay_check_results: Vec, + packets: HashMap>, + replay_check_results: HashMap>, ) { - for (packet, replayed) in packets.into_iter().zip(replay_check_results) { - let unwrapped_packet = if replayed { - Err(PacketProcessingError::PacketReplay) - } else { - packet.finalise_unwrapping() + for (rotation_id, packets) in packets { + let Some(replay_checks) = replay_check_results.get(&rotation_id) else { + // this should never happen, but if we messed up, and it does, don't panic, just drop the packets + error!("inconsistent replay check result - no values for rotation {rotation_id}"); + continue; }; + for (packet, &replayed) in packets.into_iter().zip(replay_checks) { + let unwrapped_packet = if replayed { + Err(PacketProcessingError::PacketReplay) + } else { + packet.finalise_unwrapping() + }; - self.handle_unwrapped_packet(now, unwrapped_packet).await; + self.handle_unwrapped_packet(now, unwrapped_packet).await; + } } } @@ -340,6 +402,43 @@ impl ConnectionHandler { .await; } + fn try_full_unwrap_packet( + &self, + packet: FramedNymPacket, + ) -> Result { + // based on the received sphinx key rotation information, + // attempt to choose appropriate key for processing the packet + // NOTE: due to the function signatures, outfox packets will **only** attempt primary key + // if no rotation information is available (but that's fine given outfox is not really in use, + // and by the time we need it, the rotation info should be present) + match packet.header().key_rotation { + SphinxKeyRotation::Unknown => { + process_framed_packet(packet, self.shared.sphinx_keys.primary().inner().as_ref()) + } + SphinxKeyRotation::OddRotation => { + let Some(odd_key) = self.shared.sphinx_keys.odd() else { + return Err(PacketProcessingError::ExpiredKey); + }; + process_framed_packet(packet, odd_key.inner().as_ref()) + } + SphinxKeyRotation::EvenRotation => { + let Some(even_key) = self.shared.sphinx_keys.even() else { + return Err(PacketProcessingError::ExpiredKey); + }; + process_framed_packet(packet, even_key.inner().as_ref()) + } + } + } + + async fn handle_received_packet_with_no_replay_detection( + &mut self, + now: Instant, + packet: FramedNymPacket, + ) { + let unwrapped_packet = self.try_full_unwrap_packet(packet); + self.handle_unwrapped_packet(now, unwrapped_packet).await; + } + #[instrument(skip(self, packet), level = "debug")] async fn handle_received_nym_packet(&mut self, packet: FramedNymPacket) { let now = Instant::now(); @@ -352,9 +451,8 @@ impl ConnectionHandler { } else { // otherwise just skip that whole procedure and go straight to payload unwrapping // (assuming the basic framing is valid) - let unwrapped_packet = - process_framed_packet(packet, self.shared.sphinx_keys.private_key().as_ref()); - self.handle_unwrapped_packet(now, unwrapped_packet).await; + self.handle_received_packet_with_no_replay_detection(now, packet) + .await; }; } @@ -365,7 +463,29 @@ impl ConnectionHandler { remote = %self.remote_address ) )] - pub(crate) async fn handle_stream(&mut self) { + pub(crate) async fn handle_connection(&mut self, socket: TcpStream) { + let noise_stream = match upgrade_noise_responder(socket, &self.shared.noise_config).await { + Ok(noise_stream) => noise_stream, + Err(err) => { + error!( + "Failed to perform Noise handshake with {:?} - {err}", + self.remote_address + ); + return; + } + }; + debug!( + "Noise responder handshake completed for {:?}", + self.remote_address + ); + self.handle_stream(Framed::new(noise_stream, NymCodec)) + .await + } + + pub(crate) async fn handle_stream( + &mut self, + mut mixnet_connection: Framed, NymCodec>, + ) { loop { tokio::select! { biased; @@ -373,7 +493,7 @@ impl ConnectionHandler { trace!("connection handler: received shutdown"); break } - maybe_framed_nym_packet = self.mixnet_connection.next() => { + maybe_framed_nym_packet = mixnet_connection.next() => { match maybe_framed_nym_packet { Some(Ok(packet)) => self.handle_received_nym_packet(packet).await, Some(Err(err)) => { diff --git a/nym-node/src/node/mixnet/packet_forwarding/mod.rs b/nym-node/src/node/mixnet/packet_forwarding/mod.rs index 109398f90c..e397124ee0 100644 --- a/nym-node/src/node/mixnet/packet_forwarding/mod.rs +++ b/nym-node/src/node/mixnet/packet_forwarding/mod.rs @@ -58,32 +58,20 @@ impl PacketForwarder { C: SendWithoutResponse, F: RoutingFilter, { - let next_hop = packet.next_hop(); + let next_hop = packet.next_hop_address(); - let packet_type = packet.packet_type(); - let packet = packet.into_packet(); - - if let Err(err) = self - .mixnet_client - .send_without_response(next_hop, packet, packet_type) - { + if let Err(err) = self.mixnet_client.send_without_response(packet) { if err.kind() == io::ErrorKind::WouldBlock { // we only know for sure if we dropped a packet if our sending queue was full // in any other case the connection might still be re-established (or created for the first time) // and the packet might get sent, but we won't know about it - self.metrics - .mixnet - .egress_dropped_forward_packet(next_hop.into()) + self.metrics.mixnet.egress_dropped_forward_packet(next_hop) } else if err.kind() == io::ErrorKind::NotConnected { // let's give the benefit of the doubt and assume we manage to establish connection - self.metrics - .mixnet - .egress_sent_forward_packet(next_hop.into()) + self.metrics.mixnet.egress_sent_forward_packet(next_hop) } } else { - self.metrics - .mixnet - .egress_sent_forward_packet(next_hop.into()) + self.metrics.mixnet.egress_sent_forward_packet(next_hop) } } diff --git a/nym-node/src/node/mixnet/shared/final_hop.rs b/nym-node/src/node/mixnet/shared/final_hop.rs index d38fc99d9f..cb6bc0ea64 100644 --- a/nym-node/src/node/mixnet/shared/final_hop.rs +++ b/nym-node/src/node/mixnet/shared/final_hop.rs @@ -1,7 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_gateway::node::{ActiveClientsStore, GatewayStorage, GatewayStorageError}; +use nym_gateway::node::{ + ActiveClientsStore, GatewayStorage, GatewayStorageError, InboxGatewayStorage, +}; use nym_sphinx_types::DestinationAddressBytes; use tracing::debug; diff --git a/nym-node/src/node/mixnet/shared/mod.rs b/nym-node/src/node/mixnet/shared/mod.rs index 13f6922eaf..7dee952d44 100644 --- a/nym-node/src/node/mixnet/shared/mod.rs +++ b/nym-node/src/node/mixnet/shared/mod.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::node::mixnet::handler::ConnectionHandler; use crate::node::mixnet::SharedFinalHopData; -use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilter; -use nym_crypto::asymmetric::x25519; +use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters; use nym_gateway::node::GatewayStorageError; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward}; use nym_node_metrics::mixnet::PacketKind; use nym_node_metrics::NymNodeMetrics; +use nym_noise::config::NoiseConfig; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_framing::processing::{ MixPacketVersion, MixProcessingResult, MixProcessingResultData, PacketProcessingError, @@ -18,7 +19,6 @@ use nym_sphinx_types::DestinationAddressBytes; use nym_task::ShutdownToken; use std::io; use std::net::{IpAddr, SocketAddr}; -use std::sync::Arc; use std::time::Duration; use tokio::net::TcpStream; use tokio::task::JoinHandle; @@ -66,8 +66,8 @@ impl ProcessingConfig { // explicitly do NOT derive clone as we want to manually apply relevant suffixes to the task clients pub(crate) struct SharedData { pub(super) processing_config: ProcessingConfig, - pub(super) sphinx_keys: Arc, - pub(super) replay_protection_filter: ReplayProtectionBloomfilter, + pub(super) sphinx_keys: ActiveSphinxKeys, + pub(super) replay_protection_filter: ReplayProtectionBloomfilters, // used for FORWARD mix packets and FINAL ack packets pub(super) mixnet_forwarder: MixForwardingSender, @@ -75,6 +75,9 @@ pub(crate) struct SharedData { // data specific to the final hop (gateway) processing pub(super) final_hop: SharedFinalHopData, + // for establishing a Noise connection + pub(super) noise_config: NoiseConfig, + pub(super) metrics: NymNodeMetrics, pub(super) shutdown: ShutdownToken, } @@ -87,21 +90,24 @@ fn convert_to_metrics_version(processed: MixPacketVersion) -> PacketKind { } impl SharedData { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( processing_config: ProcessingConfig, - x25519_keys: Arc, - replay_protection_filter: ReplayProtectionBloomfilter, + sphinx_keys: ActiveSphinxKeys, + replay_protection_filter: ReplayProtectionBloomfilters, mixnet_forwarder: MixForwardingSender, final_hop: SharedFinalHopData, + noise_config: NoiseConfig, metrics: NymNodeMetrics, shutdown: ShutdownToken, ) -> Self { SharedData { processing_config, - sphinx_keys: x25519_keys, + sphinx_keys, replay_protection_filter, mixnet_forwarder, final_hop, + noise_config, metrics, shutdown, } @@ -164,8 +170,9 @@ impl SharedData { match accepted { Ok((socket, remote_addr)) => { debug!("accepted incoming mixnet connection from: {remote_addr}"); - let mut handler = ConnectionHandler::new(self, socket, remote_addr); - let join_handle = tokio::spawn(async move { handler.handle_stream().await }); + let mut handler = ConnectionHandler::new(self, remote_addr); + let join_handle = + tokio::spawn(async move { handler.handle_connection(socket).await }); self.log_connected_clients(); Some(join_handle) } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 5dc3e3302e..2fca246997 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -9,15 +9,17 @@ use crate::config::{ use crate::error::{EntryGatewayError, NymNodeError, ServiceProvidersError}; use crate::node::description::{load_node_description, save_node_description}; use crate::node::helpers::{ - load_ed25519_identity_keypair, load_key, load_x25519_noise_keypair, load_x25519_sphinx_keypair, + get_current_rotation_id, load_ed25519_identity_keypair, load_key, load_x25519_noise_keypair, store_ed25519_identity_keypair, store_key, store_keypair, store_x25519_noise_keypair, - store_x25519_sphinx_keypair, DisplayDetails, + DisplayDetails, }; use crate::node::http::api::api_requests; -use crate::node::http::helpers::sign_host_details; use crate::node::http::helpers::system_info::get_system_info; -use crate::node::http::state::AppState; +use crate::node::http::state::{AppState, StaticNodeInformation}; use crate::node::http::{HttpServerConfig, NymNodeHttpServer, NymNodeRouter}; +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use crate::node::key_rotation::controller::KeyRotationController; +use crate::node::key_rotation::manager::SphinxKeyManager; use crate::node::metrics::aggregator::MetricsAggregator; use crate::node::metrics::console_logger::ConsoleLogger; use crate::node::metrics::handler::client_sessions::GatewaySessionStatsHandler; @@ -28,10 +30,14 @@ use crate::node::metrics::handler::pending_egress_packets_updater::PendingEgress use crate::node::mixnet::packet_forwarding::PacketForwarder; use crate::node::mixnet::shared::ProcessingConfig; use crate::node::mixnet::SharedFinalHopData; -use crate::node::replay_protection::background_task::ReplayProtectionBackgroundTask; -use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilter; +use crate::node::nym_apis_client::NymApisClient; +use crate::node::replay_protection::background_task::ReplayProtectionDiskFlush; +use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters; +use crate::node::replay_protection::manager::ReplayProtectionBloomfiltersManager; use crate::node::routing_filter::{OpenFilter, RoutingFilter}; -use crate::node::shared_network::{CachedNetwork, CachedTopologyProvider, NetworkRefresher}; +use crate::node::shared_network::{ + CachedNetwork, CachedTopologyProvider, LocalGatewayNode, NetworkRefresher, +}; use nym_bin_common::bin_info; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder}; @@ -44,32 +50,33 @@ use nym_network_requester::{ use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; +use nym_noise::config::{NoiseConfig, NoiseNetworkView}; +use nym_noise_keys::VersionedNoiseKey; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{ShutdownManager, ShutdownToken, TaskClient}; -use nym_validator_client::client::NymApiClientExt; -use nym_validator_client::models::NodeRefreshBody; -use nym_validator_client::{NymApiClient, UserAgent}; +use nym_validator_client::UserAgent; use nym_verloc::measurements::SharedVerlocStats; use nym_verloc::{self, measurements::VerlocMeasurer}; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; use std::net::SocketAddr; +use std::ops::Deref; use std::path::Path; use std::sync::Arc; -use std::time::Duration; use tokio::sync::mpsc; -use tokio::time::timeout; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, info, trace}; use zeroize::Zeroizing; pub mod bonding_information; pub mod description; pub mod helpers; pub(crate) mod http; +pub(crate) mod key_rotation; pub(crate) mod metrics; pub(crate) mod mixnet; +mod nym_apis_client; pub(crate) mod replay_protection; mod routing_filter; mod shared_network; @@ -148,16 +155,16 @@ impl ServiceProvidersData { store_keypair( &ed25519_keys, - ed25519_paths, + &ed25519_paths, format!("{typ}-ed25519-identity"), )?; - store_keypair(&x25519_keys, x25519_paths, format!("{typ}-x25519-dh"))?; + store_keypair(&x25519_keys, &x25519_paths, format!("{typ}-x25519-dh"))?; store_key(&aes128ctr_key, ack_key_path, format!("{typ}-ack-key"))?; Ok(()) } - async fn initialise_client_gateway_storage( + pub(crate) async fn initialise_client_gateway_storage( storage_path: &Path, registration: &GatewayRegistration, ) -> Result<(), ServiceProvidersError> { @@ -324,7 +331,7 @@ impl WireguardData { let (inner, peer_rx) = WireguardGatewayData::new( config.clone().into(), Arc::new(load_x25519_wireguard_keypair( - config.storage_paths.x25519_wireguard_storage_paths(), + &config.storage_paths.x25519_wireguard_storage_paths(), )?), ); Ok(WireguardData { inner, peer_rx }) @@ -336,7 +343,7 @@ impl WireguardData { store_keypair( &x25519_keys, - config.storage_paths.x25519_wireguard_storage_paths(), + &config.storage_paths.x25519_wireguard_storage_paths(), "wg-x25519-dh", )?; @@ -372,7 +379,7 @@ pub(crate) struct NymNode { wireguard: Option, ed25519_identity_keys: Arc, - x25519_sphinx_keys: Arc, + sphinx_key_manager: Option, // to be used when noise is integrated #[allow(dead_code)] @@ -389,25 +396,26 @@ impl NymNode { // global initialisation let ed25519_identity_keys = ed25519::KeyPair::new(&mut rng); - let x25519_sphinx_keys = x25519::KeyPair::new(&mut rng); let x25519_noise_keys = x25519::KeyPair::new(&mut rng); + let current_rotation_id = + get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?; + let _ = SphinxKeyManager::initialise_new( + &mut rng, + current_rotation_id, + &config.storage_paths.keys.primary_x25519_sphinx_key_file, + &config.storage_paths.keys.secondary_x25519_sphinx_key_file, + )?; trace!("attempting to store ed25519 identity keypair"); store_ed25519_identity_keypair( &ed25519_identity_keys, - config.storage_paths.keys.ed25519_identity_storage_paths(), - )?; - - trace!("attempting to store x25519 sphinx keypair"); - store_x25519_sphinx_keypair( - &x25519_sphinx_keys, - config.storage_paths.keys.x25519_sphinx_storage_paths(), + &config.storage_paths.keys.ed25519_identity_storage_paths(), )?; trace!("attempting to store x25519 noise keypair"); store_x25519_noise_keypair( &x25519_noise_keys, - config.storage_paths.keys.x25519_noise_storage_paths(), + &config.storage_paths.keys.x25519_noise_storage_paths(), )?; trace!("creating description file"); @@ -434,16 +442,20 @@ impl NymNode { pub(crate) async fn new(config: Config) -> Result { let wireguard_data = WireguardData::new(&config.wireguard)?; + let current_rotation_id = + get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?; Ok(NymNode { ed25519_identity_keys: Arc::new(load_ed25519_identity_keypair( - config.storage_paths.keys.ed25519_identity_storage_paths(), + &config.storage_paths.keys.ed25519_identity_storage_paths(), )?), - x25519_sphinx_keys: Arc::new(load_x25519_sphinx_keypair( - config.storage_paths.keys.x25519_sphinx_storage_paths(), + sphinx_key_manager: Some(SphinxKeyManager::try_load_or_regenerate( + current_rotation_id, + &config.storage_paths.keys.primary_x25519_sphinx_key_file, + &config.storage_paths.keys.secondary_x25519_sphinx_key_file, )?), x25519_noise_keys: Arc::new(load_x25519_noise_keypair( - config.storage_paths.keys.x25519_noise_storage_paths(), + &config.storage_paths.keys.x25519_noise_storage_paths(), )?), description: load_node_description(&config.storage_paths.description)?, metrics: NymNodeMetrics::new(), @@ -510,11 +522,13 @@ impl NymNode { } pub(crate) fn display_details(&self) -> Result { + let sphinx_keys = self.sphinx_keys()?; Ok(DisplayDetails { current_modes: self.config.modes, description: self.description.clone(), ed25519_identity_key: self.ed25519_identity_key().to_base58_string(), - x25519_sphinx_key: self.x25519_sphinx_key().to_base58_string(), + x25519_primary_sphinx_key: sphinx_keys.keys.primary().deref().into(), + x25519_secondary_sphinx_key: sphinx_keys.keys.secondary().map(|g| g.deref().into()), x25519_noise_key: self.x25519_noise_key().to_base58_string(), x25519_wireguard_key: self.x25519_wireguard_key()?.to_base58_string(), exit_network_requester_address: self.exit_network_requester_address().to_string(), @@ -531,22 +545,19 @@ impl NymNode { self.ed25519_identity_keys.public_key() } - pub(crate) fn x25519_sphinx_key(&self) -> &x25519::PublicKey { - self.x25519_sphinx_keys.public_key() - } - - pub(crate) fn x25519_sphinx_keys(&self) -> Arc { - self.x25519_sphinx_keys.clone() - } - pub(crate) fn x25519_noise_key(&self) -> &x25519::PublicKey { self.x25519_noise_keys.public_key() } + #[track_caller] + pub(crate) fn active_sphinx_keys(&self) -> Result { + Ok(self.sphinx_keys()?.keys.clone()) + } + async fn build_network_refresher(&self) -> Result { NetworkRefresher::initialise_new( self.config.debug.testnet, - self.user_agent(), + Self::user_agent(), self.config.mixnet.nym_api_urls.clone(), self.config.debug.topology_cache_ttl, self.config.debug.routing_nodes_check_interval, @@ -555,7 +566,7 @@ impl NymNode { .await } - fn as_gateway_topology_node(&self) -> Result { + fn as_gateway_topology_node(&self) -> Result { let ip_addresses = self.config.host.public_ips.clone(); let Some(ip) = ip_addresses.first() else { @@ -575,21 +586,15 @@ impl NymNode { .announce_ws_port .unwrap_or(self.config.gateway_tasks.ws_bind_address.port()); - Ok(nym_topology::RoutingNode { - node_id: u32::MAX, + Ok(LocalGatewayNode { + active_sphinx_keys: self.active_sphinx_keys()?.clone(), mix_host, - entry: Some(nym_topology::EntryDetails { + identity_key: *self.ed25519_identity_key(), + entry: nym_topology::EntryDetails { ip_addresses, clients_ws_port, hostname: self.config.host.hostname.clone(), clients_wss_port: self.config.gateway_tasks.announce_wss_port, - }), - sphinx_key: *self.x25519_sphinx_key(), - identity_key: *self.ed25519_identity_key(), - supported_roles: nym_topology::SupportedRoles { - mixnode: false, - mixnet_entry: true, - mixnet_exit: true, }, }) } @@ -600,7 +605,8 @@ impl NymNode { metrics_sender: MetricEventsSender, active_clients_store: ActiveClientsStore, mix_packet_sender: MixForwardingSender, - task_client: TaskClient, + legacy_task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Result<(), NymNodeError> { let config = gateway_tasks_config(&self.config); @@ -618,7 +624,8 @@ impl NymNode { metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), - task_client, + legacy_task_client, + shutdown_token, ); // if we're running in entry mode, start the websocket @@ -697,13 +704,6 @@ impl NymNode { } pub(crate) async fn build_http_server(&self) -> Result { - let host_details = sign_host_details( - &self.config, - self.x25519_sphinx_keys.public_key(), - self.x25519_noise_keys.public_key(), - &self.ed25519_identity_keys, - )?; - let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { location: self.config.host.location, announce_ports: AnnouncePorts { @@ -718,8 +718,11 @@ impl NymNode { // entry gateway info let wireguard = if self.config.wireguard.enabled { + #[allow(deprecated)] Some(api_requests::v1::gateway::models::Wireguard { - port: self.config.wireguard.announced_port, + port: self.config.wireguard.announced_tunnel_port, + tunnel_port: self.config.wireguard.announced_tunnel_port, + metadata_port: self.config.wireguard.announced_metadata_port, public_key: self.x25519_wireguard_key()?.to_string(), }) } else { @@ -773,7 +776,7 @@ impl NymNode { policy: None, }; - let mut config = HttpServerConfig::new(host_details) + let mut config = HttpServerConfig::new() .with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref()) .with_mixnode_details(mixnode_details) .with_gateway_details(gateway_details) @@ -804,7 +807,23 @@ impl NymNode { config.api.v1_config.node.roles.ip_packet_router_enabled = true; } + let x25519_versioned_noise_key = if self.config.mixnet.debug.unsafe_disable_noise { + None + } else { + Some(VersionedNoiseKey { + supported_version: nym_noise::LATEST_NOISE_VERSION, + x25519_pubkey: *self.x25519_noise_keys.public_key(), + }) + }; + let app_state = AppState::new( + StaticNodeInformation { + ed25519_identity_keys: self.ed25519_identity_keys.clone(), + x25519_versioned_noise_key, + ip_addresses: self.config.host.public_ips.clone(), + hostname: self.config.host.hostname.clone(), + }, + self.active_sphinx_keys()?.clone(), self.metrics.clone(), self.verloc_stats.clone(), self.config.http.node_load_cache_ttl, @@ -815,54 +834,20 @@ impl NymNode { .await?) } - fn user_agent(&self) -> UserAgent { + fn user_agent() -> UserAgent { bin_info!().into() } - async fn try_refresh_remote_nym_api_cache(&self) { - info!("attempting to request described cache refresh from nym-api..."); - if self.config.mixnet.nym_api_urls.is_empty() { - warn!("no nym-api urls available"); - return; - } + async fn try_refresh_remote_nym_api_cache( + &self, + client: &NymApisClient, + ) -> Result<(), NymNodeError> { + info!("attempting to request described cache refresh from nym-api(s)..."); - for nym_api_url in &self.config.mixnet.nym_api_urls { - info!("trying {nym_api_url}..."); - - let nym_api = - match nym_http_api_client::ClientBuilder::new_with_url(nym_api_url.clone()) - .no_hickory_dns() - .with_user_agent(self.user_agent()) - .build::<&str>() - { - Ok(b) => b, - Err(e) => { - warn!("failed to build http client for \"{nym_api_url}\": {e}",); - continue; - } - }; - - let client = NymApiClient { nym_api }; - - // make new request every time in case previous one takes longer and invalidates the signature - let request = NodeRefreshBody::new(self.ed25519_identity_keys.private_key()); - match timeout( - Duration::from_secs(10), - client.nym_api.force_refresh_describe_cache(&request), - ) - .await - { - Ok(Ok(_)) => { - info!("managed to refresh own self-described data cache") - } - Ok(Err(request_failure)) => { - warn!("failed to resolve the refresh request: {request_failure}") - } - Err(_timeout) => { - warn!("timed out while attempting to resolve the request. the cache might be stale") - } - }; - } + client + .broadcast_force_refresh(self.ed25519_identity_keys.private_key()) + .await; + Ok(()) } pub(crate) fn start_verloc_measurements(&self) { @@ -871,7 +856,7 @@ impl NymNode { self.config.verloc.bind_address ); - let mut base_agent = self.user_agent(); + let mut base_agent = Self::user_agent(); base_agent.application = format!("{}-verloc", base_agent.application); let config = nym_verloc::measurements::ConfigBuilder::new( self.config.mixnet.nym_api_urls.clone(), @@ -964,7 +949,6 @@ impl NymNode { // >>>> END: register all relevant handlers // console logger to preserve old mixnode functionalities - // if self.config.logging.debug.log_to_console { if self.config.metrics.debug.log_stats_to_console { ConsoleLogger::new( self.config.metrics.debug.console_logging_update_interval, @@ -984,31 +968,80 @@ impl NymNode { pub(crate) async fn setup_replay_detection( &self, - ) -> Result { + ) -> Result { if self.config.mixnet.replay_protection.debug.unsafe_disabled { - return Ok(ReplayProtectionBloomfilter::new_disabled()); + return Ok(ReplayProtectionBloomfiltersManager::new_disabled( + self.metrics.clone(), + )); } // create the background task for the bloomfilter // to reset it and flush it to disk - let mut replay_detection_background = ReplayProtectionBackgroundTask::new( + let sphinx_keys = self.sphinx_keys()?; + let mut replay_detection_background = ReplayProtectionDiskFlush::new( &self.config, + sphinx_keys.keys.primary_key_rotation_id(), + sphinx_keys.keys.secondary_key_rotation_id(), self.metrics.clone(), self.shutdown_manager - .clone_token("replay-detection-background"), + .clone_token("replay-detection-background-flush"), ) .await?; - let replay_protection_bloomfilter = replay_detection_background.global_bloomfilter(); + let bloomfilters_manager = replay_detection_background.bloomfilters_manager(); self.shutdown_manager .spawn(async move { replay_detection_background.run().await }); - Ok(replay_protection_bloomfilter) + Ok(bloomfilters_manager) + } + + // I'm assuming this will be needed in other places, so it's explicitly extracted + fn setup_nym_apis_client(&self) -> Result { + NymApisClient::new( + &self.config.mixnet.nym_api_urls, + self.shutdown_manager.clone_token("nym-apis-client"), + ) + } + + #[track_caller] + fn sphinx_keys(&self) -> Result<&SphinxKeyManager, NymNodeError> { + self.sphinx_key_manager + .as_ref() + .ok_or(NymNodeError::ConsumedSphinxKeys) + } + + fn take_managed_sphinx_keys(&mut self) -> Result { + self.sphinx_key_manager + .take() + .ok_or(NymNodeError::ConsumedSphinxKeys) + } + + pub(crate) async fn setup_key_rotation( + &mut self, + nym_apis_client: NymApisClient, + replay_protection_manager: ReplayProtectionBloomfiltersManager, + ) -> Result<(), NymNodeError> { + let managed_keys = self.take_managed_sphinx_keys()?; + let rotation_state = nym_apis_client.get_key_rotation_info().await?; + + let rotation_controller = KeyRotationController::new( + &self.config, + rotation_state.into(), + nym_apis_client, + replay_protection_manager, + managed_keys, + self.shutdown_manager.clone_token("key-rotation-controller"), + ); + + rotation_controller.start(); + Ok(()) } pub(crate) async fn start_mixnet_listener( &self, active_clients_store: &ActiveClientsStore, + replay_protection_bloomfilter: ReplayProtectionBloomfilters, routing_filter: F, + noise_config: NoiseConfig, shutdown: ShutdownToken, ) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError> where @@ -1029,16 +1062,17 @@ impl NymNode { self.config.mixnet.debug.packet_forwarding_maximum_backoff, self.config.mixnet.debug.initial_connection_timeout, self.config.mixnet.debug.maximum_connection_buffer_size, + self.config.mixnet.debug.use_legacy_packet_encoding, ); let mixnet_client = nym_mixnet_client::Client::new( mixnet_client_config, + noise_config.clone(), self.metrics .network .active_egress_mixnet_connections_counter(), ); let active_connections = mixnet_client.active_connections(); - let replay_protection_bloomfilter = self.setup_replay_detection().await?; let mut packet_forwarder = PacketForwarder::new( mixnet_client, routing_filter, @@ -1055,10 +1089,11 @@ impl NymNode { let shared = mixnet::SharedData::new( processing_config, - self.x25519_sphinx_keys.clone(), + self.active_sphinx_keys()?, replay_protection_bloomfilter, mix_packet_sender.clone(), final_hop_data, + noise_config, self.metrics.clone(), shutdown, ); @@ -1068,20 +1103,29 @@ impl NymNode { } pub(crate) async fn run_minimal_mixnet_processing(self) -> Result<(), NymNodeError> { + let noise_config = nym_noise::config::NoiseConfig::new( + self.x25519_noise_keys.clone(), + NoiseNetworkView::new_empty(), + self.config.mixnet.debug.initial_connection_timeout, + ) + .with_unsafe_disabled(true); + self.start_mixnet_listener( &ActiveClientsStore::new(), + ReplayProtectionBloomfilters::new_disabled(), OpenFilter, + noise_config, self.shutdown_manager.clone_token("mixnet-traffic"), ) .await?; self.shutdown_manager.close(); - self.shutdown_manager.wait_for_shutdown_signal().await; + self.shutdown_manager.run_until_shutdown().await; Ok(()) } - pub(crate) async fn run(mut self) -> Result<(), NymNodeError> { + async fn start_nym_node_tasks(mut self) -> Result { info!("starting Nym Node {} with the following modes: mixnode: {}, entry: {}, exit: {}, wireguard: {}", self.ed25519_identity_key(), self.config.modes.mixnode, @@ -1104,16 +1148,30 @@ impl NymNode { } }); - self.try_refresh_remote_nym_api_cache().await; + let nym_apis_client = self.setup_nym_apis_client()?; + + self.try_refresh_remote_nym_api_cache(&nym_apis_client) + .await?; self.start_verloc_measurements(); let network_refresher = self.build_network_refresher().await?; let active_clients_store = ActiveClientsStore::new(); + let bloomfilters_manager = self.setup_replay_detection().await?; + + let noise_config = nym_noise::config::NoiseConfig::new( + self.x25519_noise_keys.clone(), + network_refresher.noise_view(), + self.config.mixnet.debug.initial_connection_timeout, + ) + .with_unsafe_disabled(self.config.mixnet.debug.unsafe_disable_noise); + let (mix_packet_sender, active_egress_mixnet_connections) = self .start_mixnet_listener( &active_clients_store, + bloomfilters_manager.bloomfilters(), network_refresher.routing_filter(), + noise_config, self.shutdown_manager.clone_token("mixnet-traffic"), ) .await?; @@ -1130,13 +1188,35 @@ impl NymNode { active_clients_store, mix_packet_sender, self.shutdown_manager.subscribe_legacy("gateway-tasks"), + self.shutdown_manager.child_token("gateway-tasks"), ) .await?; - network_refresher.start(); + self.setup_key_rotation(nym_apis_client, bloomfilters_manager) + .await?; + network_refresher.start(); self.shutdown_manager.close(); - self.shutdown_manager.wait_for_shutdown_signal().await; + + Ok(self.shutdown_manager) + } + + pub(crate) async fn run(mut self) -> Result<(), NymNodeError> { + let mut shutdown_signals = self.shutdown_manager.detach_shutdown_signals(); + + // listen for shutdown signal in case we received it when attempting to spawn all the tasks + tokio::select! { + _ = shutdown_signals.wait_for_signal() => { + info!("received shutdown signal during setup - exiting"); + // ideally we'd also do some cleanup here, but currently there's no easy way to access the handles + return Ok(()) + } + startup_result = self.start_nym_node_tasks() => { + let mut shutdown_manager = startup_result?; + shutdown_manager.replace_shutdown_signals(shutdown_signals); + shutdown_manager.run_until_shutdown().await; + } + } Ok(()) } diff --git a/nym-node/src/node/nym_apis_client.rs b/nym-node/src/node/nym_apis_client.rs new file mode 100644 index 0000000000..08ef68aff7 --- /dev/null +++ b/nym-node/src/node/nym_apis_client.rs @@ -0,0 +1,223 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymNodeError; +use crate::node::NymNode; +use futures::{stream, StreamExt}; +use nym_crypto::asymmetric::ed25519; +use nym_http_api_client::Client; +use nym_task::ShutdownToken; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::{KeyRotationInfoResponse, NodeRefreshBody}; +use nym_validator_client::nym_api::error::NymAPIError; +use nym_validator_client::NymApiClient; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::RwLock; +use tokio::time::sleep; +use tracing::{debug, warn}; +use url::Url; + +#[derive(Clone)] +pub struct NymApisClient { + inner: Arc>, +} + +struct InnerClient { + // NOTE: this was implemented before the internal http client supported multiple URLs + active_client: NymApiClient, + available_urls: Vec, + shutdown_token: ShutdownToken, + currently_used_api: usize, +} + +impl NymApisClient { + pub(crate) fn new( + nym_apis: &[Url], + shutdown_token: ShutdownToken, + ) -> Result { + if nym_apis.is_empty() { + return Err(NymNodeError::NoNymApiUrls); + } + + let mut urls = nym_apis.to_vec(); + urls.shuffle(&mut thread_rng()); + + let active_client = nym_http_api_client::Client::builder(urls[0].clone())? + .no_hickory_dns() + .with_user_agent(NymNode::user_agent()) + .with_timeout(Duration::from_secs(5)) + .build()?; + + Ok(NymApisClient { + inner: Arc::new(RwLock::new(InnerClient { + active_client: NymApiClient::from(active_client), + available_urls: urls, + shutdown_token, + currently_used_api: 0, + })), + }) + } + + // async fn use_next_endpoint(&self) { + // let mut guard = self.inner.write().await; + // if guard.available_urls.len() == 1 { + // return; + // } + // + // let next_index = (guard.currently_used_api + 1) % guard.available_urls.len(); + // let next = guard.available_urls[next_index].clone(); + // guard.currently_used_api = next_index; + // guard.active_client.change_nym_api(next) + // } + + pub(crate) async fn query_exhaustively( + &self, + req: R, + timeout_duration: Duration, + ) -> Result + where + R: AsyncFn(Client) -> Result, + { + let guard = self.inner.read().await; + let (res, last_working_endpoint) = guard.query_exhaustively(req, timeout_duration).await?; + + // if we had to use a different api, update our starting point for the future calls + if guard.currently_used_api != last_working_endpoint { + drop(guard); + let mut guard = self.inner.write().await; + let next_url = guard.available_urls[last_working_endpoint].clone(); + guard.currently_used_api = last_working_endpoint; + guard.active_client.change_nym_api(next_url); + } + + Ok(res) + } + + pub(crate) async fn broadcast_force_refresh(&self, private_key: &ed25519::PrivateKey) { + self.inner + .read() + .await + .broadcast_force_refresh(private_key) + .await; + } + + pub(crate) async fn get_key_rotation_info( + &self, + ) -> Result { + self.query_exhaustively( + async |c| c.get_key_rotation_info().await, + Duration::from_secs(5), + ) + .await + } +} + +impl InnerClient { + // currently there are no cases without json body, but for those we'd just need to slightly adjust the signature + async fn broadcast(&self, request_body: &B, req: R, timeout_duration: Duration) + where + R: AsyncFn(Client, &B) -> Result<(), NymAPIError>, + { + let broadcast_fut = + stream::iter(self.available_urls.clone()).for_each_concurrent(None, |url| { + let nym_api = self + .active_client + .nym_api + .clone_with_new_url(url.clone().into()); + let req_fut = req(nym_api, request_body); + async move { + if let Err(err) = req_fut.await { + warn!("broadcast request to {url} failed: {err}") + } + } + }); + + let timeout_fut = sleep(timeout_duration); + + tokio::select! { + _ = broadcast_fut => { + debug!("managed to broadcast data to all nym apis") + } + _ = timeout_fut => { + warn!("timed out while attempting to broadcast data to known nym apis") + + } + _ = self.shutdown_token.cancelled() => { + debug!("received shutdown while attempting to broadcast data to known nym apis") + } + } + } + + async fn query_exhaustively( + &self, + req: R, + timeout_duration: Duration, + ) -> Result<(T, usize), NymNodeError> + where + R: AsyncFn(Client) -> Result, + { + let last_working = self.currently_used_api; + + // start from the last working api and progress from there + // also, note this is DESIGNED to query sequentially (but exhaustively) + // and not to try to send queries to ALL apis at once + // and check which resolves first + for (idx, url) in self + .available_urls + .iter() + .enumerate() + .skip(last_working) + .chain(self.available_urls.iter().enumerate().take(last_working)) + { + let nym_api = self + .active_client + .nym_api + .clone_with_new_url(url.clone().into()); + + let timeout_fut = sleep(timeout_duration); + let query_fut = req(nym_api); + + tokio::select! { + res = query_fut => { + debug!("managed to broadcast data to all nym apis"); + match res { + Ok(res) => return Ok((res, idx)), + Err(err) => { + warn!("failed to resolve query for {url}: {err}"); + } + } + } + _ = timeout_fut => { + warn!("timed out while attempting to query {url}") + + } + _ = self.shutdown_token.cancelled() => { + debug!("received shutdown while attempting to query {url}"); + return Err(NymNodeError::ShutdownReceived) + } + } + } + + Err(NymNodeError::NymApisExhausted) + } + + async fn broadcast_force_refresh(&self, private_key: &ed25519::PrivateKey) { + let request = NodeRefreshBody::new(private_key); + + self.broadcast( + &request, + async |client, request| client.force_refresh_describe_cache(request).await, + Duration::from_secs(10), + ) + .await; + } +} + +impl AsRef for InnerClient { + fn as_ref(&self) -> &NymApiClient { + &self.active_client + } +} diff --git a/nym-node/src/node/replay_protection/background_task.rs b/nym-node/src/node/replay_protection/background_task.rs index 392c79cd68..68092e0208 100644 --- a/nym-node/src/node/replay_protection/background_task.rs +++ b/nym-node/src/node/replay_protection/background_task.rs @@ -1,204 +1,233 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::persistence::{ + DEFAULT_RD_BLOOMFILTER_FILE_EXT, DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT, +}; use crate::config::Config; use crate::error::NymNodeError; -use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilter; +use crate::node::replay_protection::bloomfilter::RotationFilter; +use crate::node::replay_protection::helpers::parse_rotation_id_from_filename; use crate::node::replay_protection::items_in_bloomfilter; -use human_repr::HumanCount; +use crate::node::replay_protection::manager::ReplayProtectionBloomfiltersManager; +use human_repr::HumanDuration; use nym_node_metrics::NymNodeMetrics; use nym_task::ShutdownToken; -use std::cmp::max; +use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::time::Duration; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; use tokio::time::{interval, Instant}; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; -struct LastResetData { - packets_received_at_last_reset: usize, - reset_time: Instant, -} - -struct ReplayProtectionBackgroundTaskConfig { - current_bloomfilter_path: PathBuf, - current_bloomfilter_temp_flush_path: PathBuf, - - false_positive_rate: f64, - filter_reset_rate: Duration, +// background task responsible for periodically flushing the bloomfilters to disk +pub struct ReplayProtectionDiskFlush { + bloomfilters_directory: PathBuf, disk_flushing_rate: Duration, - bloomfilter_size_multiplier: f64, - minimum_bloomfilter_packets_per_second: usize, + + filters_manager: ReplayProtectionBloomfiltersManager, + shutdown_token: ShutdownToken, } -impl From<&Config> for ReplayProtectionBackgroundTaskConfig { - fn from(config: &Config) -> Self { - ReplayProtectionBackgroundTaskConfig { - current_bloomfilter_path: config +impl ReplayProtectionDiskFlush { + pub(crate) async fn new( + config: &Config, + primary_key_rotation_id: u32, + secondary_key_rotation_id: Option, + metrics: NymNodeMetrics, + shutdown_token: ShutdownToken, + ) -> Result { + let bloomfilters_directory = config + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory + .clone(); + + let dir_read_err = |source| NymNodeError::BloomfilterIoFailure { + source, + path: bloomfilters_directory.clone(), + }; + + if !bloomfilters_directory.exists() { + fs::create_dir_all(&bloomfilters_directory).map_err(dir_read_err)?; + } + + let available_filters_dir = fs::read_dir(&bloomfilters_directory).map_err(dir_read_err)?; + + // figure out what bloomfilters we have available on disk + let mut filter_files = HashMap::new(); + for entry in available_filters_dir.into_iter() { + let entry = entry.map_err(dir_read_err)?; + let path = entry.path(); + + let Some(rotation) = entry + .file_name() + .to_str() + .and_then(parse_rotation_id_from_filename) + else { + warn!("invalid bloomfilter file at '{}'", path.display()); + continue; + }; + + // if any bloomfilter has the temp extension, we can't trust its data as it hasn't completed the flush + if let Some(ext) = entry.path().extension() { + if ext == DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT { + error!( + "bloomfilter {rotation} didn't get successfully flushed to disk and its data got corrupted" + ); + fs::remove_file(&path) + .map_err(|source| NymNodeError::BloomfilterIoFailure { source, path })?; + continue; + } + } + + filter_files.insert(rotation, path); + } + + let rebuild_items_in_filter = items_in_bloomfilter( + Duration::from_secs(25 * 60 * 60), + config .mixnet .replay_protection - .storage_paths - .current_bloomfilter_filepath(), - current_bloomfilter_temp_flush_path: config - .mixnet - .replay_protection - .storage_paths - .current_bloomfilter_being_flushed_filepath(), - false_positive_rate: config.mixnet.replay_protection.debug.false_positive_rate, - filter_reset_rate: config.mixnet.replay_protection.debug.bloomfilter_reset_rate, + .debug + .initial_expected_packets_per_second, + ); + let fp_r = config.mixnet.replay_protection.debug.false_positive_rate; + + // if filters do not exist on disk, we must make new ones + let primary_bloomfilter = match filter_files.get(&primary_key_rotation_id) { + Some(primary_path) => RotationFilter::load(primary_path)?, + None => { + info!("no stored bloomfilter for rotation {primary_key_rotation_id}"); + RotationFilter::new(rebuild_items_in_filter, fp_r, 0, primary_key_rotation_id)? + } + }; + + let secondary_bloomfilter = + if let Some(secondary_key_rotation_id) = secondary_key_rotation_id { + match filter_files.get(&secondary_key_rotation_id) { + Some(secondary_path) => Some(RotationFilter::load(secondary_path)?), + None => { + info!("no stored bloomfilter for rotation {secondary_key_rotation_id}"); + Some(RotationFilter::new( + rebuild_items_in_filter, + fp_r, + 0, + secondary_key_rotation_id, + )?) + } + } + } else { + None + }; + + Ok(ReplayProtectionDiskFlush { + bloomfilters_directory, disk_flushing_rate: config .mixnet .replay_protection .debug .bloomfilter_disk_flushing_rate, - bloomfilter_size_multiplier: config - .mixnet - .replay_protection - .debug - .bloomfilter_size_multiplier, - minimum_bloomfilter_packets_per_second: config - .mixnet - .replay_protection - .debug - .bloomfilter_minimum_packets_per_second_size, - } - } -} - -// background task responsible for periodically flushing the bloomfilter to disk -// as well as clearing it up on the specified timer -// (in the future this will be enforced by key rotation) -pub struct ReplayProtectionBackgroundTask { - config: ReplayProtectionBackgroundTaskConfig, - last_reset: LastResetData, - - filter: ReplayProtectionBloomfilter, - metrics: NymNodeMetrics, - shutdown_token: ShutdownToken, -} - -impl ReplayProtectionBackgroundTask { - pub(crate) async fn new( - config: &Config, - metrics: NymNodeMetrics, - shutdown_token: ShutdownToken, - ) -> Result { - let task_config: ReplayProtectionBackgroundTaskConfig = config.into(); - - if task_config.current_bloomfilter_temp_flush_path.exists() { - error!( - "bloomfilter didn't get successfully flushed to disk and its data got corrupted" - ); - fs::remove_file(&task_config.current_bloomfilter_temp_flush_path).map_err(|source| { - NymNodeError::BloomfilterIoFailure { - source, - path: task_config.current_bloomfilter_temp_flush_path.clone(), - } - })? - } - - // if there's nothing on disk, we must create a new filter - let bloomfilter = if task_config.current_bloomfilter_path.exists() { - ReplayProtectionBloomfilter::load(&task_config.current_bloomfilter_path).await? - } else { - let bf_items = items_in_bloomfilter( - task_config.filter_reset_rate, - config - .mixnet - .replay_protection - .debug - .initial_expected_packets_per_second, - ); - - ReplayProtectionBloomfilter::new_empty(bf_items, task_config.false_positive_rate)? - }; - - Ok(ReplayProtectionBackgroundTask { - config: task_config, - last_reset: LastResetData { - packets_received_at_last_reset: 0, - reset_time: Instant::now(), - }, - filter: bloomfilter, - metrics, + filters_manager: ReplayProtectionBloomfiltersManager::new( + config, + primary_bloomfilter, + secondary_bloomfilter, + metrics, + ), shutdown_token, }) } - pub(crate) fn global_bloomfilter(&self) -> ReplayProtectionBloomfilter { - self.filter.clone() + fn bloomfilter_filepath(&self, rotation_id: u32) -> PathBuf { + self.bloomfilters_directory + .join(format!("rot-{rotation_id}")) + .with_extension(DEFAULT_RD_BLOOMFILTER_FILE_EXT) } - async fn flush_to_disk(&self) -> Result<(), NymNodeError> { - if let Some(temp_parent) = self.config.current_bloomfilter_temp_flush_path.parent() { - fs::create_dir_all(temp_parent).map_err(|source| { - NymNodeError::BloomfilterIoFailure { - source, - path: temp_parent.to_path_buf(), - } - })? - } - if let Some(current_parent) = self.config.current_bloomfilter_temp_flush_path.parent() { - fs::create_dir_all(current_parent).map_err(|source| { - NymNodeError::BloomfilterIoFailure { - source, - path: current_parent.to_path_buf(), - } - })? - } + fn current_bloomfilter_being_flushed_filepath(&self, rotation_id: u32) -> PathBuf { + self.bloomfilters_directory + .join(format!("rot-{rotation_id}")) + .with_extension(DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT) + } + pub(crate) fn bloomfilters_manager(&self) -> ReplayProtectionBloomfiltersManager { + self.filters_manager.clone() + } + + async fn flush(&self, data: Vec, rotation_id: u32) -> Result<(), NymNodeError> { // because it takes a while to actually write the file to disk, // we first write bytes to temporary location, // and then we move it to the correct path - let temp = &self.config.current_bloomfilter_temp_flush_path; - self.filter.flush_to_disk(temp).await?; - fs::rename(temp, &self.config.current_bloomfilter_path).map_err(|source| { + let temp_path = self.current_bloomfilter_being_flushed_filepath(rotation_id); + let final_path = self.bloomfilter_filepath(rotation_id); + debug!("flushing replay protection bloomfilter {rotation_id} to disk..."); + let start = Instant::now(); + + let mut file = File::create(&temp_path).await.map_err(|source| { NymNodeError::BloomfilterIoFailure { source, - path: self.config.current_bloomfilter_path.clone(), + path: temp_path.clone(), } })?; + + file.write_all(&data) + .await + .map_err(|source| NymNodeError::BloomfilterIoFailure { + source, + path: temp_path.to_path_buf(), + })?; + + fs::rename(temp_path, &final_path).map_err(|source| { + NymNodeError::BloomfilterIoFailure { + source, + path: final_path, + } + })?; + + let elapsed = start.elapsed(); + + info!( + "flushed replay protection bloomfilter {rotation_id} to disk. it took: {}", + elapsed.human_duration() + ); + Ok(()) } - fn reset_bloomfilter(&mut self) -> Result<(), NymNodeError> { - // 1. determine parameters for new bloomfilter - let received = self.metrics.mixnet.ingress.forward_hop_packets_received() - + self.metrics.mixnet.ingress.final_hop_packets_received(); + // average HDD has the write speed of ~80MB/s so a 2GB bloomfilter would take almost 30s to write... + // and this function is explicitly async and using tokio's async operations, because otherwise + // we'd have to go through the whole hassle of using spawn_blocking and awaiting that one instead + async fn flush_primary(&self) -> Result<(), NymNodeError> { + let (bytes, id) = self.filters_manager.primary_bytes_and_id()?; + self.flush(bytes, id).await + } - let time_delta = self.last_reset.reset_time.elapsed(); - let received_since_last_reset = received - self.last_reset.packets_received_at_last_reset; - let received_per_second = - (received_since_last_reset as f64 / time_delta.as_secs_f64()).round() as usize; + async fn flush_secondary(&self) -> Result<(), NymNodeError> { + let Some((bytes, id)) = self.filters_manager.secondary_bytes_and_id()? else { + return Ok(()); + }; + self.flush(bytes, id).await + } - let bf_received = max( - received_per_second, - self.config.minimum_bloomfilter_packets_per_second, - ); - let items_in_new_filter = items_in_bloomfilter(self.config.filter_reset_rate, bf_received); - let adjusted = - (items_in_new_filter as f64 * self.config.bloomfilter_size_multiplier).round() as usize; + async fn flush_filters_to_disk(&self) -> Result<(), NymNodeError> { + if let Some(parent) = self.bloomfilters_directory.parent() { + fs::create_dir_all(parent).map_err(|source| NymNodeError::BloomfilterIoFailure { + source, + path: parent.to_path_buf(), + })? + } - info!( - "resetting bloom filter. new expected number of packets: {} that preserve fp rate of {}", - adjusted.human_count_bare(), - self.config.false_positive_rate - ); + self.flush_primary().await?; + self.flush_secondary().await?; - // 2. update the filter - self.last_reset.reset_time = Instant::now(); - self.last_reset.packets_received_at_last_reset = received_since_last_reset; - - // if this fails with the mutex getting poisoned, the next received packet is going to cause - // a shutdown, so we don't have to propagate it here - self.filter.reset(adjusted, self.config.false_positive_rate) + Ok(()) } pub(crate) async fn run(&mut self) { - let mut reset_timer = interval(self.config.filter_reset_rate); - reset_timer.reset(); - - let mut flush_timer = interval(self.config.disk_flushing_rate); + let mut flush_timer = interval(self.disk_flushing_rate); flush_timer.reset(); loop { @@ -208,13 +237,8 @@ impl ReplayProtectionBackgroundTask { trace!("ReplayProtectionBackgroundTask: Received shutdown"); break; } - _ = reset_timer.tick() => { - if let Err(err) = self.reset_bloomfilter() { - error!("failed to reset the bloomfilter: {err}") - } - } _ = flush_timer.tick() => { - if let Err(err) = self.flush_to_disk().await { + if let Err(err) = self.flush_filters_to_disk().await { error!("failed to flush bloomfilter to disk: {err}") } } @@ -222,8 +246,8 @@ impl ReplayProtectionBackgroundTask { } info!("SHUTDOWN: flushing replay detection bloomfilter to disk. this might take a while. DO NOT INTERRUPT THIS PROCESS"); - if let Err(err) = self.flush_to_disk().await { - warn!("failed to flush replay detection bloom filter on shutdown: {err}"); + if let Err(err) = self.flush_filters_to_disk().await { + warn!("failed to flush replay detection bloom filters on shutdown: {err}"); } } } diff --git a/nym-node/src/node/replay_protection/bloomfilter.rs b/nym-node/src/node/replay_protection/bloomfilter.rs index 087b7b5444..7ae35875cb 100644 --- a/nym-node/src/node/replay_protection/bloomfilter.rs +++ b/nym-node/src/node/replay_protection/bloomfilter.rs @@ -3,43 +3,128 @@ use crate::error::NymNodeError; use bloomfilter::Bloom; -use human_repr::HumanDuration; use nym_sphinx_types::REPLAY_TAG_SIZE; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use std::mem; use std::path::Path; -use std::sync::{Arc, PoisonError, TryLockError}; -use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::time::Instant; -use tracing::{debug, info}; +use std::sync::{Arc, Mutex, PoisonError, TryLockError}; +use time::OffsetDateTime; +use tracing::{error, info, warn}; + +// auxiliary data associated with the bloomfilter to get some statistics from the time of its creation +// this is needed in order to more accurately resize it upon reset + +#[derive(Copy, Clone)] +pub(crate) struct ReplayProtectionBloomfilterMetadata { + // used in the unlikely case of epoch durations being changed. it doesn't really cost us anything + // to include it, so might as well + pub(crate) creation_time: OffsetDateTime, + + /// Number of packets that this node has received since startup, as recorded when this bloomfilter was created. + /// Used for determining the approximate packet rate and thus number of entries in the bloomfilter + pub(crate) packets_received_at_creation: usize, + + pub(crate) rotation_id: u32, +} + +impl ReplayProtectionBloomfilterMetadata { + const SERIALIZED_LEN: usize = size_of::() + size_of::() + size_of::(); + + // UNIX_TIMESTAMP || PACKETS_RECEIVED || ROTATION_ID + pub(crate) fn bytes(&self) -> Vec { + self.creation_time + .unix_timestamp() + .to_be_bytes() + .into_iter() + .chain((self.packets_received_at_creation as u64).to_be_bytes()) + .chain(self.rotation_id.to_be_bytes()) + .collect() + } + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::SERIALIZED_LEN { + return Err(NymNodeError::BloomfilterMetadataDeserialisationFailure); + } + + // SAFETY: we just checked we have correct number of bytes + #[allow(clippy::unwrap_used)] + let creation_timestamp = i64::from_be_bytes(bytes[0..8].try_into().unwrap()); + + #[allow(clippy::unwrap_used)] + let packets_received_at_creation = + u64::from_be_bytes(bytes[8..16].try_into().unwrap()) as usize; + + #[allow(clippy::unwrap_used)] + let rotation_id = u32::from_be_bytes(bytes[16..].try_into().unwrap()); + + Ok(ReplayProtectionBloomfilterMetadata { + creation_time: OffsetDateTime::from_unix_timestamp(creation_timestamp) + .map_err(|_| NymNodeError::BloomfilterMetadataDeserialisationFailure)?, + packets_received_at_creation, + rotation_id, + }) + } +} // it appears that now std Mutex is faster (or comparable) to parking_lot // in high contention situations: https://github.com/rust-lang/rust/pull/95035#issuecomment-1073966631 // (tokio's async Mutex has too much overhead due to the number of access required) #[derive(Clone)] -pub(crate) struct ReplayProtectionBloomfilter { +pub(crate) struct ReplayProtectionBloomfilters { disabled: bool, - inner: Arc>, + inner: Arc>, } -impl ReplayProtectionBloomfilter { - pub(crate) fn new_empty(items_count: usize, fp_p: f64) -> Result { - Ok(ReplayProtectionBloomfilter { +impl ReplayProtectionBloomfilters { + pub(crate) fn new(primary: RotationFilter, secondary: Option) -> Self { + // figure out if the secondary filter is the overlap or pre_announced filter + let primary_id = primary.metadata.rotation_id; + + let next = primary_id + 1; + let previous = primary_id.checked_sub(1); + let (overlap, pre_announced) = match secondary { + None => (None, None), + Some(secondary_filter) => { + let secondary_id = secondary_filter.metadata.rotation_id; + if secondary_id == next { + (None, Some(secondary_filter)) + } else if Some(secondary_id) == previous { + (Some(secondary_filter), None) + } else { + warn!("{secondary_id} is not valid for either pre_announced or overlap bloomfilter given primary rotation of {primary_id}"); + (None, None) + } + } + }; + + ReplayProtectionBloomfilters { disabled: false, - inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfilterInner { - current_filter: Bloom::new_for_fp_rate(items_count, fp_p) - .map_err(NymNodeError::bloomfilter_failure)?, + inner: Arc::new(Mutex::new(ReplayProtectionBloomfiltersInner { + primary, + overlap, + pre_announced, })), - }) + } } // SAFETY: the hardcoded values of 1,1 are valid #[allow(clippy::unwrap_used)] pub(crate) fn new_disabled() -> Self { // well, technically it's not fully empty, but the memory footprint is negligible - ReplayProtectionBloomfilter { + ReplayProtectionBloomfilters { disabled: true, - inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfilterInner { - current_filter: Bloom::new(1, 1).unwrap(), + inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfiltersInner { + primary: RotationFilter { + metadata: ReplayProtectionBloomfilterMetadata { + creation_time: OffsetDateTime::now_utc(), + packets_received_at_creation: 0, + rotation_id: u32::MAX, + }, + data: Bloom::new(1, 1).unwrap(), + }, + overlap: None, + pre_announced: None, })), } } @@ -48,14 +133,13 @@ impl ReplayProtectionBloomfilter { self.disabled } - pub(crate) fn reset(&self, items_count: usize, fp_p: f64) -> Result<(), NymNodeError> { - // 1. build the new filter - let new_inner = ReplayProtectionBloomfilterInner { - current_filter: Bloom::new_for_fp_rate(items_count, fp_p) - .map_err(NymNodeError::bloomfilter_failure)?, - }; - - // 2. swap it + pub(crate) fn allocate_pre_announced( + &self, + items_count: usize, + fp_p: f64, + packets_received_at_creation: usize, + rotation_id: u32, + ) -> Result<(), NymNodeError> { let mut guard = self .inner .lock() @@ -63,161 +147,256 @@ impl ReplayProtectionBloomfilter { message: "mutex got poisoned", })?; - *guard = new_inner; + guard.pre_announced = Some(RotationFilter::new( + items_count, + fp_p, + packets_received_at_creation, + rotation_id, + )?); Ok(()) } - // NOTE: with key rotations we'll have to check whether the file is still valid and which - // key it corresponds to, but that's a future problem - pub(crate) async fn load>(path: P) -> Result { - info!("attempting to load prior replay detection bloomfilter..."); - let path = path.as_ref(); - let mut file = - File::open(path) - .await - .map_err(|source| NymNodeError::BloomfilterIoFailure { - source, - path: path.to_path_buf(), - })?; - - let mut buf = Vec::new(); - file.read_to_end(&mut buf) - .await - .map_err(|source| NymNodeError::BloomfilterIoFailure { - source, - path: path.to_path_buf(), + pub(crate) fn promote_pre_announced(&self) -> Result<(), NymNodeError> { + let mut guard = self + .inner + .lock() + .map_err(|_| NymNodeError::BloomfilterFailure { + message: "mutex got poisoned", })?; - Ok(ReplayProtectionBloomfilter { - disabled: false, - inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfilterInner { - current_filter: Bloom::from_bytes(buf) - .map_err(NymNodeError::bloomfilter_failure)?, - })), - }) - } + let Some(mut pre_announced) = guard.pre_announced.take() else { + error!("there was no pre-announced bloomfilter to promote"); + return Ok(()); + }; - // average HDD has the write speed of ~80MB/s so a 2GB bloomfilter would take almost 30s to write... - // and this function is explicitly async and using tokio's async operations, because otherwise - // we'd have to go through the whole hassle of using spawn_blocking and awaiting that one instead - pub(crate) async fn flush_to_disk>(&self, path: P) -> Result<(), NymNodeError> { - debug!("flushing replay protection bloomfilter to disk..."); - let start = Instant::now(); - let path = path.as_ref(); - - let mut file = - File::create(path) - .await - .map_err(|source| NymNodeError::BloomfilterIoFailure { - source, - path: path.to_path_buf(), - })?; - let data = self.bytes().map_err(|_| NymNodeError::BloomfilterFailure { - message: "mutex got poisoned", - })?; - file.write_all(&data) - .await - .map_err(|source| NymNodeError::BloomfilterIoFailure { - source, - path: path.to_path_buf(), - })?; - - let elapsed = start.elapsed(); - - info!( - "flushed replay protection bloomfilter to disk. it took: {}", - elapsed.human_duration() - ); + // pre_announced -> primary + // primary -> temp (pre_announced) + mem::swap(&mut guard.primary, &mut pre_announced); + // temp (pre_announced) -> secondary + guard.overlap = Some(pre_announced); Ok(()) } -} -struct ReplayProtectionBloomfilterInner { - // metadata to do with epochs, etc. - current_filter: Bloom<[u8; REPLAY_TAG_SIZE]>, - // overlap_filter: bloomfilter::Bloom<[u8; REPLAY_TAG_SIZE]>, -} - -impl ReplayProtectionBloomfilter { - #[allow(dead_code)] - pub(crate) fn check_and_set( - &self, - replay_tag: &[u8; REPLAY_TAG_SIZE], - ) -> Result> { - let Ok(mut guard) = self.inner.lock() else { - return Err(PoisonError::new(())); - }; - - Ok(guard.current_filter.check_and_set(replay_tag)) + pub(crate) fn purge_secondary(&self) -> Result<(), NymNodeError> { + let mut guard = self + .inner + .lock() + .map_err(|_| NymNodeError::BloomfilterFailure { + message: "mutex got poisoned", + })?; + guard.overlap = None; + Ok(()) } - #[allow(dead_code)] - pub(crate) fn try_check_and_set( + pub(crate) fn primary_metadata( &self, - replay_tag: &[u8; REPLAY_TAG_SIZE], - ) -> Option>> { - let mut guard = match self.inner.try_lock() { - Ok(guard) => guard, - Err(TryLockError::Poisoned(_)) => return Some(Err(PoisonError::new(()))), - Err(TryLockError::WouldBlock) => return None, - }; + ) -> Result { + let metadata = self + .inner + .lock() + .map_err(|_| NymNodeError::BloomfilterFailure { + message: "mutex got poisoned", + })? + .primary + .metadata; - Some(Ok(guard.current_filter.check_and_set(replay_tag))) + Ok(metadata) } + pub(crate) fn primary_bytes_and_id(&self) -> Result<(Vec, u32), NymNodeError> { + let guard = self + .inner + .lock() + .map_err(|_| NymNodeError::BloomfilterFailure { + message: "mutex got poisoned", + })?; + + let id = guard.primary.metadata.rotation_id; + let bytes = guard.primary.bytes(); + Ok((bytes, id)) + } + + pub(crate) fn secondary_bytes_and_id(&self) -> Result, u32)>, NymNodeError> { + let guard = self + .inner + .lock() + .map_err(|_| NymNodeError::BloomfilterFailure { + message: "mutex got poisoned", + })?; + + let secondary = match guard.overlap.as_ref() { + Some(overlap) => overlap, + None => { + let Some(pre_announced) = guard.pre_announced.as_ref() else { + return Ok(None); + }; + pre_announced + } + }; + + let id = secondary.metadata.rotation_id; + let bytes = secondary.bytes(); + Ok(Some((bytes, id))) + } +} + +// map from particular rotation id to vector of results, based on the order of requests received +type BatchCheckResult = HashMap>; + +impl ReplayProtectionBloomfilters { pub(crate) fn batch_try_check_and_set( &self, - reply_tags: &[&[u8; REPLAY_TAG_SIZE]], - ) -> Option, PoisonError<()>>> { + reply_tags: &HashMap>, + ) -> Option>> { let mut guard = match self.inner.try_lock() { Ok(guard) => guard, Err(TryLockError::Poisoned(_)) => return Some(Err(PoisonError::new(()))), Err(TryLockError::WouldBlock) => return None, }; - let mut result = Vec::with_capacity(reply_tags.len()); - for tag in reply_tags { - result.push(guard.current_filter.check_and_set(tag)); - } - - // for testing throughput without disabling checks: - // return Some(Ok(vec![false; reply_tags.len()])); - - Some(Ok(result)) + Some(Ok(guard.batch_check_and_set(reply_tags))) } pub(crate) fn batch_check_and_set( &self, - reply_tags: &[&[u8; REPLAY_TAG_SIZE]], - ) -> Result, PoisonError<()>> { + reply_tags: &HashMap>, + ) -> Result>, PoisonError<()>> { let Ok(mut guard) = self.inner.lock() else { return Err(PoisonError::new(())); }; - let mut result = Vec::with_capacity(reply_tags.len()); - for tag in reply_tags { - result.push(guard.current_filter.check_and_set(tag)); + Ok(guard.batch_check_and_set(reply_tags)) + } +} + +struct ReplayProtectionBloomfiltersInner { + primary: RotationFilter, + + // don't worry, we'll never have 3 active filters at once, + // we will either have a overlap (during the first epoch of a new rotation) + // or a pre_announced (during the last epoch of the current rotation) + // during epoch transition, the following change will happen: + // primary -> overlap + // pre_announced -> primary + // I'm not using an enum because it's easier to reason about those as separate fields + overlap: Option, + pre_announced: Option, +} + +impl ReplayProtectionBloomfiltersInner { + fn batch_check_and_set( + &mut self, + reply_tags: &HashMap>, + ) -> HashMap> { + let mut result = HashMap::with_capacity(reply_tags.len()); + for (&rotation_id, reply_tags) in reply_tags { + // try to 'find' the relevant filter. we might be doing 3 reads here, but realistically it's + // going to be 'primary' most of the time and even if not, it's just few ns of overhead... + let filter = if self.primary.metadata.rotation_id == rotation_id { + Some(&mut self.primary.data) + } else if let Some(secondary) = &mut self.overlap { + // if let chaining won't be stable until 1.88 so we have to do the Option workaround + if secondary.metadata.rotation_id == rotation_id { + Some(&mut secondary.data) + } else { + None + } + } else if let Some(pre_announced) = &mut self.pre_announced { + if pre_announced.metadata.rotation_id == rotation_id { + Some(&mut pre_announced.data) + } else { + None + } + } else { + None + }; + + let Some(filter) = filter else { + // if we've received a packet from an unknown rotation, it most likely means it has been replayed + // from an older rotation, so mark it as such + result.insert(rotation_id, vec![false; reply_tags.len()]); + continue; + }; + + let mut rotation_results = Vec::with_capacity(reply_tags.len()); + for tag in reply_tags { + rotation_results.push(filter.check_and_set(tag)) + } + result.insert(rotation_id, rotation_results); } - // for testing throughput without disabling checks: - // return Ok(vec![false; reply_tags.len()]); - - Ok(result) + result } +} - #[allow(dead_code)] - pub(crate) fn clear(&self) -> Result<(), PoisonError<()>> { - let mut guard = self.inner.lock().map_err(|_| PoisonError::new(()))?; - guard.current_filter.clear(); - Ok(()) +pub(crate) struct RotationFilter { + metadata: ReplayProtectionBloomfilterMetadata, + data: Bloom<[u8; REPLAY_TAG_SIZE]>, +} + +impl RotationFilter { + pub(crate) fn new( + items_count: usize, + fp_p: f64, + packets_received_at_creation: usize, + rotation_id: u32, + ) -> Result { + let filter = + Bloom::new_for_fp_rate(items_count, fp_p).map_err(NymNodeError::bloomfilter_failure)?; + + Ok(RotationFilter { + metadata: ReplayProtectionBloomfilterMetadata { + creation_time: OffsetDateTime::now_utc(), + packets_received_at_creation, + rotation_id, + }, + data: filter, + }) } // due to the size of the bloomfilter, extra caution has to be applied when using this method // note: we're not getting reference to bytes as this method is used when flushing data to the disk // (which takes ~30s) and we can't block the mutex for that long. - fn bytes(&self) -> Result, PoisonError<()>> { - let guard = self.inner.lock().map_err(|_| PoisonError::new(()))?; - Ok(guard.current_filter.to_bytes()) + fn bytes(&self) -> Vec { + // attach metadata bytes at the end as it would make deserialisation cheaper (as we could avoid + // copying the bloomfilter bytes twice) + let mut bloom_bytes = self.data.to_bytes(); + bloom_bytes.extend_from_slice(&self.metadata.bytes()); + bloom_bytes + } + + pub(crate) fn try_from_bytes(bytes: Vec) -> Result { + let len = bytes.len(); + if bytes.len() < ReplayProtectionBloomfilterMetadata::SERIALIZED_LEN { + return Err(NymNodeError::BloomfilterMetadataDeserialisationFailure); + } + + let mut bloom_bytes = bytes; + let metadata_bytes = + bloom_bytes.split_off(len - ReplayProtectionBloomfilterMetadata::SERIALIZED_LEN); + + Ok(RotationFilter { + metadata: ReplayProtectionBloomfilterMetadata::try_from_bytes(&metadata_bytes)?, + data: Bloom::from_bytes(bloom_bytes).map_err(NymNodeError::bloomfilter_failure)?, + }) + } + + pub(crate) fn load>(path: P) -> Result { + info!("attempting to load prior replay detection bloomfilter..."); + let path = path.as_ref(); + let mut file = File::open(path).map_err(|source| NymNodeError::BloomfilterIoFailure { + source, + path: path.to_path_buf(), + })?; + + let mut buf = Vec::new(); + file.read_to_end(&mut buf) + .map_err(|source| NymNodeError::BloomfilterIoFailure { + source, + path: path.to_path_buf(), + })?; + + RotationFilter::try_from_bytes(buf) } } diff --git a/nym-node/src/node/replay_protection/helpers.rs b/nym-node/src/node/replay_protection/helpers.rs new file mode 100644 index 0000000000..d46e994919 --- /dev/null +++ b/nym-node/src/node/replay_protection/helpers.rs @@ -0,0 +1,34 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) fn parse_rotation_id_from_filename(name: &str) -> Option { + let stripped = name.strip_prefix("rot-")?; + let ext_idx = stripped.rfind(".").unwrap_or(stripped.len()); + let rotation = stripped.chars().take(ext_idx).collect::(); + rotation.parse::().ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parsing_rotation_id() { + let test_cases = vec![ + ("rot", None), + ("rot-123", Some(123)), + ("foo-123", None), + ("rot-123.ext", Some(123)), + ("rot-123.different-ext", Some(123)), + ("rot.123.aaa", None), + ]; + + for (raw, expected) in test_cases { + assert_eq!( + parse_rotation_id_from_filename(raw), + expected, + "failed: {raw} to {expected:?}" + ); + } + } +} diff --git a/nym-node/src/node/replay_protection/manager.rs b/nym-node/src/node/replay_protection/manager.rs new file mode 100644 index 0000000000..56b20d4ea5 --- /dev/null +++ b/nym-node/src/node/replay_protection/manager.rs @@ -0,0 +1,115 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::NymNodeError; +use crate::node::replay_protection::bloomfilter::{ReplayProtectionBloomfilters, RotationFilter}; +use crate::node::replay_protection::items_in_bloomfilter; +use human_repr::HumanCount; +use nym_node_metrics::NymNodeMetrics; +use std::cmp::max; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::info; + +#[derive(Clone)] +pub(crate) struct ReplayProtectionBloomfiltersManager { + target_fp_p: f64, + minimum_bloomfilter_packets_per_second: usize, + bloomfilter_size_multiplier: f64, + + metrics: NymNodeMetrics, + filters: ReplayProtectionBloomfilters, +} + +impl ReplayProtectionBloomfiltersManager { + pub(crate) fn new_disabled(metrics: NymNodeMetrics) -> Self { + // the exact config values are irrelevant as the filters will never be recreated + ReplayProtectionBloomfiltersManager { + target_fp_p: 0.001, + minimum_bloomfilter_packets_per_second: 1, + bloomfilter_size_multiplier: 1.0, + metrics, + filters: ReplayProtectionBloomfilters::new_disabled(), + } + } + + pub(crate) fn new( + config: &Config, + primary: RotationFilter, + secondary: Option, + metrics: NymNodeMetrics, + ) -> Self { + ReplayProtectionBloomfiltersManager { + target_fp_p: config.mixnet.replay_protection.debug.false_positive_rate, + minimum_bloomfilter_packets_per_second: config + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: config + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + metrics, + filters: ReplayProtectionBloomfilters::new(primary, secondary), + } + } + + pub(crate) fn bloomfilters(&self) -> ReplayProtectionBloomfilters { + self.filters.clone() + } + + pub(crate) fn primary_bytes_and_id(&self) -> Result<(Vec, u32), NymNodeError> { + self.filters.primary_bytes_and_id() + } + + pub(crate) fn secondary_bytes_and_id(&self) -> Result, u32)>, NymNodeError> { + self.filters.secondary_bytes_and_id() + } + + pub(crate) fn purge_secondary(&self) -> Result<(), NymNodeError> { + self.filters.purge_secondary() + } + + pub(crate) fn promote_pre_announced(&self) -> Result<(), NymNodeError> { + self.filters.promote_pre_announced() + } + + // TODO: actually do add some metrics + pub(crate) fn allocate_pre_announced( + &self, + rotation_id: u32, + rotation_lifetime: Duration, + ) -> Result<(), NymNodeError> { + // 1. estimated the number of items in the filter based on the extrapolated items received + // by the primary filter + let received = self.metrics.mixnet.ingress.forward_hop_packets_received() + + self.metrics.mixnet.ingress.final_hop_packets_received(); + + let primary = self.filters.primary_metadata()?; + let time_delta = OffsetDateTime::now_utc() - primary.creation_time; + let received_since_creation = received.saturating_sub(primary.packets_received_at_creation); + let received_per_second = + (received_since_creation as f64 / time_delta.as_seconds_f64()).round() as usize; + + let bf_received = max( + received_per_second, + self.minimum_bloomfilter_packets_per_second, + ); + let items_in_new_filter = items_in_bloomfilter(rotation_lifetime, bf_received); + let adjusted = + (items_in_new_filter as f64 * self.bloomfilter_size_multiplier).round() as usize; + + info!( + "allocating new bloom filter. new expected number of packets: {} that preserve fp rate of {}", + adjusted.human_count_bare(), + self.target_fp_p + ); + + // 2. allocate the filter + self.filters + .allocate_pre_announced(adjusted, self.target_fp_p, received, rotation_id) + } +} diff --git a/nym-node/src/node/replay_protection/mod.rs b/nym-node/src/node/replay_protection/mod.rs index 5d5c9d57e2..969e4f5fd0 100644 --- a/nym-node/src/node/replay_protection/mod.rs +++ b/nym-node/src/node/replay_protection/mod.rs @@ -6,6 +6,8 @@ use std::time::Duration; pub(crate) mod background_task; pub(crate) mod bloomfilter; +mod helpers; +pub(crate) mod manager; pub fn bitmap_size(false_positive_rate: f64, items_in_filter: usize) -> usize { /// Equivalent to ln(1 / 2^ln(2)) = −ln^2(2) diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index 17e229f4e3..7fb88b31b0 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -2,18 +2,28 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::error::NymNodeError; +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::node::routing_filter::network_filter::NetworkRoutingFilter; use async_trait::async_trait; +use nym_crypto::asymmetric::ed25519; use nym_gateway::node::UserAgent; use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; +use nym_noise::config::NoiseNetworkView; use nym_task::ShutdownToken; use nym_topology::node::RoutingNode; -use nym_topology::{EpochRewardedSet, NymTopology, Role, TopologyProvider}; +use nym_topology::provider_trait::ToTopologyMetadata; +use nym_topology::{ + EntryDetails, EpochRewardedSet, NodeId, NymTopology, NymTopologyMetadata, Role, + TopologyProvider, +}; use nym_validator_client::nym_api::NymApiClientExt; -use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_validator_client::nym_nodes::{ + NodesByAddressesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, +}; use nym_validator_client::{NymApiClient, ValidatorClientError}; -use std::collections::HashSet; -use std::net::IpAddr; +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, SocketAddr}; +use std::ops::Deref; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; @@ -22,6 +32,8 @@ use tracing::log::error; use tracing::{debug, trace, warn}; use url::Url; +const LOCAL_NODE_ID: NodeId = 1234567890; + struct NodesQuerier { client: NymApiClient, nym_api_urls: Vec, @@ -53,10 +65,12 @@ impl NodesQuerier { res } - async fn current_nymnodes(&mut self) -> Result, ValidatorClientError> { + async fn current_nymnodes( + &mut self, + ) -> Result { let res = self .client - .get_all_basic_nodes() + .get_all_expanded_nodes() .await .inspect_err(|err| error!("failed to get network nodes: {err}")); @@ -84,16 +98,40 @@ impl NodesQuerier { } } +pub(crate) struct LocalGatewayNode { + pub(crate) active_sphinx_keys: ActiveSphinxKeys, + pub(crate) mix_host: SocketAddr, + pub(crate) identity_key: ed25519::PublicKey, + pub(crate) entry: EntryDetails, +} + +impl LocalGatewayNode { + pub(crate) fn to_routing_node(&self) -> RoutingNode { + RoutingNode { + node_id: LOCAL_NODE_ID, + mix_host: self.mix_host, + entry: Some(self.entry.clone()), + identity_key: self.identity_key, + sphinx_key: self.active_sphinx_keys.primary().deref().x25519_pubkey(), + supported_roles: nym_topology::SupportedRoles { + mixnode: false, + mixnet_entry: true, + mixnet_exit: true, + }, + } + } +} + #[derive(Clone)] pub struct CachedTopologyProvider { - gateway_node: Arc, + gateway_node: Arc, cached_network: CachedNetwork, min_mix_performance: u8, } impl CachedTopologyProvider { pub(crate) fn new( - gateway_node: RoutingNode, + gateway_node: LocalGatewayNode, cached_network: CachedNetwork, min_mix_performance: u8, ) -> Self { @@ -111,20 +149,30 @@ impl TopologyProvider for CachedTopologyProvider { let network_guard = self.cached_network.inner.read().await; let self_node = self.gateway_node.identity_key; - let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone()) - .with_additional_nodes(network_guard.network_nodes.iter().filter(|node| { - if node.supported_roles.mixnode { - node.performance.round_to_integer() >= self.min_mix_performance - } else { - true - } - })); + let mut topology = NymTopology::new( + network_guard.topology_metadata, + network_guard.rewarded_set.clone(), + Vec::new(), + ) + .with_additional_nodes( + network_guard + .network_nodes + .iter() + .map(|node| &node.basic) + .filter(|node| { + if node.supported_roles.mixnode { + node.performance.round_to_integer() >= self.min_mix_performance + } else { + true + } + }), + ); - if !topology.has_node_details(self.gateway_node.node_id) { + if !topology.has_node(self.gateway_node.identity_key) { debug!("{self_node} didn't exist in topology. inserting it.",); - topology.insert_node_details(self.gateway_node.as_ref().clone()); + topology.insert_node_details(self.gateway_node.to_routing_node()); } - topology.force_set_active(self.gateway_node.node_id, Role::EntryGateway); + topology.force_set_active(LOCAL_NODE_ID, Role::EntryGateway); Some(topology) } @@ -140,6 +188,7 @@ impl CachedNetwork { CachedNetwork { inner: Arc::new(RwLock::new(CachedNetworkInner { rewarded_set: Default::default(), + topology_metadata: Default::default(), network_nodes: vec![], })), } @@ -148,7 +197,8 @@ impl CachedNetwork { struct CachedNetworkInner { rewarded_set: EpochRewardedSet, - network_nodes: Vec, + topology_metadata: NymTopologyMetadata, + network_nodes: Vec, } pub struct NetworkRefresher { @@ -159,6 +209,7 @@ pub struct NetworkRefresher { network: CachedNetwork, routing_filter: NetworkRoutingFilter, + noise_view: NoiseNetworkView, } impl NetworkRefresher { @@ -177,7 +228,7 @@ impl NetworkRefresher { let mut this = NetworkRefresher { querier: NodesQuerier { - client: NymApiClient { nym_api }, + client: NymApiClient::from(nym_api), nym_api_urls, currently_used_api: 0, }, @@ -186,6 +237,7 @@ impl NetworkRefresher { shutdown_token, network: CachedNetwork::new_empty(), routing_filter: NetworkRoutingFilter::new_empty(testnet), + noise_view: NoiseNetworkView::new_empty(), }; this.obtain_initial_network().await?; @@ -235,12 +287,14 @@ impl NetworkRefresher { async fn refresh_network_nodes_inner(&mut self) -> Result<(), ValidatorClientError> { let rewarded_set = self.querier.rewarded_set().await?; - let nodes = self.querier.current_nymnodes().await?; + let res = self.querier.current_nymnodes().await?; + let nodes = res.nodes; + let metadata = res.metadata; // collect all known/allowed nodes information let known_nodes = nodes .iter() - .flat_map(|n| n.ip_addresses.iter()) + .flat_map(|n| n.basic.ip_addresses.iter()) .copied() .collect::>(); @@ -263,7 +317,24 @@ impl NetworkRefresher { self.routing_filter.resolved.swap_denied(current_denied); self.routing_filter.pending.clear().await; + //update noise Noise Nodes + let noise_nodes = nodes + .iter() + .filter(|n| n.x25519_noise_versioned_key.is_some()) + .flat_map(|n| { + n.basic.ip_addresses.iter().map(|ip_addr| { + ( + SocketAddr::new(*ip_addr, n.basic.mix_port), + #[allow(clippy::unwrap_used)] + n.x25519_noise_versioned_key.unwrap(), // SAFETY : we filtered out nodes where this option can be None + ) + }) + }) + .collect::>(); + self.noise_view.swap_view(noise_nodes); + let mut network_guard = self.network.inner.write().await; + network_guard.topology_metadata = metadata.to_topology_metadata(); network_guard.network_nodes = nodes; network_guard.rewarded_set = rewarded_set; @@ -296,6 +367,10 @@ impl NetworkRefresher { self.network.clone() } + pub(crate) fn noise_view(&self) -> NoiseNetworkView { + self.noise_view.clone() + } + pub(crate) async fn run(&mut self) { let mut full_refresh_interval = interval(self.full_refresh_interval); full_refresh_interval.reset(); diff --git a/nym-node/src/throughput_tester/client.rs b/nym-node/src/throughput_tester/client.rs index a402511f7b..32a0cf78f5 100644 --- a/nym-node/src/throughput_tester/client.rs +++ b/nym-node/src/throughput_tester/client.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::throughput_tester::stats::ClientStats; use anyhow::bail; use arrayref::array_ref; @@ -14,7 +15,7 @@ use nym_crypto::asymmetric::x25519; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_framing::codec::{NymCodec, NymCodecError}; use nym_sphinx_framing::packet::FramedNymPacket; -use nym_sphinx_params::PacketSize; +use nym_sphinx_params::{PacketSize, SphinxKeyRotation}; use nym_sphinx_routing::generate_hop_delays; use nym_sphinx_types::constants::{ EXPANDED_SHARED_SECRET_HKDF_INFO, EXPANDED_SHARED_SECRET_HKDF_SALT, @@ -28,6 +29,7 @@ use nym_task::ShutdownToken; use rand::rngs::OsRng; use sha2::Sha256; use std::net::SocketAddr; +use std::ops::Deref; use std::pin::Pin; use std::task::{Context, Poll, Waker}; use std::time::Duration; @@ -99,6 +101,7 @@ pub(crate) struct ThroughputTestingClient { listener: TcpListener, forward_connection: Framed, payload_key: PayloadKey, + key_rotation: SphinxKeyRotation, } fn rederive_lioness_payload_key(shared_secret: &[u8; 32]) -> PayloadKey { @@ -119,7 +122,7 @@ impl ThroughputTestingClient { initial_sending_delay: Duration, initial_batch_size: usize, latency_threshold: Duration, - node_keys: &x25519::KeyPair, + node_keys: ActiveSphinxKeys, node_listener: SocketAddr, stats: ClientStats, cancellation_token: ShutdownToken, @@ -137,10 +140,14 @@ impl ThroughputTestingClient { // keys of this client let ephemeral_keys = x25519::KeyPair::new(&mut rng); + let loaded_private = node_keys.primary(); + let private = loaded_private.deref(); + let public = private.x25519_pubkey(); + let route = [ Node::new( NymNodeRoutingAddress::from(node_listener).try_into()?, - (*node_keys.public_key()).into(), + public.into(), ), Node::new( NymNodeRoutingAddress::from(local_address).try_into()?, @@ -168,9 +175,9 @@ impl ThroughputTestingClient { // derive the expanded shared secret for our node so we could tag the payload to figure out latency // by tagging the packet - let shared_secret = node_keys - .private_key() + let shared_secret = private .as_ref() + .inner() .diffie_hellman(&header.shared_secret); let payload_key = rederive_lioness_payload_key(shared_secret.as_bytes()); @@ -189,6 +196,12 @@ impl ThroughputTestingClient { } }; + let key_rotation = if loaded_private.is_even_rotation() { + SphinxKeyRotation::EvenRotation + } else { + SphinxKeyRotation::OddRotation + }; + Ok(ThroughputTestingClient { stats, last_received_update: Instant::now(), @@ -204,6 +217,7 @@ impl ThroughputTestingClient { listener, forward_connection: Framed::new(forward_connection, NymCodec), payload_key, + key_rotation, }) } @@ -250,7 +264,14 @@ impl ThroughputTestingClient { packet_bytes.append(&mut payload_bytes); let forward_packet = NymPacket::sphinx_from_bytes(&packet_bytes)?; - Ok(FramedNymPacket::new(forward_packet, Default::default())) + // let key_rotation = if self.s + + Ok(FramedNymPacket::new( + forward_packet, + Default::default(), + self.key_rotation, + false, + )) } async fn send_packets(&mut self) -> anyhow::Result<()> { diff --git a/nym-node/src/throughput_tester/global_stats.rs b/nym-node/src/throughput_tester/global_stats.rs index 97239ddcee..e095c702c5 100644 --- a/nym-node/src/throughput_tester/global_stats.rs +++ b/nym-node/src/throughput_tester/global_stats.rs @@ -150,9 +150,7 @@ impl GlobalStatsUpdater { info!("wrote global stats to {}", global.display()); for (sender_id, records) in self.records.iter() { - let output = self - .output_directory - .join(format!("sender{}.csv", sender_id)); + let output = self.output_directory.join(format!("sender{sender_id}.csv")); let mut writer = csv::Writer::from_path(&output)?; for record in records { writer.serialize(record)?; diff --git a/nym-node/src/throughput_tester/mod.rs b/nym-node/src/throughput_tester/mod.rs index 68a7719fbd..aaa2360726 100644 --- a/nym-node/src/throughput_tester/mod.rs +++ b/nym-node/src/throughput_tester/mod.rs @@ -2,19 +2,17 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::upgrade_helpers::try_load_current_config; +use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::node::NymNode; use crate::throughput_tester::client::ThroughputTestingClient; use crate::throughput_tester::global_stats::GlobalStatsUpdater; use crate::throughput_tester::stats::ClientStats; use futures::future::join_all; use human_repr::HumanDuration; -use indicatif::{ProgressState, ProgressStyle}; -use nym_crypto::asymmetric::x25519; use nym_task::ShutdownToken; use rand::{thread_rng, Rng}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; use tokio::runtime; use tokio::runtime::Runtime; @@ -72,7 +70,7 @@ impl ThroughputTest { #[allow(clippy::too_many_arguments)] async fn run_testing_client( sender_id: usize, - node_keys: Arc, + node_keys: ActiveSphinxKeys, node_listener: SocketAddr, packet_latency_threshold: Duration, starting_sending_batch_size: usize, @@ -85,7 +83,7 @@ async fn run_testing_client( starting_sending_delay, starting_sending_batch_size, packet_latency_threshold, - &node_keys, + node_keys, node_listener, stats, shutdown_token, @@ -117,7 +115,7 @@ pub(crate) fn test_mixing_throughput( let nym_node = tester.prepare_nymnode(config_path)?; let listener = nym_node.config().mixnet.bind_address; - let sphinx_keys = nym_node.x25519_sphinx_keys(); + let sphinx_keys = nym_node.active_sphinx_keys()?; let mut stats = Vec::with_capacity(senders); for _ in 0..senders { @@ -125,18 +123,6 @@ pub(crate) fn test_mixing_throughput( } let header_span = info_span!("header"); - header_span.pb_set_style( - &ProgressStyle::with_template( - "testing mixing throughput of this machine... {wide_msg} {elapsed}\n{wide_bar}", - )? - .with_key( - "elapsed", - |state: &ProgressState, writer: &mut dyn std::fmt::Write| { - let _ = writer.write_str(&format!("{}", state.elapsed().human_duration())); - }, - ) - .progress_chars("---"), - ); header_span.pb_start(); // Bit of a hack to show a full "-----" line underneath the header. diff --git a/nym-signers-monitor/Cargo.toml b/nym-signers-monitor/Cargo.toml new file mode 100644 index 0000000000..d043aa2871 --- /dev/null +++ b/nym-signers-monitor/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "nym-signers-monitor" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } +humantime = { workspace = true } +itertools = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing = { workspace = true } +url = { workspace = true } + +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-ecash-signer-check = { path = "../common/ecash-signer-check" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-task = { path = "../common/task" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +zulip-client = { path = "../common/zulip-client" } + +[lints] +workspace = true diff --git a/service-providers/authenticator/src/cli/build_info.rs b/nym-signers-monitor/src/cli/build_info.rs similarity index 54% rename from service-providers/authenticator/src/cli/build_info.rs rename to nym-signers-monitor/src/cli/build_info.rs index 7351753134..077d989975 100644 --- a/service-providers/authenticator/src/cli/build_info.rs +++ b/nym-signers-monitor/src/cli/build_info.rs @@ -1,16 +1,15 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only -use clap::Args; use nym_bin_common::bin_info_owned; use nym_bin_common::output_format::OutputFormat; -#[derive(Args)] -pub(crate) struct BuildInfo { +#[derive(clap::Args, Debug)] +pub(crate) struct Args { #[arg(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } -pub(crate) fn execute(args: BuildInfo) { +pub(crate) fn execute(args: Args) { println!("{}", args.output.format(&bin_info_owned!())) } diff --git a/nym-signers-monitor/src/cli/env.rs b/nym-signers-monitor/src/cli/env.rs new file mode 100644 index 0000000000..2be0cbbb1e --- /dev/null +++ b/nym-signers-monitor/src/cli/env.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod vars { + pub(crate) const ZULIP_BOT_EMAIL_ARG: &str = "ZULIP_BOT_EMAIL"; + pub(crate) const ZULIP_BOT_API_KEY_ARG: &str = "ZULIP_BOT_API_KEY"; + pub(crate) const ZULIP_SERVER_URL_ARG: &str = "ZULIP_SERVER_URL"; + pub(crate) const ZULIP_NOTIFICATION_CHANNEL_ID_ARG: &str = "ZULIP_NOTIFICATION_CHANNEL_ID"; + pub(crate) const ZULIP_NOTIFICATION_CHANNEL_TOPIC_ARG: &str = + "ZULIP_NOTIFICATION_CHANNEL_TOPIC"; + + pub(crate) const SIGNERS_MONITOR_CHECK_INTERVAL_ARG: &str = "SIGNERS_MONITOR_CHECK_INTERVAL"; + pub(crate) const SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY_ARG: &str = + "SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY"; + + pub(crate) const KNOWN_NETWORK_NAME_ARG: &str = "KNOWN_NETWORK_NAME"; + pub(crate) const NYXD_CLIENT_CONFIG_ENV_FILE_ARG: &str = "NYXD_CLIENT_CONFIG_ENV_FILE"; + pub(crate) const NYXD_RPC_ENDPOINT_ARG: &str = "NYXD_RPC_ENDPOINT"; + pub(crate) const NYXD_DKG_CONTRACT_ADDRESS_ARG: &str = "NYXD_DKG_CONTRACT_ADDRESS"; +} diff --git a/nym-signers-monitor/src/cli/mod.rs b/nym-signers-monitor/src/cli/mod.rs new file mode 100644 index 0000000000..7a59f459fd --- /dev/null +++ b/nym-signers-monitor/src/cli/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; + +pub(crate) mod build_info; +pub(crate) mod env; +pub(crate) mod run; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::Run(args) => run::execute(*args).await?, + } + + Ok(()) + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Start signers monitor and send notifications on any failures + Run(Box), +} diff --git a/nym-signers-monitor/src/cli/run.rs b/nym-signers-monitor/src/cli/run.rs new file mode 100644 index 0000000000..b0b0855b49 --- /dev/null +++ b/nym-signers-monitor/src/cli/run.rs @@ -0,0 +1,167 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::env::vars::*; +use crate::monitor::SignersMonitor; +use anyhow::{bail, Context}; +use clap::ArgGroup; +use nym_network_defaults::{setup_env, NymNetworkDetails}; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::QueryHttpRpcNyxdClient; +use std::time::Duration; +use url::Url; + +#[derive(Debug, clap::Args)] +pub(crate) struct NyxdConnectionArgs { + // for well-known networks, such mainnet, we can use hardcoded values + /// Name of a well known network (such as 'mainnet') that has well-known + /// pre-configured setup values + #[clap(long, env = KNOWN_NETWORK_NAME_ARG)] + pub(crate) known_network_name: Option, + + /// Path pointing to an env file that configures the nyxd client. + #[clap( + short, + long, + env = NYXD_CLIENT_CONFIG_ENV_FILE_ARG + )] + pub(crate) config_env_file: Option, + + /// For unknown networks (or if one wishes to override defaults), + /// specify the RPC endpoint of a node from which signer information should be retrieved + #[clap(long, env = NYXD_RPC_ENDPOINT_ARG)] + pub(crate) nyxd_rpc_endpoint: Option, + + /// For unknown networks, specify address of the DKG contract to pull signer information from. + #[clap( + long, + requires("nyxd_rpc_endpoint"), + env = NYXD_DKG_CONTRACT_ADDRESS_ARG + )] + pub(crate) dkg_contract_address: Option, + // if needed down the line (not sure why), we could define additional args + // for specifying denoms, etc. + // #[clap(long, requires("dkg_contract_address"))] + // pub(crate) mix_denom: Option, +} + +impl NyxdConnectionArgs { + fn get_minimal_nym_network_details(&self) -> anyhow::Result { + if let Some(known_network_name) = &self.known_network_name { + match known_network_name.as_str() { + "mainnet" => return Ok(NymNetworkDetails::new_mainnet()), + other => bail!("{other} is not a known network name - please use another method of setting up chain connection"), + } + } + + if let Some(config_env_file) = &self.config_env_file { + setup_env(Some(config_env_file)); + return Ok(NymNetworkDetails::new_from_env()); + } + + // SAFETY: clap ensures at least one of the fields is set + #[allow(clippy::unwrap_used)] + let dkg_contract = self.dkg_contract_address.as_ref().unwrap(); + + // use mainnet's chain details (i.e. prefixes, denoms, etc) + let mainnet_chain_details = NymNetworkDetails::new_mainnet().chain_details; + Ok(NymNetworkDetails::new_empty() + .with_chain_details(mainnet_chain_details) + .with_coconut_dkg_contract(Some(dkg_contract.to_string()))) + } + + pub(crate) fn try_create_nyxd_client(&self) -> anyhow::Result { + let network_details = self.get_minimal_nym_network_details()?; + + let nyxd_endpoint = match &self.nyxd_rpc_endpoint { + Some(nyxd_rpc_endpoint) => nyxd_rpc_endpoint.clone(), + None => network_details + .endpoints + .first() + .context("no nyxd endpoints provided")? + .nyxd_url + .parse()?, + }; + + Ok(QueryHttpRpcNyxdClient::connect_with_network_details( + nyxd_endpoint.as_str(), + network_details, + )?) + } +} + +#[derive(clap::Args, Debug)] +#[command(group( + ArgGroup::new("nyxd_connection") + .multiple(true) + .required(true) + .args([ + "known_network_name", + "config_env_file", + "nyxd_rpc_endpoint" + ]) +))] +pub(crate) struct Args { + /// Specify email address for the bot responsible for sending notifications to the zulip server + /// in case 'upgrade' mode is detected + #[clap( + long, + env = ZULIP_BOT_EMAIL_ARG + )] + pub(crate) zulip_bot_email: String, + + /// Specify the API key for the bot responsible for sending notifications to the zulip server + /// in case 'upgrade' mode is detected + #[clap( + long, + env = ZULIP_BOT_API_KEY_ARG + )] + pub(crate) zulip_bot_api_key: String, + + /// Specify the sever endpoint for the bot responsible for sending notifications + /// in case 'upgrade' mode is detected + #[clap( + long, + env = ZULIP_SERVER_URL_ARG + )] + pub(crate) zulip_server_url: Url, + + /// Specify the channel id for where the notification is going to be sent + #[clap( + long, + env = ZULIP_NOTIFICATION_CHANNEL_ID_ARG + )] + pub(crate) zulip_notification_channel_id: u32, + + /// Optionally specify the channel topic for where the notification is going to be sent + #[clap( + long, + env = ZULIP_NOTIFICATION_CHANNEL_TOPIC_ARG + )] + pub(crate) zulip_notification_topic: Option, + + /// Specify the delay between subsequent signers checks + #[clap( + long, + env = SIGNERS_MONITOR_CHECK_INTERVAL_ARG, + value_parser = humantime::parse_duration, + default_value = "15m" + )] + pub(crate) signers_check_interval: Duration, + + /// Specify the minimum delay between two subsequent notifications + #[clap( + long, + env = SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY_ARG, + value_parser = humantime::parse_duration, + default_value = "1h" + )] + pub(crate) minimum_notification_delay: Duration, + + #[clap(flatten)] + pub(crate) nyxd_connection: NyxdConnectionArgs, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + SignersMonitor::new(args)?.run().await +} diff --git a/nym-signers-monitor/src/main.rs b/nym-signers-monitor/src/main.rs new file mode 100644 index 0000000000..b2955277e0 --- /dev/null +++ b/nym-signers-monitor/src/main.rs @@ -0,0 +1,24 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::Cli; +use clap::Parser; +use nym_bin_common::bin_info_owned; +use nym_bin_common::logging::setup_tracing_logger; +use tracing::{info, trace}; + +mod cli; +mod monitor; +pub(crate) mod test_result; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing_logger(); + let cli = Cli::parse(); + trace!("args: {cli:#?}"); + + let bin_info = bin_info_owned!(); + info!("using the following version: {bin_info}"); + + cli.execute().await +} diff --git a/nym-signers-monitor/src/monitor.rs b/nym-signers-monitor/src/monitor.rs new file mode 100644 index 0000000000..caae9cbe1f --- /dev/null +++ b/nym-signers-monitor/src/monitor.rs @@ -0,0 +1,223 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::run; +use crate::test_result::{DisplayableSignerResult, Summary, TestResult}; +use nym_ecash_signer_check::check_signers_with_client; +use nym_task::ShutdownManager; +use nym_validator_client::QueryHttpRpcNyxdClient; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::time::interval; +use tracing::{error, info}; + +const LOST_QUORUM_MSG: &str = r#" +# 🔥🔥🔥 LOST SIGNING QUORUM 🔥🔥🔥 +We seem to have lost the signing quorum - check if we should enable the 'upgrade' mode! +"#; + +const UNKNOWN_QUORUM_MSG: &str = r#" +# ❓❓❓ UNKNOWN SIGNING QUORUM ❓❓❓ +We can't determine the signing quroum - if we're not undergoing DKG exchange check if we should enable the 'upgrade' mode! +"#; + +pub(crate) struct SignersMonitor { + zulip_client: zulip_client::Client, + nyxd_client: QueryHttpRpcNyxdClient, + + notification_channel_id: u32, + notification_topic: Option, + check_interval: Duration, + min_notification_delay: Duration, + last_notification_sent: Option, +} + +impl SignersMonitor { + pub(crate) fn new(args: run::Args) -> anyhow::Result { + let zulip_client = zulip_client::Client::builder( + args.zulip_bot_email, + args.zulip_bot_api_key, + args.zulip_server_url, + )? + .build()?; + let nyxd_client = args.nyxd_connection.try_create_nyxd_client()?; + + Ok(SignersMonitor { + zulip_client, + nyxd_client, + notification_channel_id: args.zulip_notification_channel_id, + notification_topic: args.zulip_notification_topic, + check_interval: args.signers_check_interval, + min_notification_delay: args.minimum_notification_delay, + last_notification_sent: None, + }) + } + + async fn check_signers(&mut self) -> anyhow::Result { + info!("starting signer check..."); + let check_result = check_signers_with_client(&self.nyxd_client).await?; + + let mut unreachable_signers = 0; + let mut unknown_local_chain_status = 0; + let mut stalled_local_chain = 0; + let mut working_local_chain = 0; + let mut unknown_credential_issuance_status = 0; + let mut working_credential_issuance = 0; + let mut unavailable_credential_issuance = 0; + + let mut fully_working = 0; + + let mut signers = Vec::new(); + for result in &check_result.results { + if result.signer_unreachable() { + unreachable_signers += 1; + } + + if result.unknown_chain_status() { + unknown_local_chain_status += 1; + } + if result.chain_available() { + working_local_chain += 1; + } + if result.chain_provably_stalled() || result.chain_unprovably_stalled() { + stalled_local_chain += 1; + } + + if result.unknown_signing_status() { + unknown_credential_issuance_status += 1; + } + if result.signing_available() { + working_credential_issuance += 1; + } + if result.signing_provably_unavailable() || result.signing_unprovably_unavailable() { + unavailable_credential_issuance += 1; + } + + let signing_available = if result.unknown_signing_status() { + None + } else { + Some(result.signing_available()) + }; + + let chain_not_stalled = if result.unknown_chain_status() { + None + } else { + Some(result.chain_available()) + }; + + if (result.chain_available()) && (result.signing_available()) { + fully_working += 1; + } + + signers.push(DisplayableSignerResult { + version: result + .try_get_test_result() + .map(|r| r.reported_version.clone()), + url: result.information.announce_address.clone(), + signing_available, + chain_not_stalled, + }) + } + + let signing_quorum_available = check_result.threshold.map(|threshold| { + (working_local_chain as u64) >= threshold + && (working_credential_issuance as u64) >= threshold + }); + signers.sort_by_key(|s| s.version.clone()); + + let summary = Summary { + signing_quorum_available, + fully_working, + unreachable_signers, + registered_signers: check_result.results.len(), + unknown_local_chain_status, + stalled_local_chain, + working_local_chain, + unknown_credential_issuance_status, + working_credential_issuance, + unavailable_credential_issuance, + threshold: check_result.threshold, + }; + + Ok(TestResult { summary, signers }) + } + + async fn perform_signer_check(&mut self) -> anyhow::Result<()> { + let result = self.check_signers().await?; + let result_md = result.results_to_markdown_message(); + + let msg = if result.quorum_unavailable() { + Some(format!("{LOST_QUORUM_MSG}\n\n{result_md}",)) + } else if result.quorum_unknown() { + Some(format!("{UNKNOWN_QUORUM_MSG}\n\n{result_md}",)) + } else { + None + }; + + if let Some(msg) = msg { + self.maybe_notify_about_failure(&msg).await? + } + + Ok(()) + } + + async fn maybe_notify_about_failure(&mut self, message: &String) -> anyhow::Result<()> { + if let Some(last_notification_sent) = self.last_notification_sent { + if last_notification_sent + self.min_notification_delay > OffsetDateTime::now_utc() { + info!("too soon to send another notification"); + return Ok(()); + } + } + self.send_zulip_notification(message).await?; + self.last_notification_sent = Some(OffsetDateTime::now_utc()); + Ok(()) + } + + async fn send_zulip_notification(&self, message: &String) -> anyhow::Result<()> { + self.zulip_client + .send_channel_message(( + self.notification_channel_id, + message, + &self.notification_topic, + )) + .await?; + Ok(()) + } + + async fn send_shutdown_notification(&self) -> anyhow::Result<()> { + println!("here be sending shutdown notification"); + Ok(()) + } + + pub(crate) async fn run(&mut self) -> anyhow::Result<()> { + let shutdown_manager = + ShutdownManager::new("nym-signers-monitor").with_default_shutdown_signals()?; + + let mut check_interval = interval(self.check_interval); + + while !shutdown_manager.root_token.is_cancelled() { + tokio::select! { + biased; + _ = shutdown_manager.root_token.cancelled() => { + info!("received shutdown"); + break; + } + _ = check_interval.tick() => { + if let Err(err) = self.perform_signer_check().await { + error!("failed to check signers: {err}"); + } + } + + } + } + + shutdown_manager.close(); + shutdown_manager.run_until_shutdown().await; + + if let Err(err) = self.send_shutdown_notification().await { + error!("failed to send shutdown notification: {err}"); + } + + Ok(()) + } +} diff --git a/nym-signers-monitor/src/test_result.rs b/nym-signers-monitor/src/test_result.rs new file mode 100644 index 0000000000..1fb091ec14 --- /dev/null +++ b/nym-signers-monitor/src/test_result.rs @@ -0,0 +1,115 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use itertools::Itertools; + +fn maybe_bool_to_emoji_string(maybe_bool: Option) -> String { + match maybe_bool { + None => "⚠️ unknown".into(), + Some(true) => "✅ yes".into(), + Some(false) => "❌ no".into(), + } +} + +pub(crate) struct DisplayableSignerResult { + pub(crate) url: String, + pub(crate) version: Option, + pub(crate) signing_available: Option, + pub(crate) chain_not_stalled: Option, +} + +impl DisplayableSignerResult { + fn to_markdown_table_row(&self) -> String { + format!( + "| {} | {} | {} | {} |", + self.url, + self.version.as_deref().unwrap_or("unknown"), + maybe_bool_to_emoji_string(self.signing_available), + maybe_bool_to_emoji_string(self.chain_not_stalled) + ) + } +} + +pub(crate) struct TestResult { + pub(crate) summary: Summary, + pub(crate) signers: Vec, +} + +impl TestResult { + pub(crate) fn quorum_unavailable(&self) -> bool { + self.summary.signing_quorum_available.unwrap_or(false) + } + + pub(crate) fn quorum_unknown(&self) -> bool { + self.summary.signing_quorum_available.is_none() + } + + pub(crate) fn results_to_markdown_message(&self) -> String { + let p_available = format!( + "{:.2}", + (self.summary.fully_working as f32 / self.summary.registered_signers as f32) * 100. + ); + + format!( + r#" +## Summary +- quorum available: {} ({p_available}% of signers fully available) +- signers fully working: {} +- signing threshold: {} +- registered signers: {} +- unreachable signers: {} + +### Chain Status +- unknown status: {} +- working chain: {} +- stalled chain: {} + +### Credential Issuance Status +(note: signers below 1.1.64 do not return fully reliable results) +- unknown status: {} +- working issuance: {} +- unavailable issuance: {} + +## Detailed Results +| address | version | chain working | issuance (maybe) available | +| - | - | - | - | +{} + "#, + maybe_bool_to_emoji_string(self.summary.signing_quorum_available), + self.summary.fully_working, + self.summary + .threshold + .map(|threshold| threshold.to_string()) + .unwrap_or("???".to_string()), + self.summary.registered_signers, + self.summary.unreachable_signers, + self.summary.unknown_local_chain_status, + self.summary.working_local_chain, + self.summary.stalled_local_chain, + self.summary.unknown_credential_issuance_status, + self.summary.working_credential_issuance, + self.summary.unavailable_credential_issuance, + self.signers + .iter() + .map(|r| r.to_markdown_table_row()) + .join("\n") + ) + } +} + +pub(crate) struct Summary { + pub(crate) signing_quorum_available: Option, + pub(crate) fully_working: usize, + pub(crate) threshold: Option, + + pub(crate) registered_signers: usize, + pub(crate) unreachable_signers: usize, + + pub(crate) unknown_local_chain_status: usize, + pub(crate) stalled_local_chain: usize, + pub(crate) working_local_chain: usize, + + pub(crate) unknown_credential_issuance_status: usize, + pub(crate) working_credential_issuance: usize, + pub(crate) unavailable_credential_issuance: usize, +} diff --git a/nym-statistics-api/.gitignore b/nym-statistics-api/.gitignore new file mode 100644 index 0000000000..ba051bb2e8 --- /dev/null +++ b/nym-statistics-api/.gitignore @@ -0,0 +1,2 @@ +*.pem +.env* diff --git a/nym-statistics-api/.sqlx/query-13bf07e42c49ea365e816eb94e4e4f607989ee95f68a0fcd95bc4a53f4e79cbb.json b/nym-statistics-api/.sqlx/query-13bf07e42c49ea365e816eb94e4e4f607989ee95f68a0fcd95bc4a53f4e79cbb.json new file mode 100644 index 0000000000..7525b8cc22 --- /dev/null +++ b/nym-statistics-api/.sqlx/query-13bf07e42c49ea365e816eb94e4e4f607989ee95f68a0fcd95bc4a53f4e79cbb.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO active_device (\n day,\n device_id,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n from_mixnet)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (device_id, day) DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Date", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "13bf07e42c49ea365e816eb94e4e4f607989ee95f68a0fcd95bc4a53f4e79cbb" +} diff --git a/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json b/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json new file mode 100644 index 0000000000..b477ea243d --- /dev/null +++ b/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO connection_stats (\n received_at,\n connection_time_ms,\n two_hop,\n source_ip,\n country_code,\n from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Int4", + "Bool", + "Text", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba" +} diff --git a/nym-statistics-api/Cargo.toml b/nym-statistics-api/Cargo.toml new file mode 100644 index 0000000000..5d070a1dcd --- /dev/null +++ b/nym-statistics-api/Cargo.toml @@ -0,0 +1,57 @@ +# Copyright 2025 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-statistics-api" +version = "0.1.5" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + + +[dependencies] +anyhow.workspace = true +axum = { workspace = true, features = ["tokio", "macros"] } +axum-client-ip.workspace = true +axum-extra = { workspace = true, features = ["typed-header"] } +celes.workspace = true +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "postgres", + "time", +] } +time.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-util.workspace = true +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tower-http = { workspace = true, features = ["cors", "trace"] } +url.workspace = true +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +utoipauto.workspace = true + +#internal +nym-bin-common = { path = "../common/bin-common" } +nym-http-api-common = { path = "../common/http-api-common", features = [ + "middleware", +] } +nym-statistics-common = { path = "../common/statistics", features = [ + "openapi", +] } +nym-task = { path = "../common/task" } + +nym-http-api-client = { path = "../common/http-api-client" } +nym-validator-client = { path = "../common/client-libs/validator-client" } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres"] } diff --git a/nym-statistics-api/Dockerfile b/nym-statistics-api/Dockerfile new file mode 100644 index 0000000000..48f6449048 --- /dev/null +++ b/nym-statistics-api/Dockerfile @@ -0,0 +1,34 @@ +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-statistics-api + +RUN cargo build --release + + +#------------------------------------------------------------------- +# The following environment variables are required at runtime: +# +# DATABASE_URL +# POSTGRES_USER +# POSTGRES_PASSWORD +# PG_SSL_CERT +# +# And optionally: +# +# NYM_API_URL +# NYM_STATISTICS_API_HTTP_PORT +# PGPORT +# +# see https://github.com/nymtech/nym/blob/develop/nym-statistics-api/src/cli/mod.rs for details +#------------------------------------------------------------------- + +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-statistics-api ./ +ENTRYPOINT [ "/nym/nym-statistics-api" ] diff --git a/nym-statistics-api/README.md b/nym-statistics-api/README.md new file mode 100644 index 0000000000..8b633c4631 --- /dev/null +++ b/nym-statistics-api/README.md @@ -0,0 +1,28 @@ +# Nym-statistics-api + +A simple API to collect and store statistics sent by nym-vpn-client. + +## Build instructions + +The statistics API is backed by a PostgreSQL database so you'll need a PostgreSQL server running if you want to add migrations or add/modify SQL queries. I recommend https://postgresapp.com on MacOS, very easy to use. If you're on another OS, it's up to you. + +Assuming your database is running at `postgresql://user:password@host:port/database_name` you'll likely need to run the following : +```bash +DATABASE_URL="postgresql://user:password@host:port/database_name" + +# if you don't have an existing datase +sqlx database create --database-url $DATABASE_URL +sqlx migrate run --database-url $DATABASE_URL + +# reset it if you messed with migrations while developping +sqlx database reset --database-url $DATABASE_URL + +# or just run new migrations +sqlx migrate run --database-url $DATABASE_URL + +# then prepare queries for offline build mode +cargo sqlx prepare --database-url $DATABASE_URL +``` + +This should allow `cargo build` without having any postgreSQL server running. +Be sure to add the `.sqlx` directory to version control diff --git a/nym-statistics-api/launch_statistics_api.sh b/nym-statistics-api/launch_statistics_api.sh new file mode 100755 index 0000000000..1ec650f99e --- /dev/null +++ b/nym-statistics-api/launch_statistics_api.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +user_rust_log_preference=$RUST_LOG + + +function run_bare() { + # export necessary env vars + set -a + source .env + set +a + export RUST_LOG=${user_rust_log_preference:-debug} + echo "RUST_LOG=${RUST_LOG}" + + cargo run --package nym-statistics-api +} + + +# Requires pg_up.sh, or a running postres instance, with the correct parameters in an .env file +run_bare diff --git a/nym-statistics-api/migrations/001_init.sql b/nym-statistics-api/migrations/001_init.sql new file mode 100644 index 0000000000..739df793bd --- /dev/null +++ b/nym-statistics-api/migrations/001_init.sql @@ -0,0 +1,24 @@ + +CREATE TABLE active_device ( + day DATE NOT NULL, + device_id TEXT NOT NULL, + os_type TEXT, + os_version TEXT, + architecture TEXT, + app_version TEXT, + user_agent TEXT, + from_mixnet BOOLEAN, + PRIMARY KEY (device_id, day) +); + +CREATE TABLE connection_stats ( + received_at TIMESTAMP WITH TIME ZONE NOT NULL, + connection_time_ms INTEGER, + two_hop BOOLEAN, + source_ip TEXT, + country_code TEXT, + from_mixnet BOOLEAN +); + + +CREATE INDEX idx_active_device_day ON active_device (day); \ No newline at end of file diff --git a/nym-statistics-api/pg_up.sh b/nym-statistics-api/pg_up.sh new file mode 100755 index 0000000000..db84118209 --- /dev/null +++ b/nym-statistics-api/pg_up.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +export PGUSER="nym" +export PGPASSWORD="password1" +export PGPORT="5432" +export DB_NAME="nym_statistics_api" +export DATABASE_URL="postgres://${PGUSER}:${PGPASSWORD}@localhost:${PGPORT}/${DB_NAME}" + +cat < .env +SQLX_OFFLINE=true +POSTGRES_USER=$PGUSER +POSTGRES_PASSWORD=$PGPASSWORD +PGPORT=$PGPORT +DB_NAME=$DB_NAME +DATABASE_URL=$DATABASE_URL +EOF + +cat < init_schema.sql +CREATE SCHEMA private_statistics_api; +EOF + + +docker run --rm -it \ + --name ${DB_NAME} \ + -e POSTGRES_USER=${PGUSER} \ + -e POSTGRES_PASSWORD=${PGPASSWORD} \ + -e POSTGRES_DB=${DB_NAME} \ + -v $(pwd)/init_schema.sql:/docker-entrypoint-initdb.d/init_schema.sql \ + -p ${PGPORT}:${PGPORT} \ + postgres + +rm init_schema.sql + + +# sqlx migrate run +# cargo sqlx prepare \ No newline at end of file diff --git a/nym-statistics-api/src/cli/mod.rs b/nym-statistics-api/src/cli/mod.rs new file mode 100644 index 0000000000..528d552e65 --- /dev/null +++ b/nym-statistics-api/src/cli/mod.rs @@ -0,0 +1,42 @@ +use clap::Parser; +use nym_bin_common::bin_info; +use std::{path::PathBuf, sync::OnceLock}; +use url::Url; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Clone, Debug, Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// URL for the NYM API to get a network view from + #[clap(long, env = "NYM_API_URL")] + pub(crate) nym_api_url: Option, + + /// HTTP port on which to run statistics api. + #[clap(long, default_value_t = 8000, env = "NYM_STATISTICS_API_HTTP_PORT")] + pub(crate) http_port: u16, + + /// Connection url for the database. + #[clap(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + /// Username for the database. + #[clap(long, env = "POSTGRES_USER")] + pub(crate) username: String, + + /// Password for the database. + #[clap(long, env = "POSTGRES_PASSWORD")] + pub(crate) password: String, + + /// PgSQL port for the database. + #[clap(long, default_value_t = 5432, env = "PGPORT")] + pub(crate) pg_port: u16, + + /// SSL root certificate path. Providing this will also set the ssl mode to `Require` + #[clap(long, env = "PG_SSL_CERT")] + pub(crate) ssl_cert_path: Option, +} diff --git a/nym-statistics-api/src/http/api/mod.rs b/nym-statistics-api/src/http/api/mod.rs new file mode 100644 index 0000000000..16e9763bd0 --- /dev/null +++ b/nym-statistics-api/src/http/api/mod.rs @@ -0,0 +1,75 @@ +use anyhow::anyhow; +use axum::{response::Redirect, Router}; +use nym_http_api_common::middleware::logging::log_request_info; +use tokio::net::ToSocketAddrs; +use tower_http::cors::CorsLayer; +use utoipa::OpenApi; +use utoipa_swagger_ui::{Config, SwaggerUi}; + +use crate::http::{server::HttpServer, state::AppState}; + +pub(crate) mod stats; + +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + pub(crate) fn with_default_routes() -> Self { + let router = Router::new() + .merge( + SwaggerUi::new("/swagger") + .config(Config::default().supported_submit_methods(["get"])) + .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), + ) + .route( + "/", + axum::routing::get(|| async { Redirect::permanent("/swagger") }), // SW let's redirect to a blogpost explaining the stats collection process once it exists + ) + .nest("/v1", Router::new().nest("/stats", stats::routes())); + + Self { + unfinished_router: router, + } + } + + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + fn finalize_routes(self) -> Router { + // layers added later wrap earlier layers + self.unfinished_router + // CORS layer needs to wrap other API layers + .layer(setup_cors()) + // logger should be outermost layer + .layer(axum::middleware::from_fn(log_request_info)) + } +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: A, + ) -> anyhow::Result { + tokio::net::TcpListener::bind(bind_address) + .await + .map(|listener| HttpServer::new(self.router, listener)) + .map_err(|err| anyhow!("Couldn't bind to address due to {}", err)) + } +} + +fn setup_cors() -> CorsLayer { + use axum::http::Method; + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} diff --git a/nym-statistics-api/src/http/api/stats.rs b/nym-statistics-api/src/http/api/stats.rs new file mode 100644 index 0000000000..7e7a51ec14 --- /dev/null +++ b/nym-statistics-api/src/http/api/stats.rs @@ -0,0 +1,68 @@ +use axum::{extract::State, Json, Router}; +use axum_client_ip::InsecureClientIp; +use axum_extra::{headers::UserAgent, TypedHeader}; +use nym_statistics_common::report::vpn_client::VpnClientStatsReport; +use tracing::debug; + +use crate::{ + http::{ + error::{HttpError, HttpResult}, + state::AppState, + }, + storage::models::{ConnectionInfoDto, DailyActiveDeviceDto}, +}; + +pub(crate) fn routes() -> Router { + Router::new().route("/report", axum::routing::post(submit_stats_report)) +} + +#[utoipa::path( + post, + request_body = VpnClientStatsReport, + tag = "Stats", + path = "/report", + context_path = "/v1/stats", + responses( + (status = 200) + ) +)] +#[tracing::instrument(level = "info", skip_all)] +async fn submit_stats_report( + State(mut state): State, + TypedHeader(user_agent): TypedHeader, + insecure_ip_addr: InsecureClientIp, + Json(report): Json, +) -> HttpResult> { + let now = time::OffsetDateTime::now_utc(); + + let gateway_record = state + .network_view() + .get_country_by_ip(&insecure_ip_addr.0) + .await; + + let from_mixnet = gateway_record.is_some(); + let maybe_location = gateway_record.unwrap_or_default(); + + if from_mixnet { + debug!("Received a report from the network"); + } else { + debug!("Received a report from outside of the network"); + } + + let active_device = DailyActiveDeviceDto::new(now, &report, user_agent, from_mixnet); + let maybe_connection_info = ConnectionInfoDto::maybe_new( + now, + &report, + insecure_ip_addr.0, + maybe_location, + from_mixnet, + ); + + state + .storage() + .store_vpn_client_report(active_device, maybe_connection_info) + .await + .map_err(HttpError::internal_with_logging)?; + + Ok(Json(())) +} diff --git a/nym-statistics-api/src/http/api_docs.rs b/nym-statistics-api/src/http/api_docs.rs new file mode 100644 index 0000000000..b3a644a0bb --- /dev/null +++ b/nym-statistics-api/src/http/api_docs.rs @@ -0,0 +1,10 @@ +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// manually import external structs which are behind feature flags because they +// can't be automatically discovered +// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829 +#[utoipauto(paths = "./nym-statistics-api/src")] +#[derive(OpenApi)] +#[openapi(info(title = "Nym Statistics API"), tags(), components(schemas()))] +pub(super) struct ApiDoc; diff --git a/nym-statistics-api/src/http/error.rs b/nym-statistics-api/src/http/error.rs new file mode 100644 index 0000000000..15119ac67e --- /dev/null +++ b/nym-statistics-api/src/http/error.rs @@ -0,0 +1,28 @@ +use std::fmt::Display; + +pub(crate) type HttpResult = Result; + +pub(crate) struct HttpError { + message: String, + status: axum::http::StatusCode, +} + +impl HttpError { + pub(crate) fn internal_with_logging(msg: impl Display) -> Self { + tracing::error!("{}", msg.to_string()); + Self::internal() + } + + pub(crate) fn internal() -> Self { + Self { + message: serde_json::json!({"message": "Internal server error"}).to_string(), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl axum::response::IntoResponse for HttpError { + fn into_response(self) -> axum::response::Response { + (self.status, self.message).into_response() + } +} diff --git a/nym-statistics-api/src/http/mod.rs b/nym-statistics-api/src/http/mod.rs new file mode 100644 index 0000000000..1506514f0f --- /dev/null +++ b/nym-statistics-api/src/http/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod api; +pub(crate) mod api_docs; +pub(crate) mod error; +pub(crate) mod server; +pub(crate) mod state; diff --git a/nym-statistics-api/src/http/server.rs b/nym-statistics-api/src/http/server.rs new file mode 100644 index 0000000000..8f84f3e4fb --- /dev/null +++ b/nym-statistics-api/src/http/server.rs @@ -0,0 +1,48 @@ +use axum::Router; +use core::net::SocketAddr; +use nym_task::ShutdownToken; +use tokio::net::TcpListener; + +use crate::{ + http::{api::RouterBuilder, state::AppState}, + network_view::NetworkView, + storage::StatisticsStorage, +}; + +pub(crate) async fn build_http_api( + storage: StatisticsStorage, + cached_network: NetworkView, + http_port: u16, +) -> anyhow::Result { + let router_builder = RouterBuilder::with_default_routes(); + + let state = AppState::new(storage, cached_network).await; + let router = router_builder.with_state(state); + + let bind_addr = format!("0.0.0.0:{http_port}"); + tracing::info!("Binding server to {bind_addr}"); + + router.build_server(bind_addr).await +} + +pub(crate) struct HttpServer { + router: Router, + listener: TcpListener, +} + +impl HttpServer { + pub(crate) fn new(router: Router, listener: TcpListener) -> Self { + Self { router, listener } + } + + pub(crate) async fn run(self, shutdown_token: ShutdownToken) -> std::io::Result<()> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async move { shutdown_token.cancelled().await }) + .await + } +} diff --git a/nym-statistics-api/src/http/state.rs b/nym-statistics-api/src/http/state.rs new file mode 100644 index 0000000000..0f17aa5b84 --- /dev/null +++ b/nym-statistics-api/src/http/state.rs @@ -0,0 +1,24 @@ +use crate::{network_view::NetworkView, storage::StatisticsStorage}; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + storage_manager: StatisticsStorage, + network_view: NetworkView, +} + +impl AppState { + pub(crate) async fn new(storage_manager: StatisticsStorage, network_view: NetworkView) -> Self { + Self { + storage_manager, + network_view, + } + } + + pub(crate) fn storage(&mut self) -> &mut StatisticsStorage { + &mut self.storage_manager + } + + pub(crate) fn network_view(&self) -> &NetworkView { + &self.network_view + } +} diff --git a/nym-statistics-api/src/logging.rs b/nym-statistics-api/src/logging.rs new file mode 100644 index 0000000000..39c2810226 --- /dev/null +++ b/nym-statistics-api/src/logging.rs @@ -0,0 +1,45 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { + fn directive_checked(directive: impl Into) -> anyhow::Result { + directive.into().parse().map_err(From::from) + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy(); + + // these crates are more granularly filtered + let warn_crates = [ + "rustls", + "sqlx", + "tower_http", + "axum", + "reqwest", + "hyper_util", + "nym_http_api_client", + ]; + for crate_name in warn_crates { + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))?); + } + + let log_level_hint = filter.max_level_hint(); + + log_builder.with_env_filter(filter).init(); + tracing::info!("Log level: {:?}", log_level_hint); + + Ok(()) +} diff --git a/nym-statistics-api/src/main.rs b/nym-statistics-api/src/main.rs new file mode 100644 index 0000000000..4af7c83a36 --- /dev/null +++ b/nym-statistics-api/src/main.rs @@ -0,0 +1,54 @@ +use clap::Parser; +use network_view::NetworkRefresher; +use nym_task::ShutdownManager; + +mod cli; +mod http; +mod logging; +mod network_view; +mod storage; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logging::setup_tracing_logger()?; + + let args = cli::Cli::parse(); + + let connection_url = args.database_url.clone(); + tracing::debug!("Using config:\n{:#?}", args); + + let storage = storage::StatisticsStorage::init( + connection_url, + args.username, + args.password, + args.pg_port, + args.ssl_cert_path, + ) + .await?; + tracing::info!("Connection to database successful"); + + let shutdown_manager = ShutdownManager::new("nym-statistics-api"); + + let network_refresher = NetworkRefresher::initialise_new( + args.nym_api_url, + shutdown_manager.child_token("network-refresher"), + ) + .await; + + let http_server = + http::server::build_http_api(storage, network_refresher.network_view(), args.http_port) + .await + .expect("Failed to build http server"); + let server_shutdown = shutdown_manager.clone_token("http-api-server"); + + // Starting tasks + shutdown_manager.spawn(async move { http_server.run(server_shutdown).await }); + network_refresher.start(); + + tracing::info!("Started HTTP server on port {}", args.http_port); + + shutdown_manager.close(); + shutdown_manager.run_until_shutdown().await; + + Ok(()) +} diff --git a/nym-statistics-api/src/network_view.rs b/nym-statistics-api/src/network_view.rs new file mode 100644 index 0000000000..ca8352d6b0 --- /dev/null +++ b/nym-statistics-api/src/network_view.rs @@ -0,0 +1,164 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::Result; +use nym_task::ShutdownToken; + +use celes::Country; +use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::NymApiClient; +use std::collections::HashMap; +use std::time::Duration; +use std::{net::IpAddr, sync::Arc}; +use tokio::sync::RwLock; +use tokio::time::interval; +use url::Url; + +use tracing::{error, info, trace, warn}; + +const NETWORK_CACHE_TTL: Duration = Duration::from_secs(600); + +type IpToCountryMap = HashMap>; + +// SW this should use a proper NS API client once it exists +struct NodesQuerier { + client: NymApiClient, +} + +impl NodesQuerier { + async fn current_nymnodes(&self) -> Result> { + Ok(self + .client + .get_all_described_nodes() + .await + .inspect_err(|err| error!("failed to get network nodes: {err}"))?) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct NetworkView { + inner: Arc>, +} + +impl NetworkView { + fn new_empty() -> Self { + NetworkView { + inner: Arc::new(RwLock::new(NetworkViewInner { + network_nodes: HashMap::new(), + })), + } + } + + pub(crate) async fn get_country_by_ip(&self, ip_addr: &IpAddr) -> Option> { + self.inner.read().await.network_nodes.get(ip_addr).copied() + } +} + +#[derive(Debug)] +struct NetworkViewInner { + network_nodes: IpToCountryMap, +} + +pub struct NetworkRefresher { + querier: Option, + full_refresh_interval: Duration, + shutdown_token: ShutdownToken, + + network: NetworkView, +} + +impl NetworkRefresher { + pub(crate) async fn initialise_new( + maybe_nym_api_url: Option, + shutdown_token: ShutdownToken, + ) -> Self { + let node_querier = match maybe_nym_api_url { + Some(url) => match Self::build_http_api_client(url) { + Ok(client) => Some(NodesQuerier { client }), + Err(e) => { + warn!("Failed to build Nym API client, no network view will be availabe : {e}"); + None + } + }, + None => { + warn!("No Nym API specified, network view is unavailable"); + None + } + }; + + let mut this = NetworkRefresher { + querier: node_querier, + full_refresh_interval: NETWORK_CACHE_TTL, + shutdown_token, + network: NetworkView::new_empty(), + }; + + if let Err(e) = this.refresh_network_nodes().await { + warn!("Failed to fetch initial network nodes : {e}"); + } + this + } + + fn build_http_api_client(url: Url) -> Result { + Ok( + nym_http_api_client::Client::builder::<_, anyhow::Error>(url)? + .no_hickory_dns() + .with_user_agent("node-statistics-api") + .build::()? + .into(), + ) + } + + async fn refresh_network_nodes(&mut self) -> Result<()> { + if let Some(querier) = &self.querier { + let nodes = querier.current_nymnodes().await?; + + // collect all known/allowed nodes information + let known_nodes = nodes + .iter() + .flat_map(|n| { + n.description + .host_information + .ip_address + .clone() + .into_iter() + .zip(std::iter::repeat(n.description.auxiliary_details.location)) + }) + .collect::>(); + + let mut network_guard = self.network.inner.write().await; + network_guard.network_nodes = known_nodes; + } + + Ok(()) + } + + pub(crate) fn network_view(&self) -> NetworkView { + self.network.clone() + } + + pub(crate) async fn run(&mut self) { + info!("NetworkRefresher started successfully"); + let mut full_refresh_interval = interval(self.full_refresh_interval); + full_refresh_interval.reset(); + + while !self.shutdown_token.is_cancelled() { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("NetworkRefresher: Received shutdown"); + } + _ = full_refresh_interval.tick() => { + if self.refresh_network_nodes().await.is_err() { + warn!("Failed to refresh network nodes, we're gonna keep the same set"); + } + } + } + } + trace!("NetworkRefresher: Exiting"); + } + + pub(crate) fn start(mut self) { + tokio::spawn(async move { self.run().await }); + } +} diff --git a/nym-statistics-api/src/storage/mod.rs b/nym-statistics-api/src/storage/mod.rs new file mode 100644 index 0000000000..4ea9bad118 --- /dev/null +++ b/nym-statistics-api/src/storage/mod.rs @@ -0,0 +1,122 @@ +use anyhow::{anyhow, Result}; +use models::{ConnectionInfoDto, DailyActiveDeviceDto}; +use sqlx::{ + migrate::Migrator, + postgres::{PgConnectOptions, PgPoolOptions}, + Executor, +}; +use std::{path::PathBuf, str::FromStr}; + +pub(crate) mod models; + +pub(crate) type DbPool = sqlx::PgPool; + +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); +const SET_SEARCH_PATH: &str = "SET search_path = private_statistics_api"; + +#[derive(Debug, Clone)] +pub(crate) struct StatisticsStorage { + connection_pool: DbPool, +} + +impl StatisticsStorage { + pub async fn init( + connection_url: String, + user: String, + password: String, + port: u16, + ssl_cert_path: Option, + ) -> Result { + let mut connect_options = PgConnectOptions::from_str(&connection_url)? + .port(port) + .username(&user) + .password(&password) + .application_name(nym_bin_common::bin_info!().binary_name); + + if let Some(ssl_cert) = ssl_cert_path { + connect_options = connect_options + .ssl_mode(sqlx::postgres::PgSslMode::Require) + .ssl_root_cert(ssl_cert); + } + + // This is a custom connection so the _sqlx_migrations table is not written in the public schema + // It then ensures we'll only write in the given schema, allowing to have the schema name only once here + // Ref : https://github.com/launchbadge/sqlx/issues/1835 + let pool = PgPoolOptions::new() + .after_connect(|conn, _meta| { + Box::pin(async move { + conn.execute(SET_SEARCH_PATH).await?; + Ok(()) + }) + }) + .connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + MIGRATOR.run(&pool).await?; + + Ok(StatisticsStorage { + connection_pool: pool, + }) + } + + pub(crate) async fn store_vpn_client_report( + &mut self, + active_device: DailyActiveDeviceDto, + connection_info: Option, + ) -> Result<()> { + self.store_device(active_device).await?; + if let Some(connection_info) = connection_info { + self.store_connection_stats(connection_info).await?; + } + Ok(()) + } + + async fn store_device(&self, active_device: DailyActiveDeviceDto) -> Result<()> { + sqlx::query!( + r#"INSERT INTO active_device ( + day, + device_id, + os_type, + os_version, + architecture, + app_version, + user_agent, + from_mixnet) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (device_id, day) DO NOTHING"#, + active_device.day as time::Date, + active_device.stats_id, + active_device.os_type, + active_device.os_version, + active_device.os_arch, + active_device.app_version, + active_device.user_agent, + active_device.from_mixnet + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + async fn store_connection_stats(&self, connection_info: ConnectionInfoDto) -> Result<()> { + sqlx::query!( + r#"INSERT INTO connection_stats ( + received_at, + connection_time_ms, + two_hop, + source_ip, + country_code, + from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)"#, + connection_info.received_at as time::OffsetDateTime, + connection_info.connection_time_ms, + connection_info.two_hop, + connection_info.received_from, + connection_info.country_code, + connection_info.from_mixnet + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } +} diff --git a/nym-statistics-api/src/storage/models.rs b/nym-statistics-api/src/storage/models.rs new file mode 100644 index 0000000000..669bcf2a54 --- /dev/null +++ b/nym-statistics-api/src/storage/models.rs @@ -0,0 +1,69 @@ +use std::net::IpAddr; + +use axum_extra::headers::UserAgent; +use celes::Country; +use nym_statistics_common::report::vpn_client::VpnClientStatsReport; +use time::{Date, OffsetDateTime}; + +pub type StatsId = String; + +#[derive(Debug, Clone, sqlx::FromRow)] +pub(crate) struct DailyActiveDeviceDto { + pub(crate) day: Date, + pub(crate) stats_id: StatsId, + pub(crate) os_type: String, + pub(crate) os_version: Option, + pub(crate) os_arch: String, + pub(crate) app_version: String, + pub(crate) user_agent: String, + pub(crate) from_mixnet: bool, +} + +impl DailyActiveDeviceDto { + pub(crate) fn new( + received_at: OffsetDateTime, + stats_report: &VpnClientStatsReport, + user_agent: UserAgent, + from_mixnet: bool, + ) -> Self { + Self { + day: received_at.date(), + stats_id: stats_report.stats_id.clone(), + os_type: stats_report.static_information.os_type.clone(), + os_version: stats_report.static_information.os_version.clone(), + os_arch: stats_report.static_information.os_arch.clone(), + app_version: stats_report.static_information.app_version.clone(), + user_agent: user_agent.to_string(), + from_mixnet, + } + } +} + +#[derive(Debug, Clone, sqlx::FromRow)] +pub(crate) struct ConnectionInfoDto { + pub(crate) received_at: OffsetDateTime, + pub(crate) received_from: String, + pub(crate) connection_time_ms: Option, + pub(crate) two_hop: bool, + pub(crate) country_code: Option, + pub(crate) from_mixnet: bool, +} + +impl ConnectionInfoDto { + pub(crate) fn maybe_new( + received_at: OffsetDateTime, + stats_report: &VpnClientStatsReport, + received_from: IpAddr, + maybe_country: Option, + from_mixnet: bool, + ) -> Option { + stats_report.basic_usage.as_ref().map(|usage_report| Self { + received_at, + received_from: received_from.to_string(), + connection_time_ms: usage_report.connection_time_ms, + two_hop: usage_report.two_hop, + country_code: maybe_country.map(|c| c.alpha2.into()), + from_mixnet, + }) + } +} diff --git a/nym-validator-rewarder/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json b/nym-validator-rewarder/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json index c4d9958ff8..db2fce9a87 100644 --- a/nym-validator-rewarder/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json +++ b/nym-validator-rewarder/.sqlx/query-2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c.json @@ -6,7 +6,7 @@ { "name": "height", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-validator-rewarder/.sqlx/query-397bde15134e32921ad87037e9436dcb76982d7859e406daa12c97f671e6fd3b.json b/nym-validator-rewarder/.sqlx/query-397bde15134e32921ad87037e9436dcb76982d7859e406daa12c97f671e6fd3b.json index a1147e12e0..c17602c590 100644 --- a/nym-validator-rewarder/.sqlx/query-397bde15134e32921ad87037e9436dcb76982d7859e406daa12c97f671e6fd3b.json +++ b/nym-validator-rewarder/.sqlx/query-397bde15134e32921ad87037e9436dcb76982d7859e406daa12c97f671e6fd3b.json @@ -6,7 +6,7 @@ { "name": "height", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-validator-rewarder/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json b/nym-validator-rewarder/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json index 0f91e82479..3abe29d4f4 100644 --- a/nym-validator-rewarder/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json +++ b/nym-validator-rewarder/.sqlx/query-3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec.json @@ -6,7 +6,7 @@ { "name": "last_pruned_height", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-validator-rewarder/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json b/nym-validator-rewarder/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json index e588b0a25a..7d41b257f4 100644 --- a/nym-validator-rewarder/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json +++ b/nym-validator-rewarder/.sqlx/query-c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d.json @@ -6,7 +6,7 @@ { "name": "last_processed_height", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-validator-rewarder/.sqlx/query-d67b6b3fc1099b3ca48eed945d9873717ba4b9d8f83bcb05f8a39094f0ff7c32.json b/nym-validator-rewarder/.sqlx/query-d67b6b3fc1099b3ca48eed945d9873717ba4b9d8f83bcb05f8a39094f0ff7c32.json index 0413a18c5b..73c6b6ec58 100644 --- a/nym-validator-rewarder/.sqlx/query-d67b6b3fc1099b3ca48eed945d9873717ba4b9d8f83bcb05f8a39094f0ff7c32.json +++ b/nym-validator-rewarder/.sqlx/query-d67b6b3fc1099b3ca48eed945d9873717ba4b9d8f83bcb05f8a39094f0ff7c32.json @@ -6,7 +6,7 @@ { "name": "count", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-validator-rewarder/.sqlx/query-e08a6456f6bd3cc5e8201d18dc3a989aecff01e835cb8fc04acee1b83480a970.json b/nym-validator-rewarder/.sqlx/query-e08a6456f6bd3cc5e8201d18dc3a989aecff01e835cb8fc04acee1b83480a970.json index a7a5ad47d1..e08e1e9534 100644 --- a/nym-validator-rewarder/.sqlx/query-e08a6456f6bd3cc5e8201d18dc3a989aecff01e835cb8fc04acee1b83480a970.json +++ b/nym-validator-rewarder/.sqlx/query-e08a6456f6bd3cc5e8201d18dc3a989aecff01e835cb8fc04acee1b83480a970.json @@ -6,7 +6,7 @@ { "name": "height", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index 3281e9f782..65e2781fd1 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -49,6 +49,7 @@ nym-serde-helpers = { path = "../common/serde-helpers", features = ["base64"] } nym-pemstore = { path = "../common/pemstore" } [build-dependencies] +anyhow = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/nym-validator-rewarder/build.rs b/nym-validator-rewarder/build.rs index 545f9c66b6..fbb510e095 100644 --- a/nym-validator-rewarder/build.rs +++ b/nym-validator-rewarder/build.rs @@ -1,25 +1,24 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// it's fine if compilation fails -#[allow(clippy::unwrap_used)] -#[allow(clippy::expect_used)] +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/scraper-example.sqlite"); let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -28,4 +27,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/nym-validator-rewarder/src/rewarder/block_signing/types.rs b/nym-validator-rewarder/src/rewarder/block_signing/types.rs index 02fc8a54cf..0a27e2258b 100644 --- a/nym-validator-rewarder/src/rewarder/block_signing/types.rs +++ b/nym-validator-rewarder/src/rewarder/block_signing/types.rs @@ -1,11 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::NymRewarderError; -use crate::rewarder::helpers::{consensus_pubkey_to_address, operator_account_to_owner_account}; +use crate::{ + error::NymRewarderError, + rewarder::helpers::{consensus_pubkey_to_address, operator_account_to_owner_account}, +}; use cosmwasm_std::{Decimal, Uint128}; -use nym_validator_client::nyxd::module_traits::staking; -use nym_validator_client::nyxd::{AccountId, Coin}; +use nym_validator_client::nyxd::{module_traits::staking, AccountId, Coin}; use nyxd_scraper::models; use std::collections::HashMap; use tracing::info; @@ -20,7 +21,7 @@ pub struct ValidatorSigning { pub voting_power_at_epoch_start: i64, pub voting_power_ratio: Decimal, - pub signed_blocks: i32, + pub signed_blocks: i64, pub ratio_signed: Decimal, } @@ -55,13 +56,13 @@ pub struct EpochSigningResults { #[derive(Debug)] pub struct RawValidatorResult { - pub signed_blocks: i32, + pub signed_blocks: i64, pub voting_power: i64, pub whitelisted: bool, } impl RawValidatorResult { - pub fn new(signed_blocks: i32, voting_power: i64, whitelisted: bool) -> Self { + pub fn new(signed_blocks: i64, voting_power: i64, whitelisted: bool) -> Self { Self { signed_blocks, voting_power, diff --git a/nym-validator-rewarder/src/rewarder/storage/manager.rs b/nym-validator-rewarder/src/rewarder/storage/manager.rs index 9c3218ac73..3eb638ea3c 100644 --- a/nym-validator-rewarder/src/rewarder/storage/manager.rs +++ b/nym-validator-rewarder/src/rewarder/storage/manager.rs @@ -186,7 +186,7 @@ impl StorageManager { amount: String, voting_power: i64, voting_power_share: String, - signed_blocks: i32, + signed_blocks: i64, signed_blocks_percent: String, ) -> Result<(), sqlx::Error> { sqlx::query!( diff --git a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs index 8cca3f062e..cb0964caf3 100644 --- a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs +++ b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs @@ -15,9 +15,9 @@ use nym_validator_client::ecash::models::{ CommitedDeposit, DepositId, IssuedTicketbooksChallengeCommitmentRequestBody, IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequestBody, IssuedTicketbooksDataResponse, IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponse, - SignableMessageBody, SignedMessage, }; use nym_validator_client::nyxd::AccountId; +use nym_validator_client::signable::{SignableMessageBody, SignedMessage}; use rand::distributions::{Distribution, WeightedIndex}; use rand::prelude::SliceRandom; use rand::thread_rng; @@ -349,6 +349,8 @@ impl IssuerUnderTest { } }; + self.issued_commitment = Some(issued_ticketbooks.clone()); + // 1. check if the signature on the response matches if !issued_ticketbooks.verify_signature(&self.details.public_key) { error!("❗ RESPONSE SIGNATURE MISMATCH ❗"); @@ -374,7 +376,6 @@ impl IssuerUnderTest { .merkle_root_hex() .unwrap_or_default() ); - self.issued_commitment = Some(issued_ticketbooks) } async fn issue_deposit_challenge( @@ -429,6 +430,8 @@ impl IssuerUnderTest { } }; + self.challenge_commitment_response = Some(challenge_commitment.clone()); + // 2. check if the signature on the response matches if !challenge_commitment.verify_signature(&self.details.public_key) { error!("❗ RESPONSE SIGNATURE MISMATCH ❗"); @@ -501,7 +504,6 @@ impl IssuerUnderTest { } info!("✅ obtained issued ticketbooks challenge commitment"); - self.challenge_commitment_response = Some(challenge_commitment) } fn verify_partial_ticketbook( diff --git a/nym-wallet/.storybook/mocks/tauri/core.js b/nym-wallet/.storybook/mocks/tauri/core.js new file mode 100644 index 0000000000..2a81b7c205 --- /dev/null +++ b/nym-wallet/.storybook/mocks/tauri/core.js @@ -0,0 +1,8 @@ +/** + * This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use + * or import dependencies that use Tauri. + */ + +module.exports = { + invoke: () => undefined, +} \ No newline at end of file diff --git a/nym-wallet/.storybook/mocks/tauri/image.js b/nym-wallet/.storybook/mocks/tauri/image.js new file mode 100644 index 0000000000..b3368e223a --- /dev/null +++ b/nym-wallet/.storybook/mocks/tauri/image.js @@ -0,0 +1,4 @@ +/** + * This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use + * or import dependencies that use Tauri. + */ \ No newline at end of file diff --git a/nym-wallet/.storybook/mocks/tauri/window.js b/nym-wallet/.storybook/mocks/tauri/webviewWindow.js similarity index 100% rename from nym-wallet/.storybook/mocks/tauri/window.js rename to nym-wallet/.storybook/mocks/tauri/webviewWindow.js diff --git a/nym-wallet/CHANGELOG.md b/nym-wallet/CHANGELOG.md index 707de916ae..a0471cbecb 100644 --- a/nym-wallet/CHANGELOG.md +++ b/nym-wallet/CHANGELOG.md @@ -2,6 +2,24 @@ ## [Unreleased] +## [v1.2.19] (2025-07-15) + +- Amended the buy section ([#5841]) +- Remove bity dir ([#5844]) + +[#5841]: https://github.com/nymtech/nym/pull/5841 +[#5844]: https://github.com/nymtech/nym/pull/5844 + +## [2025.7-tex] (2025-04-14) + +- Fix the mac build of the wallet ([#5684]) +- Allow copy and paste on logins fields for the wallet ([#5699]) +- Tauri V2 - Wallet Migration ([#5687]) + +[#5684]: https://github.com/nymtech/nym/pull/5684 +[#5699]: https://github.com/nymtech/nym/pull/5699 +[#5687]: https://github.com/nymtech/nym/pull/5687 + ## [2025.6-chuckles] (2025-04-01) - Wallet-revamp to be in line with new nym-theming ([#5653]) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 0352370e06..26ccca2b9a 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "NymWallet" -version = "1.2.18" +version = "1.2.19" dependencies = [ "async-trait", "base64 0.13.1", @@ -40,7 +40,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "strum 0.23.0", + "strum", "tap", "tauri", "tauri-build", @@ -415,11 +415,14 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" dependencies = [ + "brotli", "flate2", "futures-core", "memchr", "pin-project-lite", "tokio", + "zstd", + "zstd-safe", ] [[package]] @@ -659,7 +662,7 @@ dependencies = [ "rand_core 0.6.4", "ripemd", "secp256k1", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -816,7 +819,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", "tinyvec", ] @@ -931,7 +934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "toml 0.8.20", + "toml 0.8.22", ] [[package]] @@ -940,6 +943,8 @@ version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -1245,7 +1250,7 @@ dependencies = [ "p256", "rand_core 0.6.4", "rayon", - "sha2 0.10.8", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1303,7 +1308,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.8", + "sha2 0.10.9", "static_assertions", "thiserror 1.0.69", ] @@ -1327,10 +1332,16 @@ dependencies = [ ] [[package]] -name = "crossbeam-channel" -version = "0.5.14" +name = "critical-section" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1902,7 +1913,7 @@ dependencies = [ "ed25519", "rand_core 0.6.4", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -1918,7 +1929,7 @@ dependencies = [ "hashbrown 0.14.5", "hex", "rand_core 0.6.4", - "sha2 0.10.8", + "sha2 0.10.9", "zeroize", ] @@ -1957,7 +1968,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.20", + "toml 0.8.22", "vswhom", "winreg 0.52.0", ] @@ -2458,6 +2469,19 @@ dependencies = [ "x11", ] +[[package]] +name = "generator" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +dependencies = [ + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.58.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2517,18 +2541,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getset" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ghash" version = "0.5.1" @@ -2787,15 +2799,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.1" @@ -2849,57 +2852,59 @@ checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" [[package]] name = "hickory-proto" -version = "0.24.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +checksum = "6d844af74f7b799e41c78221be863bade11c430d46042c3b49ca8ae0c6d27287" dependencies = [ + "async-recursion", "async-trait", "bytes", "cfg-if", + "critical-section", "data-encoding", "enum-as-inner", "futures-channel", "futures-io", "futures-util", - "h2 0.3.26", - "http 0.2.12", + "h2 0.4.8", + "http 1.3.1", "idna", "ipnet", "once_cell", - "rand 0.8.5", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", - "thiserror 1.0.69", + "rand 0.9.0", + "ring", + "rustls 0.23.25", + "thiserror 2.0.12", "tinyvec", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.2", "tracing", "url", - "webpki-roots 0.25.4", + "webpki-roots", ] [[package]] name = "hickory-resolver" -version = "0.24.4" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +checksum = "a128410b38d6f931fcc6ca5c107a3b02cabd6c05967841269a4ad65d23c44331" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", - "lru-cache", + "moka", "once_cell", "parking_lot", - "rand 0.8.5", + "rand 0.9.0", "resolv-conf", - "rustls 0.21.12", + "rustls 0.23.25", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.2", "tracing", - "webpki-roots 0.25.4", + "webpki-roots", ] [[package]] @@ -3046,7 +3051,7 @@ dependencies = [ "httpdate", "itoa 1.0.15", "pin-project-lite", - "socket2", + "socket2 0.5.9", "tokio", "tower-service", "tracing", @@ -3102,7 +3107,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 0.26.8", + "webpki-roots", ] [[package]] @@ -3135,7 +3140,7 @@ dependencies = [ "hyper 1.6.0", "libc", "pin-project-lite", - "socket2", + "socket2 0.5.9", "tokio", "tower-service", "tracing", @@ -3379,13 +3384,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.9", "widestring", "windows-sys 0.48.0", "winreg 0.50.0", @@ -3508,6 +3524,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -3556,7 +3582,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2 0.10.8", + "sha2 0.10.9", "signature", ] @@ -3616,9 +3642,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -3641,12 +3667,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3685,12 +3705,16 @@ dependencies = [ ] [[package]] -name = "lru-cache" -version = "0.1.2" +name = "loom" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ - "linked-hash-map", + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", ] [[package]] @@ -3713,6 +3737,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matches" version = "0.1.10" @@ -3773,6 +3806,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "moka" +version = "0.12.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "loom", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "thiserror 1.0.69", + "uuid", +] + [[package]] name = "muda" version = "0.16.1" @@ -3876,6 +3928,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3948,28 +4010,31 @@ dependencies = [ "cosmrs", "cosmwasm-std", "ecdsa", - "getset", "hex", "humantime-serde", + "nym-coconut-dkg-common", "nym-compact-ecash", "nym-config", "nym-contracts-common", "nym-credentials-interface", "nym-crypto", + "nym-ecash-signer-check-types", "nym-ecash-time", "nym-mixnet-contract-common", "nym-network-defaults", "nym-node-requests", + "nym-noise-keys", "nym-serde-helpers", "nym-ticketbooks-merkle", "schemars", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "tendermint", "tendermint-rpc", "thiserror 2.0.12", "time", + "tracing", "utoipa", ] @@ -3979,9 +4044,10 @@ version = "0.6.0" dependencies = [ "const-str", "log", - "pretty_env_logger", "schemars", "serde", + "tracing", + "tracing-subscriber", "utoipa", "vergen", ] @@ -4015,7 +4081,7 @@ dependencies = [ "nym-pemstore", "rand 0.8.5", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "subtle", "thiserror 2.0.12", "zeroize", @@ -4031,7 +4097,7 @@ dependencies = [ "nym-network-defaults", "serde", "thiserror 2.0.12", - "toml 0.8.20", + "toml 0.8.22", "url", ] @@ -4059,7 +4125,8 @@ dependencies = [ "nym-network-defaults", "rand 0.8.5", "serde", - "strum 0.26.3", + "strum", + "strum_macros", "thiserror 2.0.12", "time", "utoipa", @@ -4095,6 +4162,21 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nym-ecash-signer-check-types" +version = "0.1.0" +dependencies = [ + "nym-coconut-dkg-common", + "nym-crypto", + "semver", + "serde", + "thiserror 2.0.12", + "time", + "tracing", + "url", + "utoipa", +] + [[package]] name = "nym-ecash-time" version = "0.1.0" @@ -4129,12 +4211,15 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bincode", "bytes", "encoding_rs", "hickory-resolver", "http 1.3.1", + "itertools 0.14.0", "mime", "nym-bin-common", + "nym-http-api-common", "once_cell", "reqwest 0.12.15", "serde", @@ -4145,6 +4230,16 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-http-api-common" +version = "0.1.0" +dependencies = [ + "bincode", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "nym-mixnet-contract-common" version = "0.6.0" @@ -4159,7 +4254,6 @@ dependencies = [ "schemars", "semver", "serde", - "serde-json-wasm", "serde_repr", "thiserror 2.0.12", "time", @@ -4186,7 +4280,7 @@ dependencies = [ name = "nym-network-defaults" version = "0.1.0" dependencies = [ - "cargo_metadata 0.18.1", + "cargo_metadata 0.19.2", "dotenvy", "log", "regex", @@ -4208,22 +4302,48 @@ dependencies = [ "nym-crypto", "nym-exit-policy", "nym-http-api-client", + "nym-noise-keys", "nym-wireguard-types", "schemars", "serde", "serde_json", - "strum 0.26.3", + "strum", + "strum_macros", "thiserror 2.0.12", "time", "utoipa", ] +[[package]] +name = "nym-noise-keys" +version = "0.1.0" +dependencies = [ + "nym-crypto", + "schemars", + "serde", + "utoipa", +] + [[package]] name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", "tracing", + "zeroize", +] + +[[package]] +name = "nym-performance-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "nym-contracts-common", + "schemars", + "serde", + "thiserror 2.0.12", ] [[package]] @@ -4268,7 +4388,7 @@ dependencies = [ "rs_merkle", "schemars", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "time", "utoipa", ] @@ -4293,8 +4413,9 @@ dependencies = [ "schemars", "serde", "serde_json", - "sha2 0.10.8", - "strum 0.26.3", + "sha2 0.10.9", + "strum", + "strum_macros", "thiserror 2.0.12", "ts-rs", "url", @@ -4333,13 +4454,14 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-performance-contract-common", "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.15", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "tendermint-rpc", "thiserror 2.0.12", "time", @@ -4373,7 +4495,6 @@ dependencies = [ "log", "nym-bin-common", "nym-store-cipher", - "pretty_env_logger", "serde_json", ] @@ -4392,7 +4513,8 @@ dependencies = [ "nym-vesting-contract-common", "serde", "serde_json", - "strum 0.23.0", + "strum", + "strum_macros", "ts-rs", ] @@ -4647,6 +4769,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -4668,9 +4794,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.9.0", "cfg-if", @@ -4700,9 +4826,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", @@ -4750,6 +4876,12 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "p256" version = "0.13.2" @@ -4759,7 +4891,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -4934,7 +5066,7 @@ checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -5193,6 +5325,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + [[package]] name = "powerfmt" version = "0.2.0" @@ -5258,7 +5396,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit 0.22.24", + "toml_edit 0.22.26", ] [[package]] @@ -5285,28 +5423,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -5388,7 +5504,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.25", - "socket2", + "socket2 0.5.9", "thiserror 2.0.12", "tokio", "tracing", @@ -5424,7 +5540,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.9", "tracing", "windows-sys 0.59.0", ] @@ -5620,8 +5736,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -5632,9 +5757,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -5730,7 +5861,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.8", + "webpki-roots", "windows-registry", ] @@ -5804,7 +5935,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21" dependencies = [ - "sha2 0.10.8", + "sha2 0.10.9", ] [[package]] @@ -5872,6 +6003,7 @@ version = "0.23.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -5997,6 +6129,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6299,15 +6437,24 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_child" version = "1.0.1" @@ -6386,6 +6533,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "softbuffer" version = "0.4.6" @@ -6489,45 +6646,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.23.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.23.1", -] - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", + "strum_macros", ] [[package]] name = "strum_macros" -version = "0.23.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "rustversion", "syn 2.0.100", ] @@ -6662,10 +6796,16 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.20", + "toml 0.8.22", "version-compare", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tao" version = "0.32.8" @@ -6807,7 +6947,7 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.8.20", + "toml 0.8.22", "walkdir", ] @@ -6828,7 +6968,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2 0.10.8", + "sha2 0.10.9", "syn 2.0.100", "tauri-utils", "thiserror 2.0.12", @@ -6865,7 +7005,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.8.20", + "toml 0.8.22", "walkdir", ] @@ -7047,7 +7187,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.12", - "toml 0.8.20", + "toml 0.8.22", "url", "urlpattern", "uuid", @@ -7061,7 +7201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56eaa45f707bedf34d19312c26d350bc0f3c59a47e58e8adbeecdc850d2c13a0" dependencies = [ "embed-resource", - "toml 0.8.20", + "toml 0.8.22", ] [[package]] @@ -7079,9 +7219,9 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.40.2" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bed57bd840305efef8e83b752dd63dde3bb0fc1c35b6490f0a4de3a0ffd8dd9" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" dependencies = [ "bytes", "digest 0.10.7", @@ -7098,7 +7238,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_repr", - "sha2 0.10.8", + "sha2 0.10.9", "signature", "subtle", "subtle-encoding", @@ -7109,23 +7249,23 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.40.2" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38aeb44e9e275b3afe31a4f3302dfc5aa1e0aa488a22abb7577634b22b144a22" +checksum = "069d1791f9b02a596abcd26eb72003b2e9906c6169a60fa82ffc080dd3a43fda" dependencies = [ "flex-error", "serde", "serde_json", "tendermint", - "toml 0.8.20", + "toml 0.8.22", "url", ] [[package]] name = "tendermint-proto" -version = "0.40.2" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a218320a6e92ac731ac094169cb742e3203a3ab24d30417b72cfdaab5d50c67" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" dependencies = [ "bytes", "flex-error", @@ -7138,9 +7278,9 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.40.2" +version = "0.40.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ffcf862b519894f8afa79e5be73e376c3367cbd2bbe0eaf9c653ab01cd8b20" +checksum = "35e0569a4b4cc42ff00df5a665be2858a39ff79df4790b176f1cd0e169bc0fc2" dependencies = [ "async-trait", "bytes", @@ -7235,6 +7375,16 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tiff" version = "0.9.1" @@ -7306,20 +7456,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7387,21 +7539,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.24", + "toml_edit 0.22.26", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] @@ -7430,17 +7582,24 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap 2.8.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.4", + "toml_write", + "winnow 0.7.10", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "tower" version = "0.5.2" @@ -7497,6 +7656,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -7757,6 +7946,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -8082,12 +8277,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "webpki-roots" version = "0.26.8" @@ -8108,7 +8297,7 @@ dependencies = [ "windows 0.60.0", "windows-core 0.60.1", "windows-implement 0.59.0", - "windows-interface", + "windows-interface 0.59.1", ] [[package]] @@ -8201,6 +8390,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.60.0" @@ -8232,6 +8431,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.60.1" @@ -8239,9 +8451,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" dependencies = [ "windows-implement 0.59.0", - "windows-interface", + "windows-interface 0.59.1", "windows-link", - "windows-result", + "windows-result 0.3.2", "windows-strings 0.3.1", ] @@ -8252,9 +8464,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", - "windows-interface", + "windows-interface 0.59.1", "windows-link", - "windows-result", + "windows-result 0.3.2", "windows-strings 0.4.0", ] @@ -8268,6 +8480,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "windows-implement" version = "0.59.0" @@ -8290,6 +8513,17 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -8323,11 +8557,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result", + "windows-result 0.3.2", "windows-strings 0.3.1", "windows-targets 0.53.0", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -8337,6 +8580,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.3.1" @@ -8653,9 +8906,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -8750,7 +9003,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2 0.10.8", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.12", @@ -8887,7 +9140,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.7.4", + "winnow 0.7.10", "xdg-home", "zbus_macros", "zbus_names", @@ -8917,7 +9170,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.4", + "winnow 0.7.10", "zvariant", ] @@ -9038,6 +9291,34 @@ dependencies = [ "memchr", ] +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.4.0" @@ -9048,7 +9329,7 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "winnow 0.7.4", + "winnow 0.7.10", "zvariant_derive", "zvariant_utils", ] @@ -9077,5 +9358,5 @@ dependencies = [ "serde", "static_assertions", "syn 2.0.100", - "winnow 0.7.4", + "winnow 0.7.10", ] diff --git a/nym-wallet/nym-wallet-recovery-cli/Cargo.toml b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml index a0523ae3c7..5d78d02d41 100644 --- a/nym-wallet/nym-wallet-recovery-cli/Cargo.toml +++ b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml @@ -11,8 +11,7 @@ base64 = "0.13" bip39 = { version = "2.0.0", features = ["zeroize"] } clap = { version = "4.0", features = ["derive"] } log = "0.4" -pretty_env_logger = "0.4" serde_json = "1.0.0" -nym-bin-common = { path = "../../common/bin-common" } -nym-store-cipher = { path = "../../common/store-cipher" } \ No newline at end of file +nym-bin-common = { path = "../../common/bin-common", features = ["basic_tracing"] } +nym-store-cipher = { path = "../../common/store-cipher" } diff --git a/nym-wallet/nym-wallet-recovery-cli/src/main.rs b/nym-wallet/nym-wallet-recovery-cli/src/main.rs index 813e53c376..0162eaa826 100644 --- a/nym-wallet/nym-wallet-recovery-cli/src/main.rs +++ b/nym-wallet/nym-wallet-recovery-cli/src/main.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Result}; use clap::Parser; -use nym_bin_common::logging::setup_logging; +use nym_bin_common::logging::setup_tracing_logger; use nym_store_cipher::{ Aes256Gcm, Algorithm, EncryptedData, KdfInfo, Params, StoreCipher, Version, ARGON2_SALT_SIZE, CURRENT_VERSION, @@ -52,7 +52,7 @@ enum ParseMode { } fn main() -> Result<()> { - setup_logging(); + setup_tracing_logger(); let args = Args::parse(); let file = File::open(args.file)?; let parse = if args.raw { diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index 83522fa191..d29f2467dd 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -9,7 +9,9 @@ license = "Apache-2.0" hex-literal = "0.3.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -strum = { version = "0.23", features = ["derive"] } +strum = { version = "0.27.2", features = ["derive"] } +strum_macros = "0.27.2" + ts-rs = "10.0.0" cosmwasm-std = "2.2.1" @@ -26,6 +28,3 @@ nym-types = { path = "../../common/types" } [features] default = [] generate-ts = ["nym-mixnet-contract-common/generate-ts", "nym-types/generate-ts"] - - - diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 97948a4248..b01c3768e3 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -5,9 +5,8 @@ use nym_config::defaults::{mainnet, DenomDetails, NymNetworkDetails}; use nym_types::{currency::DecCoin, error::TypesError}; use serde::{Deserialize, Serialize}; use std::{fmt, ops::Not, str::FromStr}; -use strum::EnumIter; +use strum_macros::EnumIter; -mod qa; mod sandbox; #[allow(clippy::upper_case_acronyms)] @@ -18,7 +17,6 @@ mod sandbox; )] #[derive(Copy, Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] pub enum Network { - QA, SANDBOX, MAINNET, } @@ -30,7 +28,6 @@ impl Network { pub fn mix_denom(&self) -> DenomDetails { match self { - Network::QA => qa::MIX_DENOM, Network::SANDBOX => sandbox::MIX_DENOM, Network::MAINNET => mainnet::MIX_DENOM, } @@ -38,7 +35,6 @@ impl Network { pub fn base_mix_denom(&self) -> &str { match self { - Network::QA => qa::MIX_DENOM.base, Network::SANDBOX => sandbox::MIX_DENOM.base, Network::MAINNET => mainnet::MIX_DENOM.base, } @@ -46,7 +42,6 @@ impl Network { pub fn display_mix_denom(&self) -> &str { match self { - Network::QA => qa::MIX_DENOM.display, Network::SANDBOX => sandbox::MIX_DENOM.display, Network::MAINNET => mainnet::MIX_DENOM.display, } @@ -72,7 +67,6 @@ impl fmt::Display for Network { impl From for NymNetworkDetails { fn from(network: Network) -> Self { match network { - Network::QA => qa::network_details(), Network::SANDBOX => sandbox::network_details(), Network::MAINNET => NymNetworkDetails::new_mainnet(), } @@ -84,7 +78,6 @@ impl FromStr for Network { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { - "qa" => Ok(Network::QA), "sandbox" => Ok(Network::SANDBOX), "mainnet" => Ok(Network::MAINNET), _ => Err(TypesError::UnknownNetwork(s.to_string())), diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs deleted file mode 100644 index 761d08f614..0000000000 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::parse_optional_str; -use nym_network_defaults::{ChainDetails, DenomDetails, NymContracts, ValidatorDetails}; - -// -- Chain details -- - -pub(crate) const NETWORK_NAME: &str = "qa"; - -pub(crate) const BECH32_PREFIX: &str = "n"; -pub(crate) const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); -pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); - -// -- Contract addresses -- - -pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = - "n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296"; -pub(crate) const VESTING_CONTRACT_ADDRESS: &str = - "n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj"; -pub(crate) const ECASH_CONTRACT_ADDRESS: &str = - "n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7"; -pub(crate) const GROUP_CONTRACT_ADDRESS: &str = - "n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t"; -pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = - "n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0"; -pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = - "n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr"; - -// -- Constructor functions -- - -pub(crate) fn validators() -> Vec { - vec![ValidatorDetails::new( - "https://qa-validator.qa.nymte.ch/", - Some("https://qa-nym-api.qa.nymte.ch/api"), - Some("wss://qa-validator.qa.nymte.ch/websocket"), - )] -} - -pub(crate) const EXPLORER_API: &str = "https://qa-network-explorer.qa.nymte.ch/api/"; - -pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { - nym_network_defaults::NymNetworkDetails { - network_name: NETWORK_NAME.into(), - chain_details: ChainDetails { - bech32_account_prefix: BECH32_PREFIX.to_string(), - mix_denom: MIX_DENOM.into(), - stake_denom: STAKE_DENOM.into(), - }, - endpoints: validators(), - contracts: NymContracts { - mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), - vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), - ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS), - group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), - multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), - coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), - }, - explorer_api: parse_optional_str(EXPLORER_API), - nym_vpn_api_url: None, - } -} diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index c436c9f09e..f8e66ebe3e 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -24,6 +24,10 @@ pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh"; +// \/ TODO: this has to be updated once the contract is deployed +pub(crate) const PERFORMANCE_CONTRACT_ADDRESS: &str = ""; +// /\ TODO: this has to be updated once the contract is deployed + // -- Constructor functions -- pub(crate) fn validators() -> Vec { @@ -34,8 +38,6 @@ pub(crate) fn validators() -> Vec { )] } -pub(crate) const EXPLORER_API: &str = "https://sandbox-explorer.nymtech.net/api/"; - pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { nym_network_defaults::NymNetworkDetails { network_name: NETWORK_NAME.into(), @@ -48,12 +50,14 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { contracts: NymContracts { mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), + performance_contract_address: parse_optional_str(PERFORMANCE_CONTRACT_ADDRESS), ecash_contract_address: parse_optional_str(ECASH_CONTRACT_ADDRESS), group_contract_address: parse_optional_str(GROUP_CONTRACT_ADDRESS), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, - explorer_api: parse_optional_str(EXPLORER_API), nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, } } diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 732346df21..6702636f1c 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.2.18", + "version": "1.2.19", "license": "MIT", "main": "index.js", "scripts": { @@ -132,4 +132,4 @@ "webpack-merge": "^5.8.0" }, "private": false -} +} \ No newline at end of file diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index dadfa64d89..7ea924a9e8 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "NymWallet" -version = "1.2.18" +version = "1.2.19" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" @@ -39,7 +39,7 @@ reqwest = { version = "0.12.4", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" -strum = { version = "0.23", features = ["derive"] } +strum = { version = "0.27.2", features = ["derive"] } tap = "1" tauri = { version = "2", features = [] } #tendermint-rpc = "0.23.0" diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 418e07eda9..b5c1ccdfba 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -142,11 +142,11 @@ impl Config { let location = Self::config_file_path(None); match toml::to_string_pretty(&global) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + .map_err(io::Error::other) .map(|toml| fs::write(location.clone(), toml)) { - Ok(_) => log::debug!("Writing to: {:#?}", location), - Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + Ok(_) => log::debug!("Writing to: {location:#?}"), + Err(err) => log::warn!("Failed to write to {location:#?}: {err}"), } } @@ -162,11 +162,11 @@ impl Config { let location = Self::config_file_path(Some(network)); match toml::to_string_pretty(config) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + .map_err(io::Error::other) .map(|toml| fs::write(location.clone(), toml)) { - Ok(_) => log::debug!("Writing to: {:#?}", location), - Err(err) => log::warn!("Failed to write to {:#?}: {err}", location), + Ok(_) => log::debug!("Writing to: {location:#?}"), + Err(err) => log::warn!("Failed to write to {location:#?}: {err}"), } } Ok(()) @@ -178,11 +178,11 @@ impl Config { let file = Self::config_file_path(None); match load_from_file::(file.clone()) { Ok(global) => { - log::debug!("Loaded from file {:#?}", file); + log::debug!("Loaded from file {file:#?}"); Some(global) } Err(err) => { - log::trace!("Not loading {:#?}: {err}", file); + log::trace!("Not loading {file:#?}: {err}"); None } } @@ -194,10 +194,10 @@ impl Config { let file = Self::config_file_path(Some(network)); match load_from_file::(file.clone()) { Ok(config) => { - log::trace!("Loaded from file {:#?}", file); + log::trace!("Loaded from file {file:#?}"); networks.insert(network.as_key(), config); } - Err(err) => log::trace!("Not loading {:#?}: {err}", file), + Err(err) => log::trace!("Not loading {file:#?}: {err}"), }; } @@ -367,10 +367,8 @@ fn load_from_file(file: PathBuf) -> Result where T: DeserializeOwned, { - fs::read_to_string(file).and_then(|contents| { - toml::from_str::(&contents) - .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) - }) + fs::read_to_string(file) + .and_then(|contents| toml::from_str::(&contents).map_err(io::Error::other)) } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -443,7 +441,6 @@ impl OptionalValidators { match network { WalletNetwork::MAINNET => self.mainnet.as_ref(), WalletNetwork::SANDBOX => self.sandbox.as_ref(), - WalletNetwork::QA => self.qa.as_ref(), } .into_iter() .flatten() @@ -462,12 +459,7 @@ impl fmt::Display for OptionalValidators { .as_ref() .map(|validators| format!(",\nsandbox: [\n{}\n]", validators.iter().format("\n"))) .unwrap_or_default(); - let s3 = self - .qa - .as_ref() - .map(|validators| format!(",\nqa: [\n{}\n]", validators.iter().format("\n"))) - .unwrap_or_default(); - write!(f, "{s1}{s2}{s3}") + write!(f, "{s1}{s2}") } } diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index e90d362ca6..3e3123e088 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -32,7 +32,7 @@ pub enum BackendError { NyxdError { pretty_error: String, #[source] - source: NyxdError, + source: Box, }, #[error(transparent)] CosmwasmStd { @@ -192,18 +192,18 @@ impl From for BackendError { if let Some(pretty_log) = pretty_log { Self::NyxdError { pretty_error: pretty_log.to_string(), - source, + source: Box::new(source), } } else { Self::NyxdError { pretty_error: source.to_string(), - source, + source: Box::new(source), } } } nyxd_error => Self::NyxdError { pretty_error: nyxd_error.to_string(), - source: nyxd_error, + source: Box::new(nyxd_error), }, } } diff --git a/nym-wallet/src-tauri/src/network_config.rs b/nym-wallet/src-tauri/src/network_config.rs index 6f75b6e5fa..c0f14a65bd 100644 --- a/nym-wallet/src-tauri/src/network_config.rs +++ b/nym-wallet/src-tauri/src/network_config.rs @@ -33,7 +33,7 @@ pub async fn get_selected_nyxd_url( ) -> Result, BackendError> { let state = state.read().await; let url = state.get_selected_nyxd_url(&network).map(String::from); - log::info!("Selected nyxd url for {network}: {:?}", url); + log::info!("Selected nyxd url for {network}: {url:?}"); Ok(url) } @@ -44,7 +44,7 @@ pub async fn get_default_nyxd_url( ) -> Result { let state = state.read().await; let url = state.get_default_nyxd_url(&network).map(String::from); - log::info!("Default nyxd url for {network}: {:?}", url); + log::info!("Default nyxd url for {network}: {url:?}"); url.ok_or_else(|| BackendError::WalletNoDefaultValidator) } diff --git a/nym-wallet/src-tauri/src/operations/app/link.rs b/nym-wallet/src-tauri/src/operations/app/link.rs index fa41aa8f19..d238242633 100644 --- a/nym-wallet/src-tauri/src/operations/app/link.rs +++ b/nym-wallet/src-tauri/src/operations/app/link.rs @@ -2,10 +2,10 @@ use tauri_plugin_opener::OpenerExt; #[tauri::command] pub async fn open_url(url: String, app_handle: tauri::AppHandle) -> Result<(), String> { - println!("Opening URL: {}", url); + println!("Opening URL: {url}"); match app_handle.opener().open_url(&url, None::<&str>) { Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to open URL: {}", err)), + Err(err) => Err(format!("Failed to open URL: {err}")), } } diff --git a/nym-wallet/src-tauri/src/operations/app/version.rs b/nym-wallet/src-tauri/src/operations/app/version.rs index d904449cf0..d69a80f5e3 100644 --- a/nym-wallet/src-tauri/src/operations/app/version.rs +++ b/nym-wallet/src-tauri/src/operations/app/version.rs @@ -10,13 +10,13 @@ pub async fn check_version(handle: tauri::AppHandle) -> Result>> Getting app version info"); let updater = handle.updater().map_err(|e| { - log::error!("Failed to get updater: {}", e); + log::error!("Failed to get updater: {e}"); BackendError::CheckAppVersionError })?; // Then check for updates let update_info = updater.check().await.map_err(|e| { - log::error!("An error occurred while checking for app update {}", e); + log::error!("An error occurred while checking for app update {e}"); BackendError::CheckAppVersionError })?; @@ -35,10 +35,7 @@ pub async fn check_version(handle: tauri::AppHandle) -> Result Result<(), BackendError> { // create the new window first, to stop the app process from exiting - log::info!("Creating {} window...", new_window_label); + log::info!("Creating {new_window_label} window..."); match tauri::WebviewWindowBuilder::new( &app_handle, new_window_label, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 3750e21aee..3319363be1 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -108,7 +108,7 @@ async fn _connect_with_mnemonic( "{}", state.get_config_validator_entries(network).format(",\n") ); - log::debug!("List of validators for {network}: [\n{}\n]", f,); + log::debug!("List of validators for {network}: [\n{f}\n]",); } state.config().clone() @@ -598,7 +598,7 @@ pub async fn list_accounts( address: account.addresses[&network].to_string(), }) .map(|account| { - log::trace!("{:?}", account); + log::trace!("{account:?}"); account }) .collect(); diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index ee9e0e3742..881b54aa3c 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -71,7 +71,7 @@ pub async fn bond_gateway( .bond_gateway(gateway, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -84,10 +84,10 @@ pub async fn unbond_gateway( ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!(">>> Unbond gateway, fee = {:?}", fee); + log::info!(">>> Unbond gateway, fee = {fee:?}"); let res = guard.current_client()?.nyxd.unbond_gateway(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -136,7 +136,7 @@ pub async fn bond_mixnode( .bond_mixnode(mixnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -184,7 +184,7 @@ pub async fn bond_nymnode( .bond_nymnode(nymnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -232,7 +232,7 @@ pub async fn update_pledge( }; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, @@ -249,10 +249,7 @@ pub async fn pledge_more( let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Pledge more, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", - additional_pledge, - additional_pledge_base, - fee, + ">>> Pledge more, additional_pledge_display = {additional_pledge}, additional_pledge_base = {additional_pledge_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -260,7 +257,7 @@ pub async fn pledge_more( .pledge_more(additional_pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -276,10 +273,7 @@ pub async fn decrease_pledge( let decrease_by_base = guard.attempt_convert_to_base_coin(decrease_by.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Decrease pledge, pledge_decrease_display = {}, pledge_decrease_base = {}, fee = {:?}", - decrease_by, - decrease_by_base, - fee, + ">>> Decrease pledge, pledge_decrease_display = {decrease_by}, pledge_decrease_base = {decrease_by_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -287,7 +281,7 @@ pub async fn decrease_pledge( .decrease_pledge(decrease_by_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -300,10 +294,10 @@ pub async fn unbond_mixnode( ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!(">>> Unbond mixnode, fee = {:?}", fee); + log::info!(">>> Unbond mixnode, fee = {fee:?}"); let res = guard.current_client()?.nyxd.unbond_mixnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -319,7 +313,7 @@ pub async fn unbond_nymnode( log::info!(">>> Unbond NymNode, fee = {fee:?}"); let res = guard.current_client()?.nyxd.unbond_nymnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, @@ -347,7 +341,7 @@ pub async fn update_mixnode_cost_params( .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -372,7 +366,7 @@ pub async fn update_mixnode_config( .update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -397,7 +391,7 @@ pub async fn update_gateway_config( .update_gateway_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -420,14 +414,14 @@ pub async fn get_mixnode_avg_uptime( match res.mixnode_details { Some(details) => { let id = details.mix_id(); - log::trace!(" >>> Get average uptime percentage: mix_id = {}", id); + log::trace!(" >>> Get average uptime percentage: mix_id = {id}"); let avg_uptime_percent = client .nym_api .get_mixnode_avg_uptime(id) .await .ok() .map(|r| r.avg_uptime); - log::trace!(" <<< {:?}", avg_uptime_percent); + log::trace!(" <<< {avg_uptime_percent:?}"); Ok(avg_uptime_percent) } None => Ok(None), @@ -461,7 +455,7 @@ pub async fn mixnode_bond_details( &r.bond_information.mix_node.identity_key )) ); - log::trace!("<<< {:?}", details); + log::trace!("<<< {details:?}"); Ok(details) } @@ -490,7 +484,7 @@ pub async fn gateway_bond_details( "<<< identity_key = {:?}", res.as_ref().map(|r| r.gateway.identity_key.to_string()) ); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(res) } @@ -521,7 +515,7 @@ pub async fn nym_node_bond_details( &r.bond_information.node.identity_key )) ); - log::trace!("<<< {:?}", details); + log::trace!("<<< {details:?}"); Ok(details) } @@ -530,7 +524,7 @@ pub async fn get_pending_operator_rewards( address: String, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!(">>> Get pending operator rewards for {}", address); + log::info!(">>> Get pending operator rewards for {address}"); let guard = state.read().await; let res = guard .current_client()? @@ -554,11 +548,7 @@ pub async fn get_pending_operator_rewards( .transpose()? .unwrap_or_else(|| guard.default_zero_mix_display_coin()); - log::info!( - "<<< rewards_base = {:?}, rewards_display = {}", - base_coin, - display_coin - ); + log::info!("<<< rewards_base = {base_coin:?}, rewards_display = {display_coin}"); Ok(display_coin) } @@ -637,7 +627,7 @@ pub async fn migrate_legacy_mixnode( let res = client.nyxd.migrate_legacy_mixnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -657,7 +647,7 @@ pub async fn migrate_legacy_gateway( let res = client.nyxd.migrate_legacy_gateway(None, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -678,7 +668,7 @@ pub async fn update_nymnode_config( .update_nymnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -705,7 +695,7 @@ pub async fn update_nymnode_cost_params( .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 8fc47187d0..5275436dcd 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -62,10 +62,7 @@ pub async fn get_pending_delegation_events( "<<< {} pending delegation events", client_specific_events.len() ); - log::trace!( - "<<< pending delegation events = {:?}", - client_specific_events - ); + log::trace!("<<< pending delegation events = {client_specific_events:?}"); Ok(client_specific_events) } @@ -83,15 +80,11 @@ pub async fn delegate_to_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode: mix_id = {}, display_amount = {}, base_amount = {}, fee = {:?}", - mix_id, - amount, - delegation_base, - fee, + ">>> Delegate to mixnode: mix_id = {mix_id}, display_amount = {amount}, base_amount = {delegation_base}, fee = {fee:?}", ); let res = client.nyxd.delegate(mix_id, delegation_base, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -106,14 +99,10 @@ pub async fn undelegate_from_mixnode( let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!( - ">>> Undelegate from mixnode: mix_id = {}, fee = {:?}", - mix_id, - fee - ); + log::info!(">>> Undelegate from mixnode: mix_id = {mix_id}, fee = {fee:?}"); let res = guard.current_client()?.nyxd.undelegate(mix_id, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -128,11 +117,7 @@ pub async fn undelegate_all_from_mixnode( state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( - ">>> Undelegate all from mixnode: mix_id = {}, uses_vesting_contract_tokens = {}, fee_liquid = {:?}, fee_vesting = {:?}", - mix_id, - uses_vesting_contract_tokens, - fee_liquid, - fee_vesting, + ">>> Undelegate all from mixnode: mix_id = {mix_id}, uses_vesting_contract_tokens = {uses_vesting_contract_tokens}, fee_liquid = {fee_liquid:?}, fee_vesting = {fee_vesting:?}", ); let mut res: Vec = vec![undelegate_from_mixnode(mix_id, fee_liquid, state.clone()).await?]; @@ -178,7 +163,7 @@ pub(crate) async fn get_node_information( let str_err = format!( "Failed to get legacy mixnode details for node_id = {node_id}. Error: {err}", ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); })? .mixnode_details; @@ -222,7 +207,7 @@ pub async fn get_all_mix_delegations( .get_all_delegator_delegations(&address) .await .tap_err(|err| { - log::error!(" <<< Failed to get delegations. Error: {}", err); + log::error!(" <<< Failed to get delegations. Error: {err}"); })?; log::info!(" <<< {} delegations", delegations.len()); @@ -230,7 +215,7 @@ pub async fn get_all_mix_delegations( get_pending_delegation_events(state.clone()) .await .tap_err(|err| { - log::error!(" <<< Failed to get pending delegations. Error: {}", err); + log::error!(" <<< Failed to get pending delegations. Error: {err}"); })?; log::info!( @@ -276,7 +261,7 @@ pub async fn get_all_mix_delegations( "Failed to get operator rewards as a display coin for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); @@ -295,7 +280,7 @@ pub async fn get_all_mix_delegations( "Failed to get delegator rewards as a display coin for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); @@ -314,12 +299,12 @@ pub async fn get_all_mix_delegations( "Failed to mixnode cost params for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); - log::trace!(" >>> Get accumulated rewards: address = {}", address); + log::trace!(" >>> Get accumulated rewards: address = {address}"); let pending_reward = client .nyxd .get_pending_delegator_reward(&address, d.mix_id, d.proxy.clone()) @@ -329,7 +314,7 @@ pub async fn get_all_mix_delegations( "Failed to get accumulated rewards for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or_default(); @@ -340,15 +325,11 @@ pub async fn get_all_mix_delegations( .attempt_convert_to_display_dec_coin(reward.clone().into()) .tap_err(|err| { let str_err = format!("Failed to get convert reward to a display coin for mix_id = {}. Error: {}", d.mix_id, err); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .ok(); - log::trace!( - " <<< rewards = {:?}, amount = {:?}", - pending_reward, - amount - ); + log::trace!(" <<< rewards = {pending_reward:?}, amount = {amount:?}"); amount } None => { @@ -367,7 +348,7 @@ pub async fn get_all_mix_delegations( "Failed to get stake saturation for mix_id = {}. Error: {}", d.mix_id, err ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .unwrap_or(MixStakeSaturationResponse { @@ -375,7 +356,7 @@ pub async fn get_all_mix_delegations( uncapped_saturation: None, current_saturation: None, }); - log::trace!(" <<< {:?}", stake_saturation); + log::trace!(" <<< {stake_saturation:?}"); log::trace!( " >>> Get average uptime percentage: mix_iid = {}", @@ -391,7 +372,7 @@ pub async fn get_all_mix_delegations( "Failed to get current node performance for node_id = {}. Error: {err}", d.mix_id ); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); }) .ok() @@ -400,7 +381,7 @@ pub async fn get_all_mix_delegations( // convert to old u8 let current_uptime = current_performance.map(|p| (p * 100.) as u8); - log::trace!(" <<< {:?}", current_uptime); + log::trace!(" <<< {current_uptime:?}"); log::trace!( " >>> Convert delegated on block height to timestamp: block_height = {}", @@ -415,19 +396,17 @@ pub async fn get_all_mix_delegations( // Check if the error is related to height not being available (pruning) if error_message.contains("height") && error_message.contains("not available") { let str_err = "Due to pruning strategies from validators, please navigate to the Settings tab and change your RPC node for your validator to retrieve your delegations."; - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err.to_string()); } else { let str_err = format!("Failed to get block timestamp for height = {} for delegation to mix_id = {}. Error: {}", d.height, d.mix_id, err); - log::error!(" <<< {}", str_err); + log::error!(" <<< {str_err}"); error_strings.push(str_err); } }).ok(); let delegated_on_iso_datetime = timestamp.map(|ts| ts.to_rfc3339()); log::trace!( - " <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}", - timestamp, - delegated_on_iso_datetime + " <<< timestamp = {timestamp:?}, delegated_on_iso_datetime = {delegated_on_iso_datetime:?}" ); let pending_events = filter_pending_events(d.mix_id, &pending_events_for_account); @@ -466,7 +445,7 @@ pub async fn get_all_mix_delegations( }, }) } - log::trace!("<<< {:?}", with_everything); + log::trace!("<<< {with_everything:?}"); Ok(with_everything) } @@ -490,11 +469,7 @@ pub async fn get_pending_delegator_rewards( proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!( - ">>> Get pending delegator rewards: mix_id = {}, proxy = {:?}", - mix_id, - proxy - ); + log::info!(">>> Get pending delegator rewards: mix_id = {mix_id}, proxy = {proxy:?}"); let guard = state.read().await; let res = guard .current_client()? @@ -518,11 +493,7 @@ pub async fn get_pending_delegator_rewards( .transpose()? .unwrap_or_else(|| guard.default_zero_mix_display_coin()); - log::info!( - "<<< rewards_base = {:?}, rewards_display = {}", - base_coin, - display_coin - ); + log::info!("<<< rewards_base = {base_coin:?}, rewards_display = {display_coin}"); Ok(display_coin) } @@ -554,7 +525,7 @@ pub async fn get_delegation_summary( total_delegations, total_rewards ); - log::trace!("<<< {:?}", delegations); + log::trace!("<<< {delegations:?}"); Ok(DelegationsSummaryResponse { delegations, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/interval.rs b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs index 05151d8a53..8268f44242 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/interval.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs @@ -14,7 +14,7 @@ pub async fn get_current_interval( ) -> Result { log::info!(">>> Get current interval"); let res = nyxd_client!(state).get_current_interval_details().await?; - log::info!("<<< current interval = {:?}", res); + log::info!("<<< current interval = {res:?}"); Ok(res.interval.into()) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 4563005b88..b32a4224b3 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -23,7 +23,7 @@ pub async fn claim_operator_reward( .withdraw_operator_reward(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -44,7 +44,7 @@ pub async fn claim_delegator_reward( .withdraw_delegator_reward(node_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -87,9 +87,7 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let did_delegate_with_vesting_contract = vesting_delegation.delegation.is_some(); log::trace!( - "<<< Delegations done with: mixnet contract = {}, vesting contract = {}", - did_delegate_with_mixnet_contract, - did_delegate_with_vesting_contract + "<<< Delegations done with: mixnet contract = {did_delegate_with_mixnet_contract}, vesting contract = {did_delegate_with_vesting_contract}" ); let mut res: Vec = vec![]; @@ -99,7 +97,7 @@ pub async fn claim_locked_and_unlocked_delegator_reward( if did_delegate_with_vesting_contract { res.push(vesting_claim_delegator_reward(node_id, fee, state).await?); } - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/send.rs b/nym-wallet/src-tauri/src/operations/mixnet/send.rs index 9e1ae7849c..5a313608ca 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -20,12 +20,7 @@ pub async fn send( let from_address = guard.current_client()?.nyxd.address().to_string(); let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Send: display_amount = {}, base_amount = {}, from = {}, to = {}, fee = {:?}", - amount, - amount_base, - from_address, - to_address, - fee, + ">>> Send: display_amount = {amount}, base_amount = {amount_base}, from = {from_address}, to = {to_address}, fee = {fee:?}", ); let raw_res = guard .current_client()? @@ -38,6 +33,6 @@ pub async fn send( TransactionDetails::new(amount, from_address, to_address.to_string()), fee_amount, ); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs index 59da5cae36..e93edd57f4 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/sign.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -40,7 +40,7 @@ pub async fn sign( signature_as_hex: signature.to_string(), }; let output_json = json!(output).to_string(); - log::info!(">>> Signing data {}", output_json); + log::info!(">>> Signing data {output_json}"); Ok(output_json) } @@ -48,17 +48,17 @@ async fn get_pubkey_from_account_address( address: &AccountId, state: &tauri::State<'_, WalletState>, ) -> Result { - log::info!("Getting public key for address {} from chain...", address); + log::info!("Getting public key for address {address} from chain..."); let guard = state.read().await; let client = guard.current_client()?; let account = client.nyxd.get_account(address).await?.ok_or_else(|| { - log::error!("No account associated with address {}", address); + log::error!("No account associated with address {address}"); BackendError::SignatureError(format!("No account associated with address {address}")) })?; let base_account = account.try_get_base_account()?; base_account.pubkey.ok_or_else(|| { - log::error!("No pubkey found for address {}", address); + log::error!("No pubkey found for address {address}"); BackendError::SignatureError(format!("No pubkey found for address {address}")) }) } @@ -125,7 +125,7 @@ pub async fn verify( )); } - log::info!("<<< Verifying signature [{}]", signature_as_hex); + log::info!("<<< Verifying signature [{signature_as_hex}]"); let verifying_key = VerifyingKey::from_sec1_bytes(&public_key.to_bytes())?; let signature = Signature::from_str(&signature_as_hex)?; let message_as_bytes = message.into_bytes(); diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index eb1b4bb1cf..3866062e2f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -114,17 +114,11 @@ pub async fn simulate_update_pledge( match new_pledge.amount.cmp(¤t_pledge.amount) { Ordering::Greater => { - log::info!( - "Simulate pledge increase, calculated additional pledge {}", - dec_delta, - ); + log::info!("Simulate pledge increase, calculated additional pledge {dec_delta}",); simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(dec_delta), &state).await } Ordering::Less => { - log::info!( - "Simulate pledge reduction, calculated reduction pledge {}", - dec_delta, - ); + log::info!("Simulate pledge reduction, calculated reduction pledge {dec_delta}",); simulate_mixnet_operation( ExecuteMsg::DecreasePledge { decrease_by: guard.attempt_convert_to_base_coin(dec_delta)?.into(), diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index 9578575c3f..28752f4bad 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -114,8 +114,7 @@ pub async fn simulate_vesting_update_pledge( })? .into(); log::info!( - ">>> Simulate pledge more, calculated additional pledge {}", - additional_pledge, + ">>> Simulate pledge more, calculated additional pledge {additional_pledge}", ); simulate_vesting_operation( ExecuteMsg::PledgeMore { @@ -134,8 +133,7 @@ pub async fn simulate_vesting_update_pledge( })? .into(); log::info!( - ">>> Simulate decrease pledge, calculated decrease pledge {}", - decrease_pledge, + ">>> Simulate decrease pledge, calculated decrease pledge {decrease_pledge}", ); simulate_vesting_operation( ExecuteMsg::DecreasePledge { diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index dfa7aec1f9..9a97b2bfe6 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -48,7 +48,7 @@ pub async fn vesting_bond_gateway( .vesting_bond_gateway(gateway, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -61,13 +61,10 @@ pub async fn vesting_unbond_gateway( ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!( - ">>> Unbond gateway bonded with locked tokens, fee = {:?}", - fee - ); + log::info!(">>> Unbond gateway bonded with locked tokens, fee = {fee:?}"); let res = nyxd_client!(state).vesting_unbond_gateway(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -118,7 +115,7 @@ pub async fn vesting_bond_mixnode( .vesting_bond_mixnode(mixnode, cost_params, msg_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -144,9 +141,7 @@ pub async fn vesting_update_pledge( let res = match new_pledge.amount.cmp(¤t_pledge.amount) { Ordering::Greater => { log::info!( - "Pledge increase with locked tokens, calculated additional pledge {}, fee = {:?}", - dec_delta, - fee, + "Pledge increase with locked tokens, calculated additional pledge {dec_delta}, fee = {fee:?}", ); guard .current_client()? @@ -156,9 +151,7 @@ pub async fn vesting_update_pledge( } Ordering::Less => { log::info!( - "Pledge reduction with locked tokens, calculated reduction pledge {}, fee = {:?}", - dec_delta, - fee, + "Pledge reduction with locked tokens, calculated reduction pledge {dec_delta}, fee = {fee:?}", ); guard .current_client()? @@ -170,7 +163,7 @@ pub async fn vesting_update_pledge( }; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, @@ -187,10 +180,7 @@ pub async fn vesting_pledge_more( let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Pledge more with locked tokens, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", - additional_pledge, - additional_pledge_base, - fee, + ">>> Pledge more with locked tokens, additional_pledge_display = {additional_pledge}, additional_pledge_base = {additional_pledge_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -198,7 +188,7 @@ pub async fn vesting_pledge_more( .vesting_pledge_more(additional_pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -214,10 +204,7 @@ pub async fn vesting_decrease_pledge( let decrease_by_base = guard.attempt_convert_to_base_coin(decrease_by.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Decrease pledge with locked tokens, pledge_decrease_display = {}, pledge_decrease_base = {}, fee = {:?}", - decrease_by, - decrease_by_base, - fee, + ">>> Decrease pledge with locked tokens, pledge_decrease_display = {decrease_by}, pledge_decrease_base = {decrease_by_base}, fee = {fee:?}", ); let res = guard .current_client()? @@ -225,7 +212,7 @@ pub async fn vesting_decrease_pledge( .vesting_decrease_pledge(decrease_by_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -238,17 +225,14 @@ pub async fn vesting_unbond_mixnode( ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - log::info!( - ">>> Unbond mixnode bonded with locked tokens, fee = {:?}", - fee - ); + log::info!(">>> Unbond mixnode bonded with locked tokens, fee = {fee:?}"); let res = guard .current_client()? .nyxd .vesting_unbond_mixnode(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -265,10 +249,7 @@ pub async fn withdraw_vested_coins( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Withdraw vested liquid coins: amount_base = {}, amount_base = {}, fee = {:?}", - amount, - amount_base, - fee + ">>> Withdraw vested liquid coins: amount_base = {amount}, amount_base = {amount_base}, fee = {fee:?}" ); let res = guard .current_client()? @@ -276,7 +257,7 @@ pub async fn withdraw_vested_coins( .withdraw_vested_coins(amount_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -304,7 +285,7 @@ pub async fn vesting_update_mixnode_cost_params( .vesting_update_mixnode_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -329,7 +310,7 @@ pub async fn vesting_update_mixnode_config( .vesting_update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -354,7 +335,7 @@ pub async fn vesting_update_gateway_config( .vesting_update_gateway_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index bbb1d8df11..ed896c0322 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -20,11 +20,7 @@ pub async fn vesting_delegate_to_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode with locked tokens: mix_id = {}, amount_display = {}, amount_base = {}, fee = {:?}", - mix_id, - amount, - delegation, - fee + ">>> Delegate to mixnode with locked tokens: mix_id = {mix_id}, amount_display = {amount}, amount_base = {delegation}, fee = {fee:?}" ); let res = guard .current_client()? @@ -32,7 +28,7 @@ pub async fn vesting_delegate_to_mixnode( .vesting_delegate_to_mixnode(mix_id, delegation, None, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -47,9 +43,7 @@ pub async fn vesting_undelegate_from_mixnode( let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Undelegate from mixnode delegated with locked tokens: mix_id = {}, fee = {:?}", - mix_id, - fee, + ">>> Undelegate from mixnode delegated with locked tokens: mix_id = {mix_id}, fee = {fee:?}", ); let res = guard .current_client()? @@ -57,7 +51,7 @@ pub async fn vesting_undelegate_from_mixnode( .vesting_undelegate_from_mixnode(mix_id, None, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index 00257464b1..6d99641f87 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -25,7 +25,7 @@ pub async fn migrate_vested_mixnode( .migrate_vested_mixnode(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -52,7 +52,7 @@ pub async fn migrate_vested_delegations( .get_all_delegator_delegations(&address) .await .inspect_err(|err| { - log::error!(" <<< Failed to get delegations. Error: {}", err); + log::error!(" <<< Failed to get delegations. Error: {err}"); })?; log::info!(" <<< {} delegations", delegations.len()); @@ -91,6 +91,6 @@ pub async fn migrate_vested_delegations( .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result(res, None)?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index f3377d5ed5..750d03caee 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -146,7 +146,7 @@ pub(crate) async fn vesting_start_time( .vesting_start_time(vesting_account_address) .await? .seconds(); - log::info!("<<< vesting start time = {}", res); + log::info!("<<< vesting start time = {res}"); Ok(res) } @@ -160,7 +160,7 @@ pub(crate) async fn vesting_end_time( .vesting_end_time(vesting_account_address) .await? .seconds(); - log::info!("<<< vesting end time = {}", res); + log::info!("<<< vesting end time = {res}"); Ok(res) } @@ -180,7 +180,7 @@ pub(crate) async fn original_vesting( .await?; let res = OriginalVestingResponse::from_vesting_contract(res, reg)?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -347,7 +347,7 @@ pub(crate) async fn vesting_get_mixnode_pledge( .map(|pledge| PledgeData::from_vesting_contract(pledge, reg)) .transpose()?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -368,7 +368,7 @@ pub(crate) async fn vesting_get_gateway_pledge( .map(|pledge| PledgeData::from_vesting_contract(pledge, reg)) .transpose()?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -381,7 +381,7 @@ pub(crate) async fn get_current_vesting_period( let res = nyxd_client!(state) .get_current_vesting_period(address) .await?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } @@ -397,6 +397,6 @@ pub(crate) async fn get_account_info( let vesting_account = guard.current_client()?.nyxd.get_account(address).await?; let res = VestingAccountInfo::from_vesting_contract(vesting_account, res)?; - log::info!("<<< {:?}", res); + log::info!("<<< {res:?}"); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index 7cf2155cdb..f41fee9055 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -22,7 +22,7 @@ pub async fn vesting_claim_operator_reward( .vesting_withdraw_operator_reward(None) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) @@ -34,10 +34,7 @@ pub async fn vesting_claim_delegator_reward( fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!( - ">>> Vesting account: claim delegator reward: mix_id = {}", - mix_id - ); + log::info!(">>> Vesting account: claim delegator reward: mix_id = {mix_id}"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard @@ -46,7 +43,7 @@ pub async fn vesting_claim_delegator_reward( .vesting_withdraw_delegator_reward(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); + log::trace!("<<< {res:?}"); Ok(TransactionExecuteResult::from_execute_result( res, fee_amount, )?) diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index 973172f4fc..42f9f080fe 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -286,7 +286,7 @@ fn remove_login_at_file(filepath: &Path, id: &LoginId) -> Result<(), BackendErro let mut stored_wallet = load_existing_wallet_at_file(filepath)?; if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); + log::info!("Removing file: {filepath:#?}"); return Ok(fs::remove_file(filepath)?); } @@ -295,7 +295,7 @@ fn remove_login_at_file(filepath: &Path, id: &LoginId) -> Result<(), BackendErro .ok_or(BackendError::WalletNoSuchLoginId)?; if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); + log::info!("Removing file: {filepath:#?}"); Ok(fs::remove_file(filepath)?) } else { write_to_file(filepath, &stored_wallet) @@ -345,7 +345,7 @@ fn _archive_wallet_file(path: &Path) -> Result<(), BackendError> { additional_number += 1; } else { if let Some(new_path) = new_path.to_str() { - log::info!("Archived to: {}", new_path); + log::info!("Archived to: {new_path}"); } else { log::warn!("Archived wallet file to filename that is not a valid UTF-8 string"); } @@ -363,17 +363,14 @@ pub(crate) fn archive_wallet_file() -> Result<(), BackendError> { if filepath.exists() { if let Some(filepath) = filepath.to_str() { - log::info!("Archiving wallet file: {}", filepath); + log::info!("Archiving wallet file: {filepath}"); } else { log::info!("Archiving wallet file"); } _archive_wallet_file(&filepath) } else { if let Some(filepath) = filepath.to_str() { - log::info!( - "Skipping archiving wallet file, as it's not found: {}", - filepath - ); + log::info!("Skipping archiving wallet file, as it's not found: {filepath}"); } else { log::info!("Skipping archiving wallet file, as it's not found"); } @@ -430,7 +427,7 @@ fn remove_account_from_login_at_file( // Remove the file, or write the new file if stored_wallet.is_empty() { - log::info!("Removing file: {:#?}", filepath); + log::info!("Removing file: {filepath:#?}"); Ok(fs::remove_file(filepath)?) } else { write_to_file(filepath, &stored_wallet) diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index b19963f4ce..75193606bf 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -40,7 +40,7 @@ }, "productName": "NymWallet", "mainBinaryName": "NymWallet", - "version": "1.2.18", + "version": "1.2.19", "identifier": "net.nymtech.wallet", "plugins": { "updater": { diff --git a/nym-wallet/src/components/Buy/SignMessageModal.stories.tsx b/nym-wallet/src/components/Buy/SignMessageModal.stories.tsx deleted file mode 100644 index 201192ac16..0000000000 --- a/nym-wallet/src/components/Buy/SignMessageModal.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from 'react'; -import { Button } from '@mui/material'; -import { MockBuyContextProvider } from 'src/context/mocks/buy'; - -import { SignMessageModal } from './SignMessageModal'; - -export default { - title: 'Buy/SignMessage', - component: SignMessageModal, -}; - -export const SignMessage = () => { - const [open, setOpen] = useState(false); - - return ( - - - {open && ( - { - setOpen(false); - }} - /> - )} - - ); -}; diff --git a/nym-wallet/src/components/Buy/SignMessageModal.tsx b/nym-wallet/src/components/Buy/SignMessageModal.tsx deleted file mode 100644 index bc9f6eebfc..0000000000 --- a/nym-wallet/src/components/Buy/SignMessageModal.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import { Stack, Typography, Box } from '@mui/material'; -import { useBuyContext } from 'src/context'; -import { SimpleModal } from '../Modals/SimpleModal'; -import { ErrorModal } from '../Modals/ErrorModal'; -import { TextFieldWithPaste } from '../Clipboard/ClipboardFormFields'; -import { CopyToClipboard } from '../Clipboard/ClipboardActions'; - -export const SignMessageModal = ({ onClose }: { onClose: () => void }) => { - const [message, setMessage] = useState(''); - const [signature, setSignature] = useState(''); - - const { signMessage, loading, refresh, error } = useBuyContext(); - - const handleSign = async () => { - if (!message) { - return; - } - try { - const sig = await signMessage(message); - setSignature(sig || ''); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Signing failed:', err); - setSignature(''); - } - }; - - const handleMessageChange = (value: string) => { - setMessage(value); - }; - - if (error) { - return ( - { - refresh(); - }} - /> - ); - } - - return ( - - - - - Message to Sign - - handleMessageChange(e.target.value)} - InputLabelProps={{ shrink: false }} - /> - - - - - Signature - - - - - - ) : undefined, - }} - /> - - - - - ); -}; diff --git a/nym-wallet/src/components/Buy/Tutorial.stories.tsx b/nym-wallet/src/components/Buy/Tutorial.stories.tsx index ec73a796eb..c0410d396f 100644 --- a/nym-wallet/src/components/Buy/Tutorial.stories.tsx +++ b/nym-wallet/src/components/Buy/Tutorial.stories.tsx @@ -1,15 +1,10 @@ import React from 'react'; import { Tutorial } from './Tutorial'; -import { MockBuyContextProvider } from '../../context/mocks/buy'; export default { title: 'Buy/Tutorial', component: Tutorial, }; -export const TutorialPage = () => ( - - - -); +export const TutorialPage = () => ; diff --git a/nym-wallet/src/components/Buy/Tutorial.tsx b/nym-wallet/src/components/Buy/Tutorial.tsx index f2b556c9b6..d3e0a11fce 100644 --- a/nym-wallet/src/components/Buy/Tutorial.tsx +++ b/nym-wallet/src/components/Buy/Tutorial.tsx @@ -1,148 +1,128 @@ -import React, { useContext, useState } from 'react'; -import { Button, Stack, Typography, Grid, useMediaQuery, useTheme } from '@mui/material'; -import { Tune as TuneIcon, BorderColor as BorderColorIcon } from '@mui/icons-material'; -import { CoinMark } from '@nymproject/react/coins/CoinMark'; -import { PoweredByBity } from 'src/svg-icons'; -import { AppContext } from 'src/context'; -import { ClientAddress } from '@nymproject/react/client-address/ClientAddress'; +import React from 'react'; +import { Box, Typography, Grid, Link, Card, CardContent, Stack } from '@mui/material'; import { NymCard } from '..'; -import { SignMessageModal } from './SignMessageModal'; +import BitfinexIcon from 'src/svg-icons/bitfinex.svg'; +import KrakenIcon from 'src/svg-icons/kraken.svg'; +import BybitIcon from 'src/svg-icons/bybit.svg'; +import GateIcon from 'src/svg-icons/gate22.svg'; +import HTXIcon from 'src/svg-icons/htx.svg'; -// TODO retrieve this value from env -const EXCHANGE_URL = 'https://buy.nymtech.net'; - -const borderColor = 'rgba(141, 147, 153, 0.2)'; - -const TutorialStep = ({ - step, - title, - text, - icon, - borderRight, - borderBottom, +const ExchangeCard = ({ + name, + tokenType, + url, + IconComponent }: { - step: number; - title: string; - text: React.ReactNode; - icon: React.ReactNode; - borderRight?: boolean; - borderBottom?: boolean; + name: string; + tokenType: string; + url: string; + IconComponent: React.FunctionComponent>; }) => ( - - - - {icon} - - {`STEP ${step}`} - + + + + + + + + {name} + + + {tokenType} + + + GET NYM + + - - {title} - - {text} - - + + ); export const Tutorial = () => { - const { clientDetails } = useContext(AppContext); - const [showSignModal, setShowSignModal] = useState(false); - const theme = useTheme(); - const showBorder = useMediaQuery(theme.breakpoints.up('md')); + const exchanges = [ + { + name: 'Bitfinex', + tokenType: 'Native NYM, ERC-20', + url: 'https://www.bitfinex.com/', + IconComponent: BitfinexIcon + }, + { + name: 'Kraken', + tokenType: 'Native NYM', + url: 'https://www.kraken.com/', + IconComponent: KrakenIcon + }, + { + name: 'Bybit', + tokenType: 'ERC-20', + url: 'https://www.bybit.com/en/', + IconComponent: BybitIcon + }, + { + name: 'Gate.io', + tokenType: 'ERC-20', + url: 'https://www.gate.io/', + IconComponent: GateIcon + }, + { + name: 'HTX', + tokenType: 'ERC-20', + url: 'https://www.htx.com/', + IconComponent: HTXIcon + }, + ]; return ( } > - - Follow below 3 steps to quickly and easily buy or sell NYM tokens. You can purchase up to 1000 Swiss Francs per - day. + + You can get NYM tokens from these exchanges - {showSignModal && setShowSignModal(false)} />} - - } - text={ - t.palette.nym.text.muted }}> - Click on{' '} - - Buy/Sell NYM - {' '} - button and follow the steps in the browser window that opens. You will be asked for purchase details i.e. - amount, wallet address, etc. - - } - borderRight={showBorder} - borderBottom={!showBorder} - /> - } - text={ - t.palette.nym.text.muted }}> - When asked for signature, copy the message and sign it using{' '} - - Sign message - {' '} - button below. Then copy and paste your signature back in the browser window. - - } - borderRight={showBorder} - borderBottom={!showBorder} - /> - } - text={ - t.palette.nym.text.muted }}> - {`Send the defined BTC amount to Bity's address that's given to you. As soon as your BTC tx has 4 - confirmations, Bity will send the purchased NYM tokens to your wallet.`} - - } - /> + + + {exchanges.map((exchange) => ( + + + + ))} - - - - - - - ); }; diff --git a/nym-wallet/src/constants.ts b/nym-wallet/src/constants.ts index 890724fbdb..c6b2cad578 100644 --- a/nym-wallet/src/constants.ts +++ b/nym-wallet/src/constants.ts @@ -1,4 +1,3 @@ -const QA_VALIDATOR_URL = 'https://qa-nym-api.qa.nymte.ch/api'; const MAINNET_VALIDATOR_URL = 'https://validator.nymtech.net/api/'; -export { QA_VALIDATOR_URL, MAINNET_VALIDATOR_URL }; +export { MAINNET_VALIDATOR_URL }; diff --git a/nym-wallet/src/context/buy.tsx b/nym-wallet/src/context/buy.tsx deleted file mode 100644 index 4444268591..0000000000 --- a/nym-wallet/src/context/buy.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { sign } from 'src/requests'; -import { Console } from 'src/utils/console'; -// import { AppContext } from './main'; - -export type TBuyContext = { - loading: boolean; - error?: string; - signMessage: (message: string) => Promise; - refresh: () => Promise; -}; - -export const BuyContext = createContext({ - loading: false, - signMessage: async () => '', - refresh: async () => undefined, -}); - -export const BuyContextProvider: FCWithChildren = ({ children }): JSX.Element => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(); - - const refresh = useCallback(async () => { - setError(undefined); - }, []); - - useEffect(() => { - refresh(); - }, [refresh]); - - const signMessage = async (message: string) => { - let signature; - setLoading(true); - try { - signature = await sign(message); - } catch (e: any) { - Console.log(`Sign message operation failed: ${e}`); - setError(`Sign message operation failed: ${e}`); - } finally { - setLoading(false); - } - return signature; - }; - - const memoizedValue = useMemo( - () => ({ - loading, - error, - refresh, - signMessage, - }), - [loading, error], - ); - - return {children}; -}; - -export const useBuyContext = () => useContext(BuyContext); diff --git a/nym-wallet/src/context/index.tsx b/nym-wallet/src/context/index.tsx index 5007dd7b17..2229332eb3 100644 --- a/nym-wallet/src/context/index.tsx +++ b/nym-wallet/src/context/index.tsx @@ -2,4 +2,5 @@ export * from './main'; export * from './auth'; export * from './accounts'; export * from './bonding'; -export * from './buy'; +export * from './delegations'; +export * from './rewards'; diff --git a/nym-wallet/src/context/mocks/buy.tsx b/nym-wallet/src/context/mocks/buy.tsx deleted file mode 100644 index 934b9f54d9..0000000000 --- a/nym-wallet/src/context/mocks/buy.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { BuyContext } from '../buy'; -import { mockSleep } from './utils'; - -export const MockBuyContextProvider: FCWithChildren = ({ children }): JSX.Element => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(); - - const refresh = useCallback(async () => { - setError(undefined); - }, []); - - const signMessage = async (message: string) => { - setLoading(true); - await mockSleep(1042); - setLoading(false); - return `imagineareallyrealisticsignaturehash${message}`; - }; - - useEffect(() => { - refresh(); - }, [refresh]); - - const memoizedValue = useMemo( - () => ({ - loading, - error, - refresh, - signMessage, - }), - [loading, error], - ); - - return {children}; -}; diff --git a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx index ec4b4b1a89..b9eeaf38d7 100644 --- a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx @@ -8,7 +8,7 @@ import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { Results } from 'src/components/TestNode/Results'; import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { PrintResults } from 'src/components/TestNode/PrintResults'; -import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants'; +import { MAINNET_VALIDATOR_URL } from 'src/constants'; import { TestStatus } from 'src/components/TestNode/types'; import { isMixnode } from 'src/types'; @@ -63,7 +63,7 @@ export const NodeTestPage = () => { const loadNodeTestClient = useCallback(async () => { try { const nodeTesterId = new Date().toISOString(); // make a new tester id for each session - const validator = network === 'MAINNET' ? MAINNET_VALIDATOR_URL : QA_VALIDATOR_URL; + const validator = network === 'MAINNET' ? MAINNET_VALIDATOR_URL : 'https://rpc.nymtech.net/api/'; const client = await createNodeTesterClient(); await client.tester.init(validator, nodeTesterId); setNodeTestClient(client); diff --git a/nym-wallet/src/pages/buy/index.stories.tsx b/nym-wallet/src/pages/buy/index.stories.tsx index 5782f4298f..d7ab6c7127 100644 --- a/nym-wallet/src/pages/buy/index.stories.tsx +++ b/nym-wallet/src/pages/buy/index.stories.tsx @@ -1,15 +1,10 @@ import React from 'react'; import { Tutorial } from '../../components/Buy/Tutorial'; -import { MockBuyContextProvider } from '../../context/mocks/buy'; export default { title: 'Buy/Page', component: Tutorial, }; -export const BuyPage = () => ( - - - -); +export const BuyPage = () => ; diff --git a/nym-wallet/src/pages/buy/index.tsx b/nym-wallet/src/pages/buy/index.tsx index 894d080727..90d359e269 100644 --- a/nym-wallet/src/pages/buy/index.tsx +++ b/nym-wallet/src/pages/buy/index.tsx @@ -1,9 +1,4 @@ import React from 'react'; -import { BuyContextProvider } from 'src/context'; import { Tutorial } from 'src/components/Buy/Tutorial'; -export const BuyPage = () => ( - - - -); +export const BuyPage = () => ; diff --git a/nym-wallet/src/pages/node-settings/node-stats.tsx b/nym-wallet/src/pages/node-settings/node-stats.tsx index 0dfd9ae18b..04b36e7741 100644 --- a/nym-wallet/src/pages/node-settings/node-stats.tsx +++ b/nym-wallet/src/pages/node-settings/node-stats.tsx @@ -4,12 +4,12 @@ import { TauriLink as Link } from 'src/components/TauriLinkWrapper'; import { urls, AppContext } from '../../context/main'; -export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => { +export const NodeStats = ({ identityKey }: { identityKey?: string }) => { const { network } = useContext(AppContext); return ( All your node stats are available on the link below - + ); }; diff --git a/nym-wallet/src/svg-icons/bitfinex.svg b/nym-wallet/src/svg-icons/bitfinex.svg new file mode 100644 index 0000000000..a3596f66a8 --- /dev/null +++ b/nym-wallet/src/svg-icons/bitfinex.svg @@ -0,0 +1,4 @@ + + + + diff --git a/nym-wallet/src/svg-icons/bybit.svg b/nym-wallet/src/svg-icons/bybit.svg new file mode 100644 index 0000000000..3c51a7a137 --- /dev/null +++ b/nym-wallet/src/svg-icons/bybit.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nym-wallet/src/svg-icons/gate22.svg b/nym-wallet/src/svg-icons/gate22.svg new file mode 100644 index 0000000000..fbd96c23a9 --- /dev/null +++ b/nym-wallet/src/svg-icons/gate22.svg @@ -0,0 +1,4 @@ + + + + diff --git a/nym-wallet/src/svg-icons/htx.svg b/nym-wallet/src/svg-icons/htx.svg new file mode 100644 index 0000000000..a88a36b235 --- /dev/null +++ b/nym-wallet/src/svg-icons/htx.svg @@ -0,0 +1,4 @@ + + + + diff --git a/nym-wallet/src/svg-icons/index.ts b/nym-wallet/src/svg-icons/index.ts index e0bd754736..ec0d4b9c49 100644 --- a/nym-wallet/src/svg-icons/index.ts +++ b/nym-wallet/src/svg-icons/index.ts @@ -3,4 +3,3 @@ export * from './undelegate'; export * from './bond'; export * from './unbond'; export * from './bonding'; -export * from './poweredByBity'; diff --git a/nym-wallet/src/svg-icons/kraken.svg b/nym-wallet/src/svg-icons/kraken.svg new file mode 100644 index 0000000000..d15a792793 --- /dev/null +++ b/nym-wallet/src/svg-icons/kraken.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/nym-wallet/src/svg-icons/poweredByBity.tsx b/nym-wallet/src/svg-icons/poweredByBity.tsx deleted file mode 100644 index 458a77369f..0000000000 --- a/nym-wallet/src/svg-icons/poweredByBity.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { SvgIcon, SvgIconProps } from '@mui/material'; - -export const PoweredByBity = (props: SvgIconProps) => ( - - - - - - - - - - - - - - - - - - - -); diff --git a/nym-wg-gateway-client/Cargo.toml b/nym-wg-gateway-client/Cargo.toml new file mode 100644 index 0000000000..7870fd96b7 --- /dev/null +++ b/nym-wg-gateway-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-wg-gateway-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +nym-authenticator-client = { path = "../nym-authenticator-client" } +nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-bandwidth-controller = { path = "../common/bandwidth-controller" } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto" } +nym-node-requests = { path = "../nym-node/nym-node-requests" } +nym-pemstore = { path = "../common/pemstore" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-statistics-common = { path = "../common/statistics" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +rand.workspace = true +thiserror.workspace = true +tracing.workspace = true +url.workspace = true diff --git a/nym-wg-gateway-client/src/deprecated.rs b/nym-wg-gateway-client/src/deprecated.rs new file mode 100644 index 0000000000..65a9d47950 --- /dev/null +++ b/nym-wg-gateway-client/src/deprecated.rs @@ -0,0 +1,154 @@ +use std::time::Duration; + +use nym_authenticator_client::{ + AuthenticatorClient, AuthenticatorResponse, AuthenticatorVersion, ClientMessage, + QueryMessageImpl, +}; +use nym_authenticator_requests::{v3, v4, v5}; +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::encryption; +use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey; +use nym_sdk::mixnet::Recipient; + +use crate::error::{Error, Result}; + +const RETRY_PERIOD: Duration = Duration::from_secs(30); + +impl crate::WgGatewayClient { + pub fn light_client(&self) -> WgGatewayLightClient { + WgGatewayLightClient { + public_key: *self.keypair.public_key(), + auth_client: self.auth_client.clone(), + } + } +} + +#[derive(Clone)] +pub struct WgGatewayLightClient { + public_key: encryption::PublicKey, + auth_client: AuthenticatorClient, +} +impl WgGatewayLightClient { + pub fn auth_recipient(&self) -> Recipient { + self.auth_client.auth_recipient() + } + + pub fn auth_client(&self) -> &AuthenticatorClient { + &self.auth_client + } + + pub fn set_auth_client(&mut self, auth_client: AuthenticatorClient) { + self.auth_client = auth_client; + } + pub async fn query_bandwidth(&mut self) -> Result> { + let query_message = match self.auth_client.auth_version() { + AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V2, + })), + AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V3, + })), + AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V4, + })), + AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V5, + })), + AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), + }; + let response = self.auth_client.send(&query_message).await?; + + let available_bandwidth = match response { + nym_authenticator_client::AuthenticatorResponse::RemainingBandwidth( + remaining_bandwidth_response, + ) => { + if let Some(available_bandwidth) = + remaining_bandwidth_response.available_bandwidth() + { + available_bandwidth + } else { + return Ok(None); + } + } + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + let remaining_pretty = if available_bandwidth > 1024 * 1024 { + format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0) + } else { + format!("{} KB", available_bandwidth / 1024) + }; + tracing::debug!( + "Remaining wireguard bandwidth with gateway {} for today: {}", + self.auth_client.auth_recipient().gateway(), + remaining_pretty + ); + if available_bandwidth < 1024 * 1024 { + tracing::warn!( + "Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon" + ); + } + Ok(Some(available_bandwidth)) + } + async fn send(&mut self, msg: ClientMessage) -> Result { + let now = std::time::Instant::now(); + while now.elapsed() < RETRY_PERIOD { + match self.auth_client.send(&msg).await { + Ok(response) => return Ok(response), + Err(nym_authenticator_client::Error::TimeoutWaitingForConnectResponse) => continue, + Err(source) => { + if msg.is_wasteful() { + return Err(Error::NoRetry { source }); + } else { + return Err(Error::AuthenticatorClientError(source)); + } + } + } + } + if msg.is_wasteful() { + Err(Error::NoRetry { + source: nym_authenticator_client::Error::TimeoutWaitingForConnectResponse, + }) + } else { + Err(Error::AuthenticatorClientError( + nym_authenticator_client::Error::TimeoutWaitingForConnectResponse, + )) + } + } + + pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { + let top_up_message = match self.auth_client.auth_version() { + AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + // NOTE: looks like a bug here using v3. But we're leaving it as is since it's working + // and V4 is deprecated in favour of V5 + AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { + return Err(Error::UnsupportedAuthenticatorVersion); + } + }; + let response = self.send(top_up_message).await?; + + let remaining_bandwidth = match response { + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.available_bandwidth() + } + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + Ok(remaining_bandwidth) + } +} diff --git a/nym-wg-gateway-client/src/error.rs b/nym-wg-gateway-client/src/error.rs new file mode 100644 index 0000000000..9223c04a12 --- /dev/null +++ b/nym-wg-gateway-client/src/error.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credentials_interface::TicketType; +use nym_sdk::mixnet::NodeIdentity; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("received invalid response from gateway authenticator")] + InvalidGatewayAuthResponse, + + #[error("unknown authenticator version number")] + UnsupportedAuthenticatorVersion, + + #[error(transparent)] + AuthenticatorClientError(#[from] nym_authenticator_client::Error), + + #[error("error that should stop auto retrying")] + NoRetry { + #[source] + source: nym_authenticator_client::Error, + }, + + #[error("verification failure")] + VerificationFailed(#[source] nym_authenticator_requests::Error), + + #[error("failed to parse entry gateway socket addr")] + FailedToParseEntryGatewaySocketAddr(#[source] std::net::AddrParseError), + + #[error("failed to get {ticketbook_type} ticket")] + GetTicket { + ticketbook_type: TicketType, + #[source] + source: nym_bandwidth_controller::error::BandwidthControllerError, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ErrorMessage { + #[error("out of bandwidth for gateway: {gateway_id}")] + OutOfBandwidth { gateway_id: Box }, + + #[error("gateway {gateway_id} is erroring out")] + ErrorsFromGateway { gateway_id: Box }, +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-wg-gateway-client/src/lib.rs b/nym-wg-gateway-client/src/lib.rs new file mode 100644 index 0000000000..3d425a6e2f --- /dev/null +++ b/nym-wg-gateway-client/src/lib.rs @@ -0,0 +1,302 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod deprecated; +mod error; + +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + path::PathBuf, + str::FromStr, +}; + +pub use error::{Error, ErrorMessage}; +use nym_authenticator_client::{ + AuthenticatorClient, AuthenticatorMixnetClient, AuthenticatorResponse, AuthenticatorVersion, + ClientMessage, +}; +use nym_authenticator_requests::{v2, v3, v4, v5}; +use nym_bandwidth_controller::PreparedCredential; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::{encryption, x25519::KeyPair, x25519::PublicKey}; +use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey; +use nym_pemstore::KeyPairPath; +use nym_sdk::mixnet::{CredentialStorage, NodeIdentity, Recipient}; +use nym_validator_client::QueryHttpRpcNyxdClient; +use rand::{rngs::OsRng, CryptoRng, RngCore}; +use tracing::{debug, error, trace}; + +use crate::error::Result; + +pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem"; +pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem"; +pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem"; +pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem"; + +pub const TICKETS_TO_SPEND: u32 = 1; + +#[derive(Clone, Debug)] +pub struct GatewayData { + pub public_key: PublicKey, + pub endpoint: SocketAddr, + pub private_ipv4: Ipv4Addr, + pub private_ipv6: Ipv6Addr, +} + +pub struct WgGatewayClient { + keypair: encryption::KeyPair, + auth_client: AuthenticatorClient, +} + +impl WgGatewayClient { + fn new_type( + data_path: &Option, + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + private_file_name: &str, + public_file_name: &str, + ) -> Self { + let mut rng = OsRng; + let auth_client = AuthenticatorClient::new(auth_mix_client, auth_recipient, auth_version); + if let Some(data_path) = data_path { + let paths = KeyPairPath::new( + data_path.join(private_file_name), + data_path.join(public_file_name), + ); + let keypair = load_or_generate_keypair(&mut rng, paths); + WgGatewayClient { + keypair, + auth_client, + } + } else { + WgGatewayClient { + keypair: KeyPair::new(&mut rng), + auth_client, + } + } + } + + pub fn new_entry( + data_path: &Option, + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self::new_type( + data_path, + auth_mix_client, + auth_recipient, + auth_version, + DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME, + ) + } + + pub fn new_exit( + data_path: &Option, + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self::new_type( + data_path, + auth_mix_client, + auth_recipient, + auth_version, + DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME, + ) + } + + pub fn keypair(&self) -> &encryption::KeyPair { + &self.keypair + } + + pub fn auth_recipient(&self) -> Recipient { + self.auth_client.auth_recipient() + } + + pub fn auth_version(&self) -> AuthenticatorVersion { + self.auth_client.auth_version() + } + + pub async fn request_bandwidth( + gateway_id: NodeIdentity, + controller: &nym_bandwidth_controller::BandwidthController, + ticketbook_type: TicketType, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + let credential = controller + .prepare_ecash_ticket(ticketbook_type, gateway_id.to_bytes(), TICKETS_TO_SPEND) + .await + .map_err(|source| Error::GetTicket { + ticketbook_type, + source, + })?; + Ok(credential) + } + + pub async fn register_wireguard( + &mut self, + gateway_host: IpAddr, + controller: &nym_bandwidth_controller::BandwidthController, + ticketbook_type: TicketType, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + debug!("Registering with the wg gateway..."); + let init_message = match self.auth_version() { + AuthenticatorVersion::V2 => { + ClientMessage::Initial(Box::new(v2::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V3 => { + ClientMessage::Initial(Box::new(v3::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V4 => { + ClientMessage::Initial(Box::new(v4::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V5 => { + ClientMessage::Initial(Box::new(v5::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), + }; + trace!("sending init msg to {}: {:?}", &gateway_host, &init_message); + let response = self.auth_client.send(&init_message).await?; + let registered_data = match response { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + // Unwrap since we have already checked that we have the keypair. + debug!("Verifying data"); + if let Err(e) = pending_registration_response.verify(self.keypair.private_key()) { + return Err(Error::VerificationFailed(e)); + } + + trace!( + "received \"pending-registration\" msg from {}: {:?}", + &gateway_host, + &pending_registration_response + ); + + let credential = Some( + Self::request_bandwidth( + self.auth_recipient().gateway(), + controller, + ticketbook_type, + ) + .await? + .data, + ); + + let finalized_message = match self.auth_version() { + AuthenticatorVersion::V2 => { + ClientMessage::Final(Box::new(v2::registration::FinalMessage { + gateway_client: v2::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips().ipv4.into(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::V3 => { + ClientMessage::Final(Box::new(v3::registration::FinalMessage { + gateway_client: v3::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips().ipv4.into(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::V4 => { + ClientMessage::Final(Box::new(v4::registration::FinalMessage { + gateway_client: v4::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips().into(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::V5 => { + ClientMessage::Final(Box::new(v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::UNKNOWN => { + return Err(Error::UnsupportedAuthenticatorVersion); + } + }; + trace!( + "sending final msg to {}: {:?}", + &gateway_host, + &finalized_message + ); + + let response = self.auth_client.send(&finalized_message).await?; + let AuthenticatorResponse::Registered(registered_response) = response else { + return Err(Error::InvalidGatewayAuthResponse); + }; + registered_response + } + AuthenticatorResponse::Registered(registered_response) => registered_response, + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + trace!( + "received \"registered\" msg from {}: {:?}", + &gateway_host, + ®istered_data + ); + + let gateway_data = GatewayData { + public_key: registered_data.pub_key().inner().into(), + endpoint: SocketAddr::from_str(&format!( + "{}:{}", + gateway_host, + registered_data.wg_port() + )) + .map_err(Error::FailedToParseEntryGatewaySocketAddr)?, + private_ipv4: registered_data.private_ips().ipv4, + private_ipv6: registered_data.private_ips().ipv6, + }; + + Ok(gateway_data) + } +} + +fn load_or_generate_keypair(rng: &mut R, paths: KeyPairPath) -> KeyPair { + match nym_pemstore::load_keypair(&paths) { + Ok(keypair) => keypair, + Err(_) => { + let keypair = KeyPair::new(rng); + if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) { + error!( + "could not store generated keypair at {:?} - {:?}; will use ephemeral keys", + paths, e + ); + } + keypair + } + } +} diff --git a/nyx-chain-watcher/.sqlx/query-83df8a7c5fb24ba4d89f1feb300a8f6d4ac14e7e9b7b482bd57fae568ebd96ba.json b/nyx-chain-watcher/.sqlx/query-83df8a7c5fb24ba4d89f1feb300a8f6d4ac14e7e9b7b482bd57fae568ebd96ba.json index 6af7f2a5bc..582b545744 100644 --- a/nyx-chain-watcher/.sqlx/query-83df8a7c5fb24ba4d89f1feb300a8f6d4ac14e7e9b7b482bd57fae568ebd96ba.json +++ b/nyx-chain-watcher/.sqlx/query-83df8a7c5fb24ba4d89f1feb300a8f6d4ac14e7e9b7b482bd57fae568ebd96ba.json @@ -6,7 +6,7 @@ { "name": "MAX(height)", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" } ], "parameters": { diff --git a/nyx-chain-watcher/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json b/nyx-chain-watcher/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json index 00a7d714a1..e13c1ae4e6 100644 --- a/nyx-chain-watcher/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json +++ b/nyx-chain-watcher/.sqlx/query-a57b74a049b33aee36b72741056d60df8ad35a747808d5d1d3d525a76bbf0618.json @@ -6,7 +6,7 @@ { "name": "timestamp", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "chf", diff --git a/nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json b/nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json index bd0a1777e5..aa82efb4ad 100644 --- a/nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json +++ b/nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json @@ -6,7 +6,7 @@ { "name": "id", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "tx_hash", @@ -16,12 +16,12 @@ { "name": "height", "ordinal": 2, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "message_index", "ordinal": 3, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "sender", diff --git a/nyx-chain-watcher/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json b/nyx-chain-watcher/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json index 3a7ffdb289..2c756ae516 100644 --- a/nyx-chain-watcher/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json +++ b/nyx-chain-watcher/.sqlx/query-f81a3275a1c7cbeefb3fdf7904c677d46a284e0446b96a2fc5bd77630c62d4b8.json @@ -6,7 +6,7 @@ { "name": "timestamp", "ordinal": 0, - "type_info": "Int64" + "type_info": "Integer" }, { "name": "chf", diff --git a/nyx-chain-watcher/build.rs b/nyx-chain-watcher/build.rs index 5cf16e56f8..a6dbb3c105 100644 --- a/nyx-chain-watcher/build.rs +++ b/nyx-chain-watcher/build.rs @@ -14,7 +14,7 @@ async fn main() -> Result<()> { } let db_path_str = db_path.display().to_string().replace('\\', "/"); - let db_url = format!("sqlite:{}", db_path_str); + let db_url = format!("sqlite:{db_path_str}"); // Ensure database file is created with proper permissions let connect_options = SqliteConnectOptions::from_str(&db_url)? @@ -30,7 +30,7 @@ async fn main() -> Result<()> { // Force SQLx to prepare all queries during build println!("cargo:rustc-env=SQLX_OFFLINE=true"); - println!("cargo:rustc-env=DATABASE_URL={}", db_url); + println!("cargo:rustc-env=DATABASE_URL={db_url}"); // Add rerun-if-changed directives println!("cargo:rerun-if-changed=migrations"); @@ -46,7 +46,7 @@ fn export_db_variables(db_url: &str) -> Result<()> { let mut file = File::create(".env")?; for (var, value) in map.iter() { - writeln!(file, "{}={}", var, value)?; + writeln!(file, "{var}={value}")?; } Ok(()) diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index 84d9f10f3b..150738dfb6 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -133,7 +133,7 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa std::fs::create_dir_all(parent)?; } - let connection_url = format!("sqlite://{}?mode=rwc", db_path); + let connection_url = format!("sqlite://{db_path}?mode=rwc"); let storage = db::Storage::init(connection_url).await?; let watcher_pool = storage.pool_owned(); diff --git a/nyx-chain-watcher/src/http/server.rs b/nyx-chain-watcher/src/http/server.rs index 96e7d47f95..4216441d66 100644 --- a/nyx-chain-watcher/src/http/server.rs +++ b/nyx-chain-watcher/src/http/server.rs @@ -34,7 +34,7 @@ pub(crate) async fn build_http_api( ); let router = router_builder.with_state(state); - let bind_addr = format!("0.0.0.0:{}", http_port); + let bind_addr = format!("0.0.0.0:{http_port}"); let server = router.build_server(bind_addr).await?; Ok(server) } diff --git a/nyx-chain-watcher/src/logging.rs b/nyx-chain-watcher/src/logging.rs index 74479e3c7d..1c8cbeebb1 100644 --- a/nyx-chain-watcher/src/logging.rs +++ b/nyx-chain-watcher/src/logging.rs @@ -36,7 +36,7 @@ pub(crate) fn setup_tracing_logger() { "axum", ]; for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + filter = filter.add_directive(directive_checked(format!("{crate_name}=warn"))); } log_builder.with_env_filter(filter).init(); diff --git a/package.json b/package.json index 3ff6703dd9..6c60f64989 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,15 @@ "types:lint:fix": "lerna run lint:fix --scope @nymproject/types --scope @nymproject/nym-wallet-app", "audit:fix": "npm_config_yes=true npx yarn-audit-fix -- --dry-run", "dev:on": "node sdk/typescript/scripts/dev-mode-add.mjs", - "dev:off": "node sdk/typescript/scripts/dev-mode-remove.mjs" + "dev:off": "node sdk/typescript/scripts/dev-mode-remove.mjs", + "security:audit": "yarn audit --level moderate", + "security:audit:fix": "yarn audit --fix", + "security:audit:ci": "yarn install --frozen-lockfile && yarn audit --level moderate", + "security:check": "yarn audit --level high && yarn list --depth=0", + "security:outdated": "yarn outdated", + "security:verify": "yarn audit --level moderate && yarn list --depth=0 && yarn outdated", + "security:full": "./scripts/security-check.sh", + "security:ci": "yarn install --frozen-lockfile && ./scripts/security-check.sh" }, "devDependencies": { "@npmcli/node-gyp": "^3.0.0", @@ -62,6 +70,13 @@ "@cosmjs/proto-signing": "^0.32.4", "@cosmjs/stargate": "^0.32.4", "@cosmjs/cosmwasm-stargate": "^0.32.4", - "cosmjs-types": "^0.9.0" + "cosmjs-types": "^0.9.0", + "chalk": "5.3.0", + "strip-ansi": "7.1.0", + "color-convert": "2.0.1", + "color-name": "1.1.4", + "is-core-module": "2.13.1", + "error-ex": "1.3.2", + "has-ansi": "5.0.1" } } \ No newline at end of file diff --git a/scripts/delegation-program/README.md b/scripts/delegation-program/README.md new file mode 100644 index 0000000000..b39bc2468a --- /dev/null +++ b/scripts/delegation-program/README.md @@ -0,0 +1,60 @@ +## Stake Adjustment Program (`stake_adjustment.py`) + +### Overview + +This simple argument based program is designed primarily for Delegation program management. + +The main goal is to generate a csv of which first two columns without headers can be passed to nym-cli as input for a quick delegation adjustment, using this command: + +```sh +./nym-cli mixnet delegators delegate-multi --mnemonic "" --input /.csv +``` + +The default values are in sync with DP rules: + +`--wallet_address`: Nym Team DP wallet address +`--saturation`: 250k NYM +`--stake_cap`: 90% as per DP rules +`--adjustment_step`: 25k NYM as per DP rules +`--max_wallet_delegation`: 125k NYM as per DP rules +`--denom`: NYM not uNYM to make it smoother and aligned with delegate-multi command of nym-cli + +Additionaly the program scrapes [api.nym.spectredao.net/api/v1/nodes](https://api.nym.spectredao.net/api/v1/nodes) and [validator.nymtech.net/api/v1/nym-nodes/described](https://validator.nymtech.net/api/v1/nym-nodes/described) endpoints and returns a sheet with 20 values per eacvh node passed in the csv input. + +The outcome is a table with these values: +NODE ID, SUGGESTED WALLET DELEGATION, CURRENT WALLET DELEGATION, SUGGESTED TOTAL STAKE, CURRENT TOTAL STAKE, SUGGESTED SATURATION, CURRENT SATURATION, UPTIME, VERSION, T&C, BINARY, ROLE, WIREGUARD, IP ADDRESS, HOSTNAME, WSS PORT, MONIKER, IDENTITY KEY, BONDING WALLET, EXPLORER URL. + +### Install and Run + +1. Download from this branch and make executable +```sh +wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/delegation-program/stake_adjustment.py && chmod u+x stake_adjustment.py +``` +2. Make a simple column csv with `node_ids` (for DP, use all the DP nodes `node_id` column, and save it to the same dir, example can be `input.csv`: +```csv +# example +1398 +1365 +2464 +1423 +1269 +1870 +1824 +1707 +``` +3. Run the program with the `input.csv` positional arg (for DP use the default values without any optional args): +```sh +./stake_adjustment.py input.csv +``` +4. The output is a `csv` with bunch of useful data +5. For DP extract the first two columns with `NODE_ID` and `SUGGESTED_WALLET_DELEGATION` without the headers and save them as a `dp_update.csv` +6. Run this `dp_update.csv` as an input with `nym-cli` to adjust the DP delegations: +```sh +./nym-cli mixnet delegators delegate-multi --mnemonic "" --input dp_update.csv +``` +### Help + +To preview all commands and arguments, run it with `--help` flag: +```sh +./stake_adjustment.py --help +``` diff --git a/scripts/delegation-program/stake_adjustment.py b/scripts/delegation-program/stake_adjustment.py new file mode 100755 index 0000000000..527101651d --- /dev/null +++ b/scripts/delegation-program/stake_adjustment.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 +import argparse +import csv +import sys +from pathlib import Path +import requests +import pandas as pd +import re + +API_SPECTRE_ROOT = "https://api.nym.spectredao.net/api/v1" +API_VALIDATOR = "https://validator.nymtech.net/api/v1" +API_BASE = f"{API_SPECTRE_ROOT}/nodes" +NYM_FACTOR = 1_000_000 + +""" +This simple argument based program is designed primarily for Delegation program management. +The main goal is to generate a csv of which first two columns without headers can also be reused for nym-cli as input: +./nym-cli mixnet delegators delegate-multi --mnemonic "" --input /.csv + +The default values therefore are: +--wallet_address: Nym Team DP wallet address +--saturation: 250k NYM +--stake_cap: 90% as per DP rules +--adjustment_Step: 25k NYM as per DP rules +--max_wallet_delegation: 125k NYM as per DP rules +--denom: NYM not uNYM to make it smoother and aligned with delegate-multi command of nym-cli + +Additionaly the program scrapes described endpoint and returns a sheet with 20 values per node. Those are: +NODE ID, SUGGESTED WALLET DELEGATION, CURRENT WALLET DELEGATION, SUGGESTED TOTAL STAKE, CURRENT TOTAL STAKE, +SUGGESTED SATURATION, CURRENT SATURATION, UPTIME, VERSION, T&C, BINARY, ROLE, WIREGUARD, IP ADDRESS, HOSTNAME, +WSS PORT, MONIKER, IDENTITY KEY, BONDING WALLET, EXPLORER URL. +""" + +def parse_args(): + p = argparse.ArgumentParser( + prog="stake_adjustment.py", + description="Suggest wallet delegation adjustments per node to hit a target saturation cap", + ) + p.add_argument("input", help="Path to CSV with a single column of NODE_ID values") + p.add_argument("--saturation", type=int, default=250_000, + help="Stake saturation in NYM (or uNYM if --denom uNYM). Default: 250000") + p.add_argument("--wallet_address", default="n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw", + help="Delegation wallet address to track and adjust. Default: %(default)s") + p.add_argument("--stake_cap", type=int, default=90, + help="Target percentage of max saturation (e.g., 90 for 90%%). Default: 90") + p.add_argument("--adjustment_step", type=int, default=25_000, + help="Amount to undelegate per step (NYM or uNYM) until target delegation percentage is met. Default: 25000") + p.add_argument("--max_wallet_delegation", type=int, default=125_000, + help="Maximum delegation allowed by the wallet (NYM or uNYM). Default: 125000") + p.add_argument("--denom", type=str, default="NYM", choices=["NYM", "uNYM", "nym", "unym"], + help="Input/output denomination. Default: NYM") + return p.parse_args() + + +def to_unym(value: int, denom: str) -> int: + d = denom.lower() + if d == "nym": + return int(value) * NYM_FACTOR + if d == "unym": + return int(value) + raise ValueError("denom must be NYM or uNYM") + + +def from_unym(value_unym: int, denom: str) -> int: + d = denom.lower() + if d == "nym": + return int(value_unym // NYM_FACTOR) + if d == "unym": + return int(value_unym) + raise ValueError("denom must be NYM or uNYM") + + +def read_node_ids(csv_path: str) -> list[int]: + path = Path(csv_path) + if not path.exists(): + raise RuntimeError(f"Input file not found: {csv_path}") + + node_ids: list[int] = [] + with path.open(newline="") as f: + reader = csv.reader(f) + for row in reader: + if not row: + continue + if len(row) != 1: + raise RuntimeError("Input CSV must have exactly one column of NODE_ID values.") + try: + node_ids.append(int(row[0])) + except ValueError: + raise RuntimeError(f"Invalid NODE_ID (not an integer): {row[0]!r}") + if not node_ids: + raise RuntimeError("Input CSV contains no NODE_IDs.") + return node_ids + + +# pagination helpers: limit/offset -> then page/page_size -> single shot fallback +def _fetch_all_limit_offset(url: str, limit: int = 1000, timeout: int = 60) -> list: + items = [] + offset = 0 + seen_guard = None + loops = 0 + while True: + loops += 1 + r = requests.get(url, params={"limit": limit, "offset": offset}, timeout=timeout) + if r.status_code >= 400: + return None + try: + data = r.json() + except Exception: + return None + if isinstance(data, dict) and "data" in data and isinstance(data["data"], list): + batch = data["data"] + elif isinstance(data, list): + batch = data + else: + return None + if not batch: + break + items.extend(batch) + if len(batch) < limit: + break + # guard against APIs that ignore offset + first_sig = str(batch[0]) + if first_sig == seen_guard: + break + seen_guard = first_sig + offset += len(batch) + if loops > 1000 or offset > 1_000_000: + break + return items + + +def _fetch_all_page_pagesize(url: str, page_size: int = 1000, timeout: int = 60) -> list: + items = [] + page = 1 + seen_guard = None + loops = 0 + while True: + loops += 1 + r = requests.get(url, params={"page": page, "page_size": page_size}, timeout=timeout) + if r.status_code >= 400: + return None + try: + data = r.json() + except Exception: + return None + if isinstance(data, dict) and "data" in data and isinstance(data["data"], list): + batch = data["data"] + elif isinstance(data, list): + batch = data + else: + return None + if not batch: + break + items.extend(batch) + if len(batch) < page_size: + break + first_sig = str(batch[0]) + if first_sig == seen_guard: + break + seen_guard = first_sig + page += 1 + if loops > 1000 or page > 10000: + break + return items + + +def _fetch_all_any(url: str, timeout: int = 60) -> list: + # try limit/offset + got = _fetch_all_limit_offset(url, limit=1000, timeout=timeout) + if isinstance(got, list) and got: + return got + # try page/page_size + got = _fetch_all_page_pagesize(url, page_size=1000, timeout=timeout) + if isinstance(got, list) and got: + return got + # fallback: single shot + r = requests.get(url, timeout=timeout) + r.raise_for_status() + data = r.json() + if isinstance(data, dict) and "data" in data and isinstance(data["data"], list): + return data["data"] + if isinstance(data, list): + return data + raise RuntimeError(f"Unexpected response format from {url}") + + +# fetching functions using robust pagination +def fetch_wallet_delegations(wallet: str) -> list[dict]: + url = f"{API_SPECTRE_ROOT}/delegations/{wallet}" + return _fetch_all_any(url, timeout=45) + + +def fetch_nodes_spectre() -> list[dict]: + url = f"{API_SPECTRE_ROOT}/nodes" + return _fetch_all_any(url, timeout=60) + + +def fetch_nodes_validator() -> list[dict]: + url = f"{API_VALIDATOR}/nym-nodes/described" + return _fetch_all_any(url, timeout=60) + + +def fetch_node_delegations_sum_unym(node_id: int) -> int: + url = f"{API_SPECTRE_ROOT}/nodes/{node_id}/delegations" + try: + data = _fetch_all_any(url, timeout=45) + except Exception: + # last resort + r = requests.get(url, timeout=45) + r.raise_for_status() + data = r.json() + if not isinstance(data, list): + return 0 + total = 0 + for item in data: + try: + total += int(item.get("amount", {}).get("amount")) + except Exception: + pass + return total + + +def suggest_wallet_delegation( + node_id: int, + wallet: str, + saturation_unym: int, + cap_pct: int, + step_unym: int, + max_wallet_unym: int, + out_denom: str, + nodes_map: dict[int, dict], + val_map: dict[int, dict], + wallet_map: dict[int, int], +) -> dict: + # CURRENT TOTAL STAKE (in uNYM) + current_total_unym = None + meta_src = None + + if node_id in nodes_map and isinstance(nodes_map[node_id], dict): + # spectre nodes + current_total_unym = int(nodes_map[node_id].get("total_stake") or 0) + meta_src = nodes_map[node_id] + elif node_id in val_map and isinstance(val_map[node_id], dict): + # validator described + current_total_unym = int(val_map[node_id].get("total_stake") or 0) + meta_src = val_map[node_id] + + # if still unknown, sum delegations as a fallback + if current_total_unym is None or current_total_unym == 0: + current_total_unym = fetch_node_delegations_sum_unym(node_id) + + # CURRENT WALLET DELEGATION (in uNYM) from wallet_map + wallet_unym = int(wallet_map.get(node_id, 0)) + + # target cap in uNYM + target_unym = (saturation_unym * cap_pct) // 100 + + # start from min(current_wallet, max_wallet) and back off by step until under target + suggested_wallet_unym = min(wallet_unym, max_wallet_unym) + suggested_total_unym = current_total_unym - wallet_unym + suggested_wallet_unym + + if suggested_total_unym > target_unym and step_unym > 0: + while suggested_total_unym > target_unym and suggested_wallet_unym > 0: + dec = min(step_unym, suggested_wallet_unym) + suggested_wallet_unym -= dec + suggested_total_unym -= dec + + # convert to denom for output + suggested_wallet = from_unym(suggested_wallet_unym, out_denom) + current_wallet = from_unym(wallet_unym, out_denom) + suggested_total = from_unym(suggested_total_unym, out_denom) + current_total = from_unym(current_total_unym, out_denom) + saturation_val = from_unym(saturation_unym, out_denom) + + # saturation as integer percentages + suggested_sat = int((suggested_total * 100) // (saturation_val or 1)) + current_sat = int((current_total * 100) // (saturation_val or 1)) + + # extra fields + uptime = None + version = None + accepted_tnc= None + binary_name = _sanitize_text(binary_name) + role = _sanitize_text(role) + ip_address = _sanitize_text(ip_address) + hostname = _sanitize_text(hostname) + moniker = _sanitize_text(moniker) + identity_key = _sanitize_text(identity_key) + bonding_addr = _sanitize_text(bonding_addr) + explorer_url = _sanitize_text(explorer_url) + version = _sanitize_text(version) + + m = meta_src or {} + +def suggest_wallet_delegation( + node_id: int, + wallet: str, + saturation_unym: int, + cap_pct: int, + step_unym: int, + max_wallet_unym: int, + out_denom: str, + # extra maps for node + nodes_map: dict[int, dict], + val_map: dict[int, dict], + wallet_map: dict[int, int], +) -> dict: + # CURRENT TOTAL STAKE (in uNYM) + current_total_unym = None + meta_src = None + + if node_id in nodes_map and isinstance(nodes_map[node_id], dict): + # spectre nodes + current_total_unym = int(nodes_map[node_id].get("total_stake") or 0) + meta_src = nodes_map[node_id] + elif node_id in val_map and isinstance(val_map[node_id], dict): + # validator described + current_total_unym = int(val_map[node_id].get("total_stake") or 0) + meta_src = val_map[node_id] + + # if still unknown, sum delegations as a fallback + if current_total_unym is None or current_total_unym == 0: + current_total_unym = fetch_node_delegations_sum_unym(node_id) + + # CURRENT WALLET DELEGATION (in uNYM) from wallet_map + wallet_unym = int(wallet_map.get(node_id, 0)) + + # target cap in uNYM + target_unym = (saturation_unym * cap_pct) // 100 + + # start from min(current_wallet, max_wallet) and back off by step until under target + suggested_wallet_unym = min(wallet_unym, max_wallet_unym) + suggested_total_unym = current_total_unym - wallet_unym + suggested_wallet_unym + + if suggested_total_unym > target_unym and step_unym > 0: + while suggested_total_unym > target_unym and suggested_wallet_unym > 0: + dec = min(step_unym, suggested_wallet_unym) + suggested_wallet_unym -= dec + suggested_total_unym -= dec + + # convert to denom for output + suggested_wallet = from_unym(suggested_wallet_unym, out_denom) + current_wallet = from_unym(wallet_unym, out_denom) + suggested_total = from_unym(suggested_total_unym, out_denom) + current_total = from_unym(current_total_unym, out_denom) + saturation_val = from_unym(saturation_unym, out_denom) + + # saturation as integer percentages + suggested_sat = int((suggested_total * 100) // (saturation_val or 1)) + current_sat = int((current_total * 100) // (saturation_val or 1)) + + # extra fields + uptime = None + version = None + accepted_tnc = None + binary_name = None + role = None + wg_enabled = None + ip_address = None + hostname = None + wss_port = None + moniker = None + identity_key = None + bonding_addr = None + explorer_url = None + + m = meta_src or {} + # spectre and validator conventions translation + identity_key = m.get("identity_key") + bonding_addr = m.get("bonding_address") + uptime = m.get("uptime") + accepted_tnc = m.get("accepted_tnc") + desc = m.get("description") or {} + build = desc.get("build_information") or {} + binary_name = build.get("binary_name") + version = build.get("build_version") + + declared = desc.get("declared_role") or {} + if declared: + if declared.get("exit_ipr") or declared.get("exit_nr"): + role = "exit-gateway" + elif declared.get("entry"): + role = "entry-gateway" + elif declared.get("mixnode"): + role = "mixnode" + + host_info = desc.get("host_information") or {} + ip_list = host_info.get("ip_address") + if isinstance(ip_list, list) and ip_list: + ip_address = ip_list[0] + elif isinstance(ip_list, str): + ip_address = ip_list + hostname = host_info.get("hostname") + + # wss/ws ports under mixnet_websockets + webs = desc.get("mixnet_websockets") or {} + wss_port = webs.get("wss_port") + + # wireguard info + wg = m.get("wireguard") or desc.get("wireguard") + if isinstance(wg, dict) and wg.get("port") and wg.get("public_key"): + wg_enabled = True + else: + wg_enabled = False + + # self desc to get moniker + self_desc = m.get("self_description") or {} + moniker = self_desc.get("moniker") + + explorer_url = f"https://explorer.nym.spectredao.net/nodes/{identity_key}" if identity_key else "" + + # sanitize all string-ish fields to kill tabs/newlines and weird spacing + binary_name = _sanitize_text(binary_name) + role = _sanitize_text(role) + ip_address = _sanitize_text(ip_address) + hostname = _sanitize_text(hostname) + moniker = _sanitize_text(moniker) + identity_key = _sanitize_text(identity_key) + bonding_addr = _sanitize_text(bonding_addr) + explorer_url = _sanitize_text(explorer_url) + version = _sanitize_text(version) + + return { + "NODE ID": node_id, + "SUGGESTED WALLET DELEGATION": suggested_wallet, + "CURRENT WALLET DELEGATION": current_wallet, + "SUGGESTED TOTAL STAKE": suggested_total, + "CURRENT TOTAL STAKE": current_total, + "SUGGESTED SATURATION": suggested_sat, + "CURRENT SATURATION": current_sat, + "UPTIME": uptime, + "VERSION": version, + "T&C": bool(accepted_tnc) if accepted_tnc is not None else None, + "BINARY": binary_name, + "ROLE": role, + "WIREGUARD": wg_enabled, + "IP ADDRESS": ip_address, + "HOSTNAME": hostname, + "WSS PORT": wss_port, + "MONIKER": moniker, + "IDENTITY KEY": identity_key, + "BONDING WALLET": bonding_addr, + "EXPLORER URL": explorer_url, + } + +def _sanitize_text(val): + """Collapse whitespace, remove control chars, strip pipes that break CSVs.""" + if val is None: + return None + s = str(val) + s = s.replace("\r", " ").replace("\n", " ").replace("\t", " ") + s = re.sub(r"\s+", " ", s) + s = s.replace("|", " ") + s = "".join(ch for ch in s if ch.isprintable()) + return s.strip() + +def main(): + args = parse_args() + denom = args.denom + + # convert user inputs to uNYM + saturation_unym = to_unym(args.saturation, denom) + step_unym = to_unym(args.adjustment_step, denom) + max_wallet_unym = to_unym(args.max_wallet_delegation, denom) + + node_ids = read_node_ids(args.input) + + # detect duplicates (just report, do not modify order) + dups = pd.Series(node_ids).duplicated(keep=False) + if dups.any(): + dup_ids = sorted(set([nid for nid, d in zip(node_ids, dups.tolist()) if d])) + print(f"warning: These node IDs are duplicated: {dup_ids}") + else: + print("There are no duplicated node IDs.") + + # one call for wallet delegations (then map node_id -> wallet amount) + print("* * * Fetching wallet delegations * * *") + wallet_delegs = fetch_wallet_delegations(args.wallet_address) + wallet_map: dict[int, int] = {} + for d in wallet_delegs: + try: + nid = int(d.get("node_id")) + amt = int((d.get("amount") or {}).get("amount")) + except Exception: + continue + wallet_map[nid] = wallet_map.get(nid, 0) + amt + + # pull nodes from Spectre (paginated) + print("* * * Fetching nodes (Spectre) with pagination * * *") + spectre_nodes = fetch_nodes_spectre() + nodes_map: dict[int, dict] = {} + for n in spectre_nodes: + try: + nid = int(n.get("node_id")) + except Exception: + continue + nodes_map[nid] = n + + # pull nodes from Validator (paginated) + print("* * * Fetching nodes (Validator) with pagination * * *") + validator_nodes = fetch_nodes_validator() + val_map: dict[int, dict] = {} + for n in validator_nodes: + try: + nid = int(n.get("node_id")) + except Exception: + continue + # do not overwrite spectre if present; keep as fallback + if nid not in nodes_map: + val_map[nid] = n + + # build rows + rows = [] + for nid in node_ids: + try: + row = suggest_wallet_delegation( + node_id=nid, + wallet=args.wallet_address, + saturation_unym=saturation_unym, + cap_pct=args.stake_cap, + step_unym=step_unym, + max_wallet_unym=max_wallet_unym, + out_denom=denom, + nodes_map=nodes_map, + val_map=val_map, + wallet_map=wallet_map, + ) + except Exception as e: + row = { + "NODE ID": nid, + "SUGGESTED WALLET DELEGATION": 0, + "CURRENT WALLET DELEGATION": 0, + "SUGGESTED TOTAL STAKE": 0, + "CURRENT TOTAL STAKE": 0, + "SUGGESTED SATURATION": 0, + "CURRENT SATURATION": 0, + "UPTIME": None, + "VERSION": None, + "T&C": None, + "BINARY": None, + "ROLE": None, + "WIREGUARD": None, + "IP ADDRESS": None, + "HOSTNAME": None, + "WSS PORT": None, + "MONIKER": None, + "IDENTITY KEY": None, + "BONDING WALLET": None, + "EXPLORER URL": "", + } + print(f"warning: node {nid}: {e}", file=sys.stderr) + rows.append(row) + + df = pd.DataFrame(rows) + + print("\nResult preview:") + print(df.to_string(index=False)) + + ans = input("\nSave to ./delegations_adjusted.csv ? [y/N]: ").strip().lower() + if ans == "y": + out_path = Path("./delegations_adjusted.csv") + df.to_csv(out_path, index=False) + print(f"Saved: {out_path.resolve()}") + else: + print("Not saved.") + + +if __name__ == "__main__": + main() diff --git a/scripts/node_api_check.py b/scripts/node_api_check.py index ee9decea3d..5a0a4997ea 100755 --- a/scripts/node_api_check.py +++ b/scripts/node_api_check.py @@ -45,7 +45,7 @@ class MainFunctions: print("\n\nNODE RESULTS FROM UNFILTERED QUERY\n") if args.markdown: - node_markdown = self._dataframe_to_markdown(node_df, ["RESULT"], ["API EDNPOINT"]) + node_markdown = self._dataframe_to_markdown(node_df, ["RESULT"], ["API ENDPOINT"]) print(node_markdown, "\n") else: self.print_neat_dict(node_dict) diff --git a/scripts/nym-node-setup/landing-page.html b/scripts/nym-node-setup/landing-page.html new file mode 100644 index 0000000000..4e09450870 --- /dev/null +++ b/scripts/nym-node-setup/landing-page.html @@ -0,0 +1,24 @@ + + + + + + Nym Node + + + +

Nym Node

+

This is a devrel testing placeholder page for Nym Node landing page.

+ + diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py new file mode 100755 index 0000000000..8c97104e61 --- /dev/null +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -0,0 +1,637 @@ +#!/usr/bin/python3 + +__version__ = "1.0.0" +__default_branch__ = "develop" + +import os +import re +import sys +import subprocess +import argparse +import tempfile +import shlex +import time +from datetime import datetime +from pathlib import Path +from typing import Iterable, Optional, Mapping +from typing import Optional, Tuple + +class NodeSetupCLI: + """All CLI main functions""" + + def __init__(self, args): + self.branch = args.dev + self.welcome_message = self.print_welcome_message() + self.mode = self.prompt_mode() + self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh") + self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") + self.node_install_sh = self.fetch_script("nym-node-install.sh") + self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh") + self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh") + self.landing_page_html = self._check_gwx_mode() and self.fetch_script("landing-page.html") + self.nginx_proxy_wss_sh = self._check_gwx_mode() and self.fetch_script("nginx_proxy_wss_sh") + self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh") + self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh") + self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh") + + def print_welcome_message(self): + """Welcome user, warns for needed pre-reqs and asks for confimation""" + self.print_character("=", 41) + print(\ + "* * * * * * NYM - NODE - CLI * * * * * *\n" \ + "An interactive tool to download, install\n" \ + "* * * * * setup & run nym-node * * * * *" + ) + self.print_character("=", 41) + msg = \ + "Before you begin, make sure that:\n"\ + "1. You run this setup on Debian based Linux (ie Ubuntu)\n"\ + "2. You run this installation program from a root shell\n"\ + "3. You meet minimal requirements: https://nym.com/docs/operators/nodes\n"\ + "4. You accept Operators Terms & Conditions: https://nym.com/operators-validators-terms\n"\ + "5. You have Nym wallet with at least 101 NYM: https://nym.com/docs/operators/nodes/preliminary-steps/wallet-preparation\n"\ + "6. In case of Gateway behind reverse proxy, you have A and AAAA DNS record pointing to this IP and propagated\n"\ + "\nTo confirm and continue, write 'ACCEPT' and press enter:" + print(msg) + confirmation = input("\n") + if confirmation.upper() == "ACCEPT": + pass + else: + print("Without confirming the points above, we cannot continue.") + exit(1) + + def prompt_mode(self): + """Ask user to insert node functionality and save it in python and bash envs""" + mode = input( + "\nEnter the mode you want to run nym-node in: " + "\n1. mixnode " + "\n2. entry-gateway " + "\n3. exit-gateway (works as entry-gateway as well) " + "\nPress 1, 2 or 3 and enter:\n" + ).strip() + + if mode in ("1", "mixnode"): + mode = "mixnode" + elif mode in ("2", "entry-gateway"): + mode = "entry-gateway" + elif mode in ("3", "exit-gateway"): + mode = "exit-gateway" + else: + print("Only numbers 1, 2 or 3 are accepted.") + raise SystemExit(1) + + # save mode for this Python instance + self.mode = mode + os.environ["MODE"] = mode + + # persist to env.sh so other scripts can source it + env_file = Path("env.sh") + with env_file.open("a") as f: + f.write(f'export MODE="{mode}"\n') + + # source env.sh so future bash subprocesses see it immediately + subprocess.run("source ./env.sh", shell=True, executable="/bin/bash") + + print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") + return mode + + + def fetch_script(self, script_name): + """Fetches needed scripts according to a defined mode""" + # print header only the first time + if not getattr(self, "_fetched_once", False): + print("\n* * * Fetching required scripts * * *") + self._fetched_once = True + url = self._return_script_url(script_name) + print(f"Fetching file from: {url}") + result = subprocess.run(["wget", "-qO-", url], capture_output=True, text=True) + if result.returncode != 0 or not result.stdout.strip(): + print(f"wget failed to download the file.") + print("stderr:", result.stderr) + raise RuntimeError(f"Failed to fetch {url}") + # Optional sanity check: + first_line = result.stdout.splitlines()[0] if result.stdout else "" + print(f"Downloaded {len(result.stdout)} bytes.") + return result.stdout + + def _return_script_url(self, script_init_name): + """Dictionary pointing to scripts url returning value according to a passed key""" + github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/" + scripts_urls = { + "nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh", + "setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", + "nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh", + "setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh", + "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", + "nginx_proxy_wss_sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-nginx-proxy-wss.sh", + "landing-page.html": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/landing-page.html", + "network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh", + "wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh", + "exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh", + } + return scripts_urls[script_init_name] + + def run_script( + self, + script_text: str, + args: Optional[Iterable[str]] = None, + env: Optional[Mapping[str, str]] = None, + cwd: Optional[str] = None, + sudo: bool = False, # ignored for root; kept for signature compat + detached: bool = False, + ) -> int: + """ + Save script to a temp file and run it + - Automatically injects ENV_FILE= unless already provided + - Adds SYSTEMD_PAGER="" and SYSTEMD_COLORS="0" by default + Returns exit code (0 if detached fire-and-forget) + """ + import os, subprocess + + path = self._write_temp_script(script_text) + try: + # build env with sensible defaults + run_env = dict(os.environ) + if env: + run_env.update(env) + + # ensure ENV_FILE is absolute and present for all scripts + if "ENV_FILE" not in run_env: + # if env.sh is elsewhere, change this to your known base dir + env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + run_env["ENV_FILE"] = env_file + + # make systemctl non-interactive everywhere + run_env.setdefault("SYSTEMD_PAGER", "") + run_env.setdefault("SYSTEMD_COLORS", "0") + + cmd = [str(path)] + (list(args) if args else []) + + if detached: + subprocess.Popen( + cmd, + env=run_env, + cwd=cwd, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + close_fds=True, + ) + return 0 + else: + cp = subprocess.run(cmd, env=run_env, cwd=cwd) + return cp.returncode + finally: + try: + path.unlink(missing_ok=True) + except Exception: + pass + + def _write_temp_script(self, script_text: str) -> Path: + """Helper: write script text to a temp file, ensure bash shebang, chmod +x, return its path""" + if not script_text.lstrip().startswith("#!"): + script_text = "#!/usr/bin/env bash\n" + script_text + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".sh") as f: + f.write(script_text) + path = Path(f.name) + os.chmod(path, 0o700) + return path + + def _check_gwx_mode(self): + """Helper: Several fns run only for GWx - this fn checks this condition""" + if self.mode == "exit-gateway": + return True + else: + return False + + def check_wg_enabled(self): + """Checks if Wireguard is enabled and if not, prompts user if they want to enable it, stores it to env.sh""" + + + env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + + def norm(v): # -> "true" or "false" + return "true" if str(v).strip().lower() in ("1", "true", "yes", "y") else "false" + + # precedence: process env → env.sh → prompt + val = os.environ.get("WIREGUARD") + + if val is None and os.path.isfile(env_file): + try: + with open(env_file, "r", encoding="utf-8") as f: + m = re.search(r'^\s*export\s+WIREGUARD\s*=\s*"?([^"\n]+)"?', f.read(), re.M) + if m: + val = m.group(1) + except Exception: + pass + + if val is None: + ans = input( + "\nWireGuard is not configured.\n" + "Nodes routing WireGuard can be listed as both entry and exit in the app.\n" + "Enable WireGuard support? (y/n): " + ).strip().lower() + val = "true" if ans in ("y", "yes") else "false" + + val = norm(val) + os.environ["WIREGUARD"] = val + + # persist to env.sh (replace or append) + try: + text = "" + if os.path.isfile(env_file): + with open(env_file, "r", encoding="utf-8") as f: + text = f.read() + if re.search(r'^\s*export\s+WIREGUARD\s*=.*$', text, re.M): + text = re.sub(r'^\s*export\s+WIREGUARD\s*=.*$', f'export WIREGUARD="{val}"', text, flags=re.M) + else: + if text and not text.endswith("\n"): + text += "\n" + text += f'export WIREGUARD="{val}"\n' + with open(env_file, "w", encoding="utf-8") as f: + f.write(text) + print(f'WIREGUARD={val} saved to {env_file}') + except Exception as e: + print(f"Warning: could not write {env_file}: {e}") + + return (val == "true") + + def run_bash_command(self, command, args=None, *, env=None, cwd=None, check=True): + """ + Run a command with optional args (no script stdin) + `command` can be a string (e.g., "ls") or a list (e.g., ["ls", "-la"]). + """ + # normalize command into a list + if isinstance(command, str): + cmd = shlex.split(command) + else: + cmd = list(command) + + if args: + cmd += list(args) + + print("Running:", " ".join(shlex.quote(c) for c in cmd)) + return subprocess.run(cmd, env=env, cwd=cwd, check=check) + + + def run_tunnel_manager_setup(self): + """A standalone fn to pass full cmd list needed for correct setup and test network tunneling, using an external script""" + print( + "\n* * * Setting up network configuration for mixnet IP router and Wireguard tunneling * * *" + "\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#1-download-network_tunnel_managersh-make-executable-and-run" + "\nThis may take a while; follow the steps below and don't kill the process..." + ) + + # each entry is the exact argv to pass to the script + steps = [ + ["check_nymtun_iptables"], + ["remove_duplicate_rules", "nymtun0"], + ["remove_duplicate_rules", "nymwg"], + ["check_nymtun_iptables"], + ["adjust_ip_forwarding"], + ["apply_iptables_rules"], + ["check_nymtun_iptables"], + ["apply_iptables_rules_wg"], + ["configure_dns_and_icmp_wg"], + ["adjust_ip_forwarding"], + ["check_ipv6_ipv4_forwarding"], + ["joke_through_the_mixnet"], + ["joke_through_wg_tunnel"], + ] + + for argv in steps: + print("Running: network_tunnel_manager.sh", *argv) + rc = self.run_script(self.tunnel_manager_sh, args=argv) + if rc != 0: + print(f"Step {' '.join(argv)} failed with exit code {rc}. Stopping.") + return rc + + print("Network tunnel manager setup completed successfully.") + return 0 + + def setup_test_wg_ip_tables(self): + """Configuration and test of Wireguard exit policy according to mixnet exit policy using external scripts""" + print( + "Setting up Wireguard IP tables to match Nym exit policy for mixnet, stored at: https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + "\nThis may take a while, follow the steps below and don't kill the process..." + ) + self.run_script(self.wg_ip_tables_manager_sh, args=["install"]) + self.run_script(self.wg_ip_tables_manager_sh, args=["status"]) + self.run_script(self.wg_ip_tables_test_sh) + + + def run_nym_node_as_service(self): + """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" + service = "nym-node.service" + service_path = "/etc/systemd/system/nym-node.service" + print(f"\n* * * We are going to start {service} from systemd config located at: {service_path} * * *") + + # if the service file is missing, run setup non-interactively + if not os.path.isfile(service_path): + print(f"Service file not found at {service_path}. Running setup...") + setup_env = { + **os.environ, + "SYSTEMD_PAGER": "", + "SYSTEMD_COLORS": "0", + "NONINTERACTIVE": "1", + "MODE": os.environ.get("MODE", "mixnode"), + } + self.run_script(self.service_config_sh, env=setup_env) + if not os.path.isfile(service_path): + print("Service file still not found after setup. Aborting.") + return + + run_env = {**os.environ, "SYSTEMD_PAGER": "", "SYSTEMD_COLORS": "0", "WAIT_TIMEOUT": "600"} + is_active = subprocess.run(["systemctl", "is-active", "--quiet", service], env=run_env).returncode == 0 + + if is_active: + while True: + ans = input(f"{service} is already running. Restart it now? (y/n):\n").strip().lower() + if ans == "y": + self.run_script(self.start_node_systemd_service_sh, args=["restart-poll"], env=run_env) + return + elif ans == "n": + print("Continuing without restart.") + return + else: + print("Invalid input. Please press 'y' or 'n' and press enter.") + else: + while True: + ans = input(f"{service} is not running. Start it now? (y/n):\n").strip().lower() + if ans == "y": + self.run_script(self.start_node_systemd_service_sh, args=["start-poll"], env=run_env) + return + elif ans == "n": + print("Okay, not starting it.") + return + else: + print("Invalid input. Please press 'y' or 'n' and press enter.") + + + + def run_bonding_prompt(self): + """Interactive function navigating user to bond node""" + print("\n") + print("* * * Bonding Nym Node * * *") + print("Time to register your node to Nym Network by bonding it using Nym wallet ...") + node_path = os.path.expandvars(os.path.expanduser("$HOME/nym-binaries/nym-node")) + if not (os.path.isfile(node_path) and os.access(node_path, os.X_OK)): + print(f"Nym node not found at {node_path}, we cannot run a bonding prompt!") + exit(1) + else: + while True: + subprocess.run([os.path.expanduser(node_path), "bonding-information"]) + self.run_bash_command(command="curl", args=["-4", "https://ifconfig.me"]) + print("\n") + self.print_character("=", 56) + print("* * * FOLLOW THESE STEPS TO BOND YOUR NODE * * *") + print("If you already bonded your node before, just press enter") + self.print_character("=", 56) + print( + "1. Open your wallet and go to Bonding menu\n" + "2. Paste Identity key and your IP address (printed above)\n" + "3. Setup your operators cost and profit margin\n" + "4. Copy the long contract message from your wallet" + ) + msg = "5. Paste the contract message from clipboard here and press enter:\n" + contract_msg = input(msg).strip() + if contract_msg == "": + print("Skipping bonding process as your node is already bonded\n") + return + else: + subprocess.run([ + os.path.expanduser(node_path), + "sign", + "--contract-msg", + contract_msg + ]) + print( + "6. Copy the last part of the string back to your Nym wallet\n" + "7. Confirm the transaction" + ) + confirmation = input( + "\n* * * Is your node bonded?\n" + "1. YES\n" + "2. NO, try again\n" + "3. Skip bonding for now\n" + "Press 1, 2, or 3 and enter:\n" + ).strip() + + if confirmation == "1": + # NEW: fetch identity + composed message and print it + _, message = self._explorer_message_from_identity(node_path) + self.print_character("*", 42) + print(message) + self.print_character("*", 42) + return + elif confirmation == "3": + print( + "Your node is not bonded, we are skipping this step.\n" + "Note that without bonding network tunnel manager will not work fully!\n" + "You can always bond manually using:\n" + "`$HOME/nym-binaries/nym-node sign --contract-msg `" + ) + return + elif confirmation == "2": + continue + else: + print( + "Your input was wrong, we are skipping this step. You can always bond manually using:\n" + "`$HOME/nym-binaries/nym-node sign --contract-msg `" + ) + return + + def _explorer_message_from_identity(self, node_path: str) -> Tuple[Optional[str], str]: + """ + Runs `$HOME/nym-binaries/nym-node bonding-information` to + extract the id_key and returns explorer URL with a message + else return the message without the URL + """ + try: + cp = subprocess.run( + [os.path.expanduser(node_path), "bonding-information"], + capture_output=True, text=True, check=False, timeout=30 + ) + output = cp.stdout or "" + except Exception as e: + output = "" + # still return the generic message + key = None + msg = ( + "* * * C O N G R A T U L A T I O N ! * * *\n" + "Your Nym node is registered to Nym network\n" + "Wait until the end of epoch for the change\n" + "to propagate (max 60 min)\n" + "(Could not obtain Identity Key automatically.)" + ) + return key, msg + + # parse the id_key + m = re.search(r"^Identity Key:\s*([A-Za-z0-9]+)\s*$", output, flags=re.MULTILINE) + key = m.group(1) if m else None + + base_msg = ( + "* * * C O N G R A T U L A T I O N ! * * *\n" + "Your Nym node is registered to Nym network\n" + "Wait until the end of epoch for the change\n" + "to propagate (max 60 min)\n" + ) + + if key: + url = f"https://explorer.nym.spectredao.net/nodes/{key}" + msg = base_msg + f"Then you can see your node at:\n{url}" + else: + msg = base_msg + "(Could not obtain Identity Key automatically.)" + + return key, msg + + def print_character(self, ch: str, count: int): + """Print `ch` repeated `count` times (no unbounded growth)""" + if not ch: + return + # Use exactly one codepoint char; trim if longer + ch = ch[:1] + # Clamp count to a sensible max to avoid huge outputs + try: + n = int(count) + except Exception: + n = 0 + n = max(0, min(n, 161)) + print(ch * n) + + def _env_with_envfile(self) -> dict: + """Helper for env persistence sanity""" + env = dict(os.environ) + env["SYSTEMD_PAGER"] = "" + env["SYSTEMD_COLORS"] = "0" + env["ENV_FILE"] = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + return env + + def run_node_installation(self,args): + """Main function called by argparser command install running full node install flow""" + self.run_script(self.prereqs_install_sh) + self.run_script(self.env_vars_install_sh) + self.run_script(self.node_install_sh) + self.run_script(self.service_config_sh) + self._check_gwx_mode() and self.run_script(self.nginx_proxy_wss_sh) + self.run_nym_node_as_service() + self.run_bonding_prompt() + if self._check_gwx_mode(): + self.run_tunnel_manager_setup() + if self.check_wg_enabled(): + self.setup_test_wg_ip_tables() + self.setup_test_wg_ip_tables() + + + +class ArgParser: + """CLI argument interface managing the NodeSetupCLI functions based on user input""" + + def parser_main(self): + # shared options to work before and after subcommands + parent = argparse.ArgumentParser(add_help=False) + parent.add_argument( + "-V", "--version", + action="version", + version=f"nym-node-cli {__version__}" + ) + parent.add_argument("-d", "--dev", metavar="BRANCH", + help="Define github branch", + type=str, + default=argparse.SUPPRESS) + parent.add_argument("-v", "--verbose", action="store_true", + help="Show full error tracebacks") + + parser = argparse.ArgumentParser( + prog="nym-node-cli", + description="An interactive tool to download, install, setup and run nym-node", + epilog="Privacy infrastructure operated by people around the world", + parents=[parent], + ) + + subparsers = parser.add_subparsers(dest="command", help="subcommands") + subparsers.required = True + + p_install = subparsers.add_parser( + "install", parents=[parent], + help="Starts nym-node installation setup CLI", + aliases=["i", "I"], add_help=True + ) + + args = parser.parse_args() + + # assign default manually only if user didn’t supply --dev + if not hasattr(args, "dev"): + args.dev = __default_branch__ + + try: + # build CLI with parsed args to catch errors soon + cli = NodeSetupCLI(args) + + commands = { + "install": cli.run_node_installation, + "i": cli.run_node_installation, + "I": cli.run_node_installation, + } + + func = commands.get(args.command) + if func is None: + parser.print_help() + parser.error(f"Unknown command: {args.command}") + + # execute subcommand within error test + func(args) + + except SystemExit: + raise + except RuntimeError as e: + print(f"{e}\nMake sure that the your BRANCH ('{args.dev}') provided in --dev option contains this program.") + sys.exit(1) + except Exception as e: + if getattr(args, "verbose", False): + traceback.print_exc() + else: + print(f"error: {e}", file=sys.stderr) + sys.exit(1) + + +class SystemSafeGuards: + """A few safe guards to deal with memory usage by this program""" + + def _protect_from_oom(self, score: int = -900): + try: + with open("/proc/self/oom_score_adj", "w") as f: + f.write(str(score)) + except Exception: + pass + + def _trim_memory(self): + """Liberate freeable Python objects and return arenas to the OS if possible""" + try: + import gc, ctypes + gc.collect() + try: + libc = ctypes.CDLL("libc.so.6") + # 0 = “trim as much as possible” + libc.malloc_trim(0) + except Exception: + pass + except Exception: + pass + + def _cap_controller_memory(self, bytes_limit: int = 2 * 1024**3): + # limit this Python process to e.g. 2 GiB virtual memory + try: + import resource + resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit)) + except Exception: + pass + + +if __name__ == '__main__': + safeguards = SystemSafeGuards() + safeguards._protect_from_oom(-900) # de-prioritize controller as OOM victim + safeguards._cap_controller_memory(2 * 1024**3) # optional: cap controller to 2 GiB + app = ArgParser() + app.parser_main() diff --git a/scripts/nym-node-setup/nym-node-install.sh b/scripts/nym-node-setup/nym-node-install.sh new file mode 100644 index 0000000000..13d97ede08 --- /dev/null +++ b/scripts/nym-node-setup/nym-node-install.sh @@ -0,0 +1,227 @@ +#!/bin/bash +set -euo pipefail + +echo -e "\n* * * Ensuring ~/nym-binaries exists * * *" +mkdir -p "$HOME/nym-binaries" + +# Load env.sh via absolute path if provided, else try ./env.sh +if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then + set -a + # shellcheck disable=SC1090 + . "${ENV_FILE}" + set +a +elif [[ -f "./env.sh" ]]; then + set -a + # shellcheck disable=SC1091 + . ./env.sh + set +a +fi + +# check for existing node config and optionally reset +NODE_CONFIG_DIR="$HOME/.nym/nym-nodes/default-nym-node" + +check_existing_config() { + # proceed only if dir exists AND has any entries inside + if [[ -d "$NODE_CONFIG_DIR" ]] && find "$NODE_CONFIG_DIR" -mindepth 1 -maxdepth 1 | read -r _; then + echo + echo "Nym node configuration already exist at $NODE_CONFIG_DIR" + echo + echo "Initialising nym-node again will NOT overwrite your existing private keys, only adjust your preferences (like mode, wireguard optionality etc)." + echo + echo "If you want to remove your current node configuration and all data files including nodes keys type 'RESET' and press enter." + echo + read -r -p "To keep your existing node and just change its preferences press enter: " resp + + if [[ "${resp}" =~ ^([Rr][Ee][Ss][Ee][Tt])$ ]]; then + echo + read -r -p "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one, do you want to back up the old one first? (y/n) " backup_ans + if [[ "${backup_ans}" =~ ^[Yy]$ ]]; then + ts="$(date +%Y%m%d-%H%M%S)" + backup_dir="$HOME/.nym/backup/$(basename "$NODE_CONFIG_DIR")-$ts" + echo "Backing up to: $backup_dir" + mkdir -p "$(dirname "$backup_dir")" + cp -a "$NODE_CONFIG_DIR" "$backup_dir" + fi + echo "Removing $NODE_CONFIG_DIR ..." + rm -rf "$NODE_CONFIG_DIR" + echo "Old node removed. Proceeding with fresh initialization..." + else + echo "Keeping existing node configuration. Proceeding to re-configure." + export ASK_WG="1" + fi + fi +} + +# run the check before any initialization +check_existing_config + +echo -e "\n* * * Resolving latest release tag URL * * *" +LATEST_TAG_URL="$(curl -sI -L -o /dev/null -w '%{url_effective}' https://github.com/nymtech/nym/releases/latest)" +# expected example: https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.13-emmental + +if [[ -z "${LATEST_TAG_URL}" || "${LATEST_TAG_URL}" != *"/releases/tag/"* ]]; then + echo "ERROR: Could not resolve latest tag URL from GitHub." >&2 + exit 1 +fi + +DOWNLOAD_URL="${LATEST_TAG_URL/tag/download}/nym-node" +NYM_NODE="$HOME/nym-binaries/nym-node" + +# if binary already exists, ask to overwrite; if yes, remove first +if [[ -e "${NYM_NODE}" ]]; then + echo + echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}" + read -r -p "Overwrite with the latest release? (y/n): " ow_ans + if [[ "${ow_ans}" =~ ^[Yy]$ ]]; then + echo "Removing existing binary to avoid 'text file busy'..." + rm -f "${NYM_NODE}" + else + echo "Keeping existing binary." + fi +fi + +echo -e "\n* * * Downloading nym-node from:" +echo " ${DOWNLOAD_URL}" +# only download if file is missing (or we just removed it) +if [[ ! -e "${NYM_NODE}" ]]; then + curl -fL "${DOWNLOAD_URL}" -o "${NYM_NODE}" +fi + +echo -e "\n * * * Making binary executable * * *" +chmod +x "${NYM_NODE}" + +echo "---------------------------------------------------" +echo "Nym node binary downloaded:" +"${NYM_NODE}" --version || true +echo "---------------------------------------------------" + +# check that MODE is set (after sourcing env.sh) +if [[ -z "${MODE:-}" ]]; then + echo "ERROR: Environment variable MODE is not set." + echo "Please export MODE as one of: mixnode, entry-gateway, exit-gateway" + exit 1 +fi + +# determine public IP (fallback if ifconfig.me fails) +echo -e "\n* * * Discovering public IP (IPv4) * * *" +if ! PUBLIC_IP="$(curl -fsS -4 https://ifconfig.me)"; then + PUBLIC_IP="$(curl -fsS https://api.ipify.org || echo '')" +fi +if [[ -z "${PUBLIC_IP}" ]]; then + echo "WARNING: Could not determine public IP automatically." +fi + +# respect existing WIREGUARD; for gateways: prompt if unset OR if we kept config and ASK_WG=1 +WIREGUARD="${WIREGUARD:-}" +if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK_WG:-}" || -z "$WIREGUARD" ) ]]; then + echo + echo "Gateways can also route WireGuard in NymVPN." + echo "Enabling it means your node may be listed as both entry and exit in the app." + # show current default in the prompt if present + def_hint="" + [[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]" + read -r -p "Enable WireGuard support? (y/n)${def_hint}: " answer || true + case "${answer:-}" in + [Yy]* ) WIREGUARD="true" ;; + [Nn]* ) WIREGUARD="false" ;; + * ) : ;; # keep existing value if user just pressed enter + esac +fi +# final default only if still empty +WIREGUARD="${WIREGUARD:-false}" + +# persist WIREGUARD to the same env file Python CLI uses +ENV_PATH="${ENV_FILE:-./env.sh}" +if [[ -n "$ENV_PATH" ]]; then + mkdir -p "$(dirname "$ENV_PATH")" + if [[ -f "$ENV_PATH" ]]; then + # replace existing export or append + if grep -qE '^[[:space:]]*export[[:space:]]+WIREGUARD=' "$ENV_PATH"; then + sed -i -E 's|^[[:space:]]*export[[:space:]]+WIREGUARD=.*$|export WIREGUARD="'"$WIREGUARD"'"|' "$ENV_PATH" + else + printf '\nexport WIREGUARD="%s"\n' "$WIREGUARD" >> "$ENV_PATH" + fi + else + printf 'export WIREGUARD="%s"\n' "$WIREGUARD" > "$ENV_PATH" + fi + echo "WIREGUARD=${WIREGUARD} persisted to $ENV_PATH" +fi + +# helpers: ensure optional env vars exist (avoid -u issues) +HOSTNAME="${HOSTNAME:-}" +LOCATION="${LOCATION:-}" +EMAIL="${EMAIL:-}" +MONIKER="${MONIKER:-}" +DESCRIPTION="${DESCRIPTION:-}" + +# initialize node config +case "${MODE}" in + mixnode) + echo -e "\n* * * Initialising nym-node in mode: mixnode * * *" + "${NYM_NODE}" run \ + --mode mixnode \ + ${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \ + ${HOSTNAME:+--hostname "$HOSTNAME"} \ + ${LOCATION:+--location "$LOCATION"} \ + -w \ + --init-only + ;; + entry-gateway) + echo -e "\n* * * Initialising nym-node in mode: entry-gateway * * *" + "${NYM_NODE}" run \ + --mode entry-gateway \ + ${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \ + ${HOSTNAME:+--hostname "$HOSTNAME"} \ + ${LOCATION:+--location "$LOCATION"} \ + --wireguard-enabled "${WIREGUARD}" \ + ${HOSTNAME:+--landing-page-assets-path "/var/www/${HOSTNAME}"} \ + -w \ + --init-only + ;; + exit-gateway) + echo -e "\n* * * Initialising nym-node in mode: exit-gateway * * *" + if [[ -z "${HOSTNAME:-}" || -z "${LOCATION:-}" ]]; then + echo "ERROR: HOSTNAME and LOCATION must be exported for exit-gateway." + exit 1 + fi + "${NYM_NODE}" run \ + --mode exit-gateway \ + ${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \ + --hostname "$HOSTNAME" \ + --location "$LOCATION" \ + --wireguard-enabled "${WIREGUARD:-false}" \ + --announce-wss-port 9001 \ + --landing-page-assets-path "/var/www/${HOSTNAME}" \ + -w \ + --init-only + ;; + *) + echo "ERROR: Unsupported MODE: '${MODE}'" + echo "Valid values: mixnode, entry-gateway, exit-gateway" + exit 1 + ;; +esac + +echo +echo "* * * nym-node initialised. Config path should be:" +echo " $HOME/.nym/nym-nodes/default-nym-node/" + +# setup description.toml (if init created the dir) +DESC_DIR="$HOME/.nym/nym-nodes/default-nym-node/data" +DESC_FILE="$DESC_DIR/description.toml" + +if [[ -d "$DESC_DIR" ]]; then + echo -e "\n* * * Writing node description: $DESC_FILE * * *" + mkdir -p "$DESC_DIR" + cat > "$DESC_FILE" < env.sh + +echo -e "\nVariables saved to ./env.sh" + +if [[ $__SOURCED -eq 1 ]]; then + # shellcheck disable=SC1091 + . ./env.sh + echo "Loaded into current shell (because you sourced this script)." +else + echo "To load them into your current shell, run: source ./env.sh" +fi diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh new file mode 100644 index 0000000000..7dd613fd2a --- /dev/null +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash +set -euo pipefail + +# load env (prefer absolute ENV_FILE injected by Python CLI; fallback to ./env.sh) +if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then + set -a; . "${ENV_FILE}"; set +a +elif [[ -f "./env.sh" ]]; then + set -a; . ./env.sh; set +a +fi + +: "${HOSTNAME:?HOSTNAME not set in env.sh}" +: "${EMAIL:?EMAIL not set in env.sh}" + +export SYSTEMD_PAGER="" +export SYSTEMD_COLORS="0" +DEBIAN_FRONTEND=noninteractive + +# sanity check +if [[ "${HOSTNAME}" == "localhost" || "${HOSTNAME}" == "127.0.0.1" ]]; then + echo "ERROR: HOSTNAME cannot be 'localhost'. Use a public FQDN." >&2 + exit 1 +fi + +echo -e "\n* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" + +# set paths & ports vars +WEBROOT="/var/www/${HOSTNAME}" +LE_ACME_DIR="/var/www/letsencrypt" +SITES_AVAIL="/etc/nginx/sites-available" +SITES_EN="/etc/nginx/sites-enabled" +BASE_HTTP="${SITES_AVAIL}/${HOSTNAME}" # :80 vhost +BASE_HTTPS="${SITES_AVAIL}/${HOSTNAME}-ssl" # :443 vhost (we’ll write it ourselves) +WSS_AVAIL="${SITES_AVAIL}/wss-config-nym" +BACKUP_DIR="/etc/nginx/sites-backups" + +NYM_PORT_HTTP="${NYM_PORT_HTTP:-8080}" +NYM_PORT_WSS="${NYM_PORT_WSS:-9000}" +WSS_LISTEN_PORT="${WSS_LISTEN_PORT:-9001}" + +mkdir -p "${WEBROOT}" "${LE_ACME_DIR}" "${BACKUP_DIR}" "${SITES_AVAIL}" "${SITES_EN}" + +# helpers +neat_backup() { + local file="$1"; [[ -f "$file" ]] || return 0 + local sha_now; sha_now="$(sha256sum "$file" | awk '{print $1}')" || return 0 + local tag; tag="$(basename "$file")" + local latest="${BACKUP_DIR}/${tag}.latest" + if [[ -f "$latest" ]]; then + local sha_prev; sha_prev="$(awk '{print $1}' "$latest")" + [[ "$sha_now" == "$sha_prev" ]] && return 0 + fi + cp -a "$file" "${BACKUP_DIR}/${tag}.bak.$(date +%s)" + echo "$sha_now ${tag}" > "$latest" + ls -1t "${BACKUP_DIR}/${tag}.bak."* 2>/dev/null | tail -n +6 | xargs -r rm -f +} + +ensure_enabled() { + local src="$1"; local name; name="$(basename "$src")" + ln -sf "$src" "${SITES_EN}/${name}" +} + +cert_ok() { + [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" && -s "/etc/letsencrypt/live/${HOSTNAME}/privkey.pem" ]] +} + +fetch_landing() { + local url="https://raw.githubusercontent.com/nymtech/nym/refs/heads/feature/node-setup-cli/scripts/nym-node-setup/landing-page.html" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "${WEBROOT}/index.html" || true + else + wget -qO "${WEBROOT}/index.html" "$url" || true + fi + if [[ ! -s "${WEBROOT}/index.html" ]]; then + cat > "${WEBROOT}/index.html" <<'HTML' +Nym Node + +

Nym node landing

+

This is a placeholder page served by nginx.

+ +HTML + fi +} + +reload_nginx() { nginx -t && systemctl reload nginx; } + +# landing page (idempotent) +fetch_landing +echo "Landing page at ${WEBROOT}/index.html" + +# disable default and stale SSL configs +[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true +for f in "${SITES_EN}"/*; do + [[ -L "$f" ]] || continue + if grep -q "/etc/letsencrypt/live/localhost" "$f"; then + echo "Disabling vhost referencing localhost cert: $f"; unlink "$f" + fi +done +for f in "${SITES_EN}"/*; do + [[ -L "$f" ]] || continue + if grep -qE 'listen\s+.*443' "$f"; then + cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) + key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) + if [[ -n "${cert:-}" && ! -s "$cert" ]] || [[ -n "${key:-}" && ! -s "$key" ]]; then + echo "Disabling SSL vhost with missing cert/key: $f"; unlink "$f" + fi + fi +done + +# HTTP :80 vhost (ACME-safe, proxy to :8080) +neat_backup "${BASE_HTTP}" +cat > "${BASE_HTTP}" </dev/null; then + echo "WARNING: Can't reach Let's Encrypt directory. We'll still keep HTTP up." >&2 +fi +THIS_IP="$(curl -fsS -4 https://ifconfig.me || true)" +DNS_IP="$(getent ahostsv4 "${HOSTNAME}" 2>/dev/null | awk '{print $1; exit}')" +echo "Public IPv4: ${THIS_IP:-unknown} DNS A(${HOSTNAME}): ${DNS_IP:-unresolved}" +if [[ -n "${THIS_IP:-}" && -n "${DNS_IP:-}" && "${THIS_IP}" != "${DNS_IP}" ]]; then + echo "WARNING: DNS for ${HOSTNAME} does not match this server's public IPv4." +fi +timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qi yes || timedatectl set-ntp true || true + +# install certbot if missing +if ! command -v certbot >/dev/null 2>&1; then + if command -v snap >/dev/null 2>&1; then + snap install core || true; snap refresh core || true + snap install --classic certbot; ln -sf /snap/bin/certbot /usr/bin/certbot + else + apt-get update -y >/dev/null 2>&1 || true + apt-get install -y certbot >/dev/null 2>&1 || true + fi +fi + +# issue/renew via WEBROOT (no nginx auto-edit), non-fatal if it fails +STAGING_FLAG=""; [[ "${CERTBOT_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging" && echo "Using Let's Encrypt STAGING." +if ! cert_ok; then + certbot certonly --non-interactive --agree-tos -m "${EMAIL}" -d "${HOSTNAME}" \ + --webroot -w "${LE_ACME_DIR}" ${STAGING_FLAG} || true +fi + +# create own 443 vhost (only if certs exist) +if cert_ok; then + neat_backup "${BASE_HTTPS}" + cat > "${BASE_HTTPS}" <HTTPS (keeps ACME path in HTTP too via separate small server) + neat_backup "${BASE_HTTP}" + cat > "${BASE_HTTP}" < "${WSS_AVAIL}" < "$SERVICE_PATH" </dev/null || echo unknown)" + sub="$(systemctl show -p SubState --value "$SERVICE" 2>/dev/null || echo unknown)" + result="$(systemctl show -p Result --value "$SERVICE" 2>/dev/null || echo unknown)" + local cur="${active}/${sub}/${result}" + if [ "$cur" != "$last" ]; then + echo "state: ActiveState=${active} SubState=${sub} Result=${result}" + last="$cur" + fi + [ "$active" = "active" ] && return 0 + if [ "$active" = "failed" ] || [ "$result" = "failed" ] || [ "$result" = "exit-code" ] || [ "$result" = "timeout" ]; then + return 1 + fi + sleep 1 + done + echo "timeout: ${WAIT_TIMEOUT}s exceeded while waiting for ${SERVICE}" + return 1 +} + +restart_poll() { + reload_and_reset + echo "Restarting $SERVICE (non-blocking) and polling up to ${WAIT_TIMEOUT}s..." + systemctl --no-ask-password restart --no-block "$SERVICE" + wait_until_active_or_fail +} + +start_poll() { + reload_and_reset + echo "Starting $SERVICE (non-blocking) and polling up to ${WAIT_TIMEOUT}s..." + systemctl --no-ask-password start --no-block "$SERVICE" + wait_until_active_or_fail +} + +case "${1:-}" in + restart-poll) restart_poll ;; + start-poll) start_poll ;; + *) echo "Usage: $0 {start-poll|restart-poll}"; exit 2 ;; +esac diff --git a/scripts/security-check.sh b/scripts/security-check.sh new file mode 100755 index 0000000000..d5d66bc7ad --- /dev/null +++ b/scripts/security-check.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e + +echo "starting security checks..." + +if [ ! -f "package.json" ]; then + echo "error: package.json not found, please run this script from the project root." + exit 1 +fi + +echo "checking Node.js version..." +if [ -f ".nvmrc" ]; then + REQUIRED_NODE_VERSION=$(cat .nvmrc) + CURRENT_NODE_VERSION=$(node --version | sed 's/v//') + echo "required Node.js version: $REQUIRED_NODE_VERSION" + echo "current Node.js version: $CURRENT_NODE_VERSION" + + if [ "$CURRENT_NODE_VERSION" != "$REQUIRED_NODE_VERSION" ]; then + echo "warning: Node.js version mismatch, consider using nvm to switch to the required version." + fi +fi + +echo "checking .npmrc configuration..." +if [ ! -f ".npmrc" ]; then + echo "Error: .npmrc file not found, security configurations are missing." + exit 1 +fi + +echo "checking yarn.lock..." +if [ ! -f "yarn.lock" ]; then + echo "error: yarn.lock not found, run 'yarn install' to generate it." + exit 1 +fi + +echo "running yarn audit..." +yarn audit --level moderate + +echo "checking for outdated packages..." +yarn outdated || true + +echo "verifying package integrity..." +yarn list --depth=0 + +echo "checking for known vulnerable packages..." +yarn audit --level high + +echo "checking package sources..." +yarn list --depth=0 --json | jq -r '.data.trees[] | select(.children) | .children[] | select(.name | test("^https?://(?!registry\\.npmjs\\.org)")) | .name' || true + +echo "checks completed successfully!" +echo "" +echo "always use 'yarn install --frozen-lockfile' in production environments" + diff --git a/scripts/wireguard-exit-policy/exit-policy-tests.sh b/scripts/wireguard-exit-policy/exit-policy-tests.sh index c7127c8cdd..5ae10b177c 100644 --- a/scripts/wireguard-exit-policy/exit-policy-tests.sh +++ b/scripts/wireguard-exit-policy/exit-policy-tests.sh @@ -47,6 +47,7 @@ test_port_range_rules() { "8087-8088:tcp:Simplify Media" "8232-8233:tcp:Zcash" "8332-8333:tcp:Bitcoin" + "18080-18081:tcp:Monero" ) local total_failures=0 @@ -148,7 +149,7 @@ test_critical_services() { # Verify default reject rule exists test_default_reject_rule() { echo -e "${YELLOW}This test takes some time, do not quit the process${NC}" - echo + echo echo -e "${YELLOW}Testing Default Reject Rule...${NC}" # Try different patterns to detect the reject rule @@ -236,4 +237,4 @@ if [[ $EUID -ne 0 ]]; then fi # Run the tests -run_all_tests "$@" \ No newline at end of file +run_all_tests "$@" diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh index 5b8c4bc2e5..bce3d09711 100644 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh @@ -357,9 +357,13 @@ apply_port_allowlist() { ["Lightning"]="9735" ["NDMP"]="10000" ["OpenPGP"]="11371" + ["Monero"]="18080-18081" + ["MoneroRPC"]="18089" ["GoogleVoice"]="19294" ["EnsimControlPanel"]="19638" + ["DarkFiTor"]="25551" ["Minecraft"]="25565" + ["DarkFi"]="26661" ["Steam"]="27000-27050" ["ElectrumSSL"]="50002" ["MOSH"]="60000-61000" @@ -672,4 +676,4 @@ main() { esac } -main "$@" \ No newline at end of file +main "$@" diff --git a/sdk/ffi/cpp/Cargo.toml b/sdk/ffi/cpp/Cargo.toml index 7b49a98540..8ac5f608db 100644 --- a/sdk/ffi/cpp/Cargo.toml +++ b/sdk/ffi/cpp/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["cdylib"] tokio = { workspace = true, features = ["full"] } # Nym clients, addressing, packet format, common tools (logging), ffi shared nym-sdk = { path = "../../rust/nym-sdk/" } -nym-bin-common = { path = "../../../common/bin-common" } +nym-bin-common = { path = "../../../common/bin-common", features = ["basic_tracing"] } nym-sphinx-anonymous-replies = { path = "../../../common/nymsphinx/anonymous-replies" } nym-ffi-shared = { path = "../shared" } lazy_static = { workspace = true } diff --git a/sdk/ffi/cpp/src/lib.rs b/sdk/ffi/cpp/src/lib.rs index 0c4784dc4e..110f0e2fbe 100644 --- a/sdk/ffi/cpp/src/lib.rs +++ b/sdk/ffi/cpp/src/lib.rs @@ -12,7 +12,7 @@ use crate::types::types::{CMessageCallback, CStringCallback, ReceivedMessage, St #[no_mangle] pub extern "C" fn init_logging() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); } #[no_mangle] diff --git a/sdk/ffi/go/Cargo.toml b/sdk/ffi/go/Cargo.toml index 584ef8546e..3366f424a5 100644 --- a/sdk/ffi/go/Cargo.toml +++ b/sdk/ffi/go/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-go-ffi" -version = "0.2.1" +version = "0.2.2" edition = "2021" license.workspace = true @@ -13,7 +13,8 @@ name = "nym_go_ffi" uniffi = { workspace = true, features = ["cli"] } # Nym clients, addressing, packet format, common tools (logging), ffi shared nym-sdk = { path = "../../rust/nym-sdk/" } -nym-bin-common = { path = "../../../common/bin-common" } +nym-crypto = { path = "../../../common/crypto" } +nym-bin-common = { path = "../../../common/bin-common", features = ["basic_tracing"] } nym-sphinx-anonymous-replies = { path = "../../../common/nymsphinx/anonymous-replies" } nym-ffi-shared = { path = "../shared" } # Async runtime diff --git a/sdk/ffi/go/go-nym/bindings/bindings.go b/sdk/ffi/go/go-nym/bindings/bindings.go index 00ad0e3a82..ff20cec0cd 100644 --- a/sdk/ffi/go/go-nym/bindings/bindings.go +++ b/sdk/ffi/go/go-nym/bindings/bindings.go @@ -401,7 +401,7 @@ func uniffiCheckChecksums() { checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { return C.uniffi_nym_go_ffi_checksum_func_new_proxy_server(uniffiStatus) }) - if checksum != 40789 { + if checksum != 35729 { // If this happens try cleaning and rebuilding your project panic("bindings: uniffi_nym_go_ffi_checksum_func_new_proxy_server: UniFFI API checksum mismatch") } @@ -1003,9 +1003,9 @@ func NewProxyClientDefault(serverAddress string, env *string) error { return _uniffiErr } -func NewProxyServer(upstreamAddress string, configDir string, env *string) error { +func NewProxyServer(upstreamAddress string, configDir string, env *string, gateway *string) error { _, _uniffiErr := rustCallWithError(FfiConverterTypeGoWrapError{}, func(_uniffiStatus *C.RustCallStatus) bool { - C.uniffi_nym_go_ffi_fn_func_new_proxy_server(FfiConverterStringINSTANCE.Lower(upstreamAddress), FfiConverterStringINSTANCE.Lower(configDir), FfiConverterOptionalStringINSTANCE.Lower(env), _uniffiStatus) + C.uniffi_nym_go_ffi_fn_func_new_proxy_server(FfiConverterStringINSTANCE.Lower(upstreamAddress), FfiConverterStringINSTANCE.Lower(configDir), FfiConverterOptionalStringINSTANCE.Lower(env), FfiConverterOptionalStringINSTANCE.Lower(gateway), _uniffiStatus) return false }) return _uniffiErr diff --git a/sdk/ffi/go/go-nym/bindings/bindings.h b/sdk/ffi/go/go-nym/bindings/bindings.h index 1a28b13dc4..21a5b3ec98 100644 --- a/sdk/ffi/go/go-nym/bindings/bindings.h +++ b/sdk/ffi/go/go-nym/bindings/bindings.h @@ -104,6 +104,7 @@ void uniffi_nym_go_ffi_fn_func_new_proxy_server( RustBuffer upstream_address, RustBuffer config_dir, RustBuffer env, + RustBuffer gateway, RustCallStatus* out_status ); diff --git a/sdk/ffi/go/proxy_example.go b/sdk/ffi/go/proxy_example.go index b979410f20..612fdf6cc2 100644 --- a/sdk/ffi/go/proxy_example.go +++ b/sdk/ffi/go/proxy_example.go @@ -99,7 +99,7 @@ func main() { } // init a proxy server - build_serv_err := bindings.NewProxyServer(upstreamAddress, configDir, &env_path) + build_serv_err := bindings.NewProxyServer(upstreamAddress, configDir, &env_path, nil) if build_serv_err != nil { fmt.Println(build_serv_err) return diff --git a/sdk/ffi/go/src/bindings.udl b/sdk/ffi/go/src/bindings.udl index ec429404c0..d63e7bfa4f 100644 --- a/sdk/ffi/go/src/bindings.udl +++ b/sdk/ffi/go/src/bindings.udl @@ -36,7 +36,7 @@ namespace bindings { [Throws=GoWrapError] void run_proxy_client(); [Throws=GoWrapError] - void new_proxy_server(string upstream_address, string config_dir, string? env); + void new_proxy_server(string upstream_address, string config_dir, string? env, string? gateway); [Throws=GoWrapError] string proxy_server_address(); [Throws=GoWrapError] diff --git a/sdk/ffi/go/src/lib.rs b/sdk/ffi/go/src/lib.rs index 83656c1b76..49bf098c44 100644 --- a/sdk/ffi/go/src/lib.rs +++ b/sdk/ffi/go/src/lib.rs @@ -4,6 +4,7 @@ // due to autogenerated code #![allow(clippy::empty_line_after_doc_comments)] +use nym_crypto::asymmetric::ed25519; use nym_sdk::mixnet::Recipient; use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag; uniffi::include_scaffolding!("bindings"); @@ -35,7 +36,7 @@ enum GoWrapError { #[no_mangle] fn init_logging() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); } #[no_mangle] @@ -141,10 +142,31 @@ fn new_proxy_server( upstream_address: String, config_dir: String, env: Option, + gateway: Option, ) -> Result<(), GoWrapError> { - match nym_ffi_shared::proxy_server_new_internal(&upstream_address, &config_dir, env) { - Ok(_) => Ok(()), - Err(_) => Err(GoWrapError::ServerInitError {}), + if gateway.is_some() { + let gateway_key = match gateway { + Some(gateway_str) => match ed25519::PublicKey::from_base58_string(&gateway_str) { + Ok(key) => Ok(key), + Err(err) => Err(anyhow::anyhow!("Failed to parse gateway key: {}", err)), + }, + None => Err(anyhow::anyhow!("Gateway string is None")), + }; + + match nym_ffi_shared::proxy_server_new_internal( + &upstream_address, + &config_dir, + env, + Some(gateway_key.expect("Couldn't unwrap gateway key")), + ) { + Ok(_) => Ok(()), + Err(_) => Err(GoWrapError::ServerInitError {}), + } + } else { + match nym_ffi_shared::proxy_server_new_internal(&upstream_address, &config_dir, env, None) { + Ok(_) => Ok(()), + Err(_) => Err(GoWrapError::ServerInitError {}), + } } } diff --git a/sdk/ffi/shared/Cargo.toml b/sdk/ffi/shared/Cargo.toml index cdc3759053..e409035681 100644 --- a/sdk/ffi/shared/Cargo.toml +++ b/sdk/ffi/shared/Cargo.toml @@ -10,6 +10,7 @@ tokio = { workspace = true, features = ["full"] } # Nym clients, addressing, packet format, common tools (logging) nym-sdk = { path = "../../rust/nym-sdk/" } nym-bin-common = { path = "../../../common/bin-common" } +nym-crypto = { path = "../../../common/crypto" } nym-sphinx-anonymous-replies = { path = "../../../common/nymsphinx/anonymous-replies" } # static var macro lazy_static = { workspace = true } diff --git a/sdk/ffi/shared/src/lib.rs b/sdk/ffi/shared/src/lib.rs index e7a770f269..eddd9b1f3e 100644 --- a/sdk/ffi/shared/src/lib.rs +++ b/sdk/ffi/shared/src/lib.rs @@ -7,6 +7,8 @@ use nym_sdk::mixnet::{ MixnetClient, MixnetClientBuilder, MixnetMessageSender, Recipient, ReconstructedMessage, StoragePaths, }; + +use nym_crypto::asymmetric::ed25519; use nym_sdk::tcp_proxy::{NymProxyClient, NymProxyServer}; use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag; use std::path::PathBuf; @@ -221,12 +223,14 @@ pub fn proxy_server_new_internal( upstream_address: &str, config_dir: &str, env: Option, + gateway: Option, ) -> Result<(), Error> { if NYM_PROXY_SERVER.lock().unwrap().as_ref().is_some() { bail!("proxy client already exists"); } else { RUNTIME.block_on(async move { - let init_proxy_server = NymProxyServer::new(upstream_address, config_dir, env).await?; + let init_proxy_server = + NymProxyServer::new(upstream_address, config_dir, env, gateway).await?; let mut client = NYM_PROXY_SERVER.try_lock(); if let Ok(ref mut client) = client { **client = Some(init_proxy_server); diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index c95118cf3c..9c67bbf898 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -77,11 +77,11 @@ dirs.workspace = true [dev-dependencies] anyhow = { workspace = true } dotenvy = { workspace = true } -pretty_env_logger = { workspace = true } reqwest = { workspace = true, features = ["json", "socks"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } -nym-bin-common = { path = "../../../common/bin-common" } +time = { workspace = true } +nym-bin-common = { path = "../../../common/bin-common", features = ["basic_tracing"] } # extra dependencies for libp2p examples #libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]} diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index bc30d1f65e..510c225946 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -6,7 +6,7 @@ use nym_sdk::mixnet::MixnetMessageSender; #[tokio::main] async fn main() -> anyhow::Result<()> { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // right now, only sandbox has coconut setup // this should be run from the `sdk/rust/nym-sdk` directory setup_env(Some("../../../envs/sandbox.env")); diff --git a/sdk/rust/nym-sdk/examples/builder.rs b/sdk/rust/nym-sdk/examples/builder.rs index c06fc55913..f0da9afc11 100644 --- a/sdk/rust/nym-sdk/examples/builder.rs +++ b/sdk/rust/nym-sdk/examples/builder.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet::MixnetMessageSender; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Create client builder, including ephemeral keys. The builder can be usable in the context // where you don't want to connect just yet. diff --git a/sdk/rust/nym-sdk/examples/builder_with_storage.rs b/sdk/rust/nym-sdk/examples/builder_with_storage.rs index b733239f5f..51fbc6b598 100644 --- a/sdk/rust/nym-sdk/examples/builder_with_storage.rs +++ b/sdk/rust/nym-sdk/examples/builder_with_storage.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Specify some config options let config_dir = PathBuf::from("/tmp/mixnet-client"); diff --git a/sdk/rust/nym-sdk/examples/change_gateway.rs b/sdk/rust/nym-sdk/examples/change_gateway.rs index 7a6771a7b9..d4ad49536c 100644 --- a/sdk/rust/nym-sdk/examples/change_gateway.rs +++ b/sdk/rust/nym-sdk/examples/change_gateway.rs @@ -3,7 +3,7 @@ #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); todo!() } diff --git a/sdk/rust/nym-sdk/examples/client_pool.rs b/sdk/rust/nym-sdk/examples/client_pool.rs index 27c452ec19..432564218c 100644 --- a/sdk/rust/nym-sdk/examples/client_pool.rs +++ b/sdk/rust/nym-sdk/examples/client_pool.rs @@ -12,7 +12,7 @@ use tokio::signal::ctrl_c; // Run with: cargo run --example client_pool -- ../../../envs/.env #[tokio::main] async fn main() -> Result<()> { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); setup_env(std::env::args().nth(1)); let conn_pool = ClientPool::new(2); // Start the Client Pool with 2 Clients always being kept in reserve diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 7cd5a6f50c..7b08e73f24 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::provider_trait::{async_trait, TopologyProvider}; +use nym_topology::provider_trait::{async_trait, ToTopologyMetadata, TopologyProvider}; use nym_topology::NymTopology; use url::Url; @@ -25,27 +25,31 @@ impl MyTopologyProvider { .await .unwrap(); - let mut base_topology = NymTopology::new_empty(rewarded_set); - - let mixnodes = self + let mixnodes_response = self .validator_client - .get_all_basic_active_mixing_assigned_nodes() + .get_all_basic_active_mixing_assigned_nodes_with_metadata() .await .unwrap(); + let metadata = mixnodes_response.metadata.to_topology_metadata(); + + let mut base_topology = NymTopology::new(metadata, rewarded_set, Vec::new()); + // in our topology provider only use mixnodes that have node_id divisible by 3 // and has exactly 100 performance score // why? because this is just an example to showcase arbitrary uses and capabilities of this trait - let filtered_mixnodes = mixnodes + let filtered_mixnodes = mixnodes_response + .nodes .into_iter() .filter(|mix| mix.node_id % 3 == 0 && mix.performance.is_hundred()) .collect::>(); let gateways = self .validator_client - .get_all_basic_entry_assigned_nodes() + .get_all_basic_entry_assigned_nodes_with_metadata() .await - .unwrap(); + .unwrap() + .nodes; base_topology.add_skimmed_nodes(&filtered_mixnodes); base_topology.add_skimmed_nodes(&gateways); @@ -63,7 +67,7 @@ impl TopologyProvider for MyTopologyProvider { #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); let nym_api = "https://validator.nymtech.net/api/".parse().unwrap(); let my_topology_provider = MyTopologyProvider::new(nym_api); diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs index f7620a835c..e2cd9d7c3e 100644 --- a/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs @@ -68,7 +68,7 @@ impl Substream { } fn check_closed(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Result<(), IoError> { - let closed_err = IoError::new(ErrorKind::Other, "stream closed"); + let closed_err = IoError::other("stream closed"); // close_rx will return an error if the channel is closed (ie. sender was dropped), // or if it's empty @@ -172,12 +172,7 @@ impl AsyncWrite for Substream { ), }), }) - .map_err(|e| { - IoError::new( - ErrorKind::Other, - format!("poll_write outbound_tx error: {}", e), - ) - })?; + .map_err(|e| IoError::other(format!("poll_write outbound_tx error: {}", e)))?; Poll::Ready(Ok(buf.len())) } @@ -195,7 +190,7 @@ impl AsyncWrite for Substream { let mut closed = self.closed.lock(); if *closed { - return Poll::Ready(Err(IoError::new(ErrorKind::Other, "stream closed"))); + return Poll::Ready(Err(IoError::other("stream closed"))); } *closed = true; @@ -210,12 +205,7 @@ impl AsyncWrite for Substream { message: SubstreamMessage::new_close(self.substream_id.clone()), }), }) - .map_err(|e| { - IoError::new( - ErrorKind::Other, - format!("poll_close outbound_rx error: {}", e), - ) - })?; + .map_err(|e| IoError::other(format!("poll_close outbound_rx error: {}", e)))?; Poll::Ready(Ok(())) } diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs index 938acd68e1..46a81a8577 100644 --- a/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs @@ -538,7 +538,7 @@ mod test { Multiaddr, StreamMuxer, }; use log::info; - use nym_bin_common::logging::setup_logging; + use nym_bin_common::logging::setup_tracing_logger; use nym_sdk::mixnet::MixnetClient; use std::{pin::Pin, str::FromStr, sync::atomic::Ordering}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; @@ -573,7 +573,7 @@ mod test { #[tokio::test] async fn test_transport_connection() { - setup_logging(); + setup_tracing_logger(); let client = MixnetClient::connect_new().await.unwrap(); let (dialer_notify_inbound_tx, mut dialer_notify_inbound_rx) = unbounded_channel(); diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 05ce8ebf01..87ba92c2ec 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -8,7 +8,7 @@ use nym_topology::provider_trait::async_trait; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Just some plain data to pretend we have some external storage that the application // implementer is using. diff --git a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs index 38c2b3d7f3..470fca4b83 100644 --- a/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs +++ b/sdk/rust/nym-sdk/examples/manually_overwrite_topology.rs @@ -3,11 +3,11 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; -use nym_topology::{NymTopology, RoutingNode, SupportedRoles}; +use nym_topology::{NymTopology, NymTopologyMetadata, RoutingNode, SupportedRoles}; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Passing no config makes the client fire up an ephemeral session and figure shit out on its own let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); @@ -76,7 +76,10 @@ async fn main() { // during client initialisation to make sure we are able to send to ourselves : ) ) let gateways = starting_topology.topology.entry_capable_nodes(); - let mut custom_topology = NymTopology::new_empty(rewarded_set); + // you should have obtained valid metadata information, in particular the key rotation ID! + let metadata = NymTopologyMetadata::new(u32::MAX, 123, time::OffsetDateTime::now_utc()); + + let mut custom_topology = NymTopology::new(metadata, rewarded_set, Vec::new()); custom_topology.add_routing_nodes(nodes); custom_topology.add_routing_nodes(gateways); diff --git a/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs b/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs index ca9b843717..b42ad5ef04 100644 --- a/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs +++ b/sdk/rust/nym-sdk/examples/parallel_sending_and_receiving.rs @@ -7,7 +7,7 @@ use nym_sdk::mixnet::MixnetMessageSender; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Passing no config makes the client fire up an ephemeral session and figure stuff out on its own let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); diff --git a/sdk/rust/nym-sdk/examples/sandbox.rs b/sdk/rust/nym-sdk/examples/sandbox.rs index 7d92b63202..c3407a2124 100644 --- a/sdk/rust/nym-sdk/examples/sandbox.rs +++ b/sdk/rust/nym-sdk/examples/sandbox.rs @@ -6,7 +6,7 @@ use nym_sdk::mixnet::MixnetMessageSender; // An example of creating a client relying on a testnet, in this case Sandbox. #[tokio::main] async fn main() -> anyhow::Result<()> { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // relative root is `sdk/rust/nym-sdk/` for fallback file path let env_path = std::env::var("NYM_ENV_PATH").unwrap_or_else(|_| "../../../envs/sandbox.env".to_string()); diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index 00ad24dc9a..55ecfb886d 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -3,7 +3,7 @@ use nym_sdk::mixnet::MixnetMessageSender; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Passing no config makes the client fire up an ephemeral session and figure shit out on its own // let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); diff --git a/sdk/rust/nym-sdk/examples/socks5.rs b/sdk/rust/nym-sdk/examples/socks5.rs index 05732db53b..a80c65b56d 100644 --- a/sdk/rust/nym-sdk/examples/socks5.rs +++ b/sdk/rust/nym-sdk/examples/socks5.rs @@ -2,7 +2,7 @@ use nym_sdk::mixnet; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); println!("Connecting receiver"); let mut receiving_client = mixnet::MixnetClient::connect_new().await.unwrap(); diff --git a/sdk/rust/nym-sdk/examples/surb_reply.rs b/sdk/rust/nym-sdk/examples/surb_reply.rs index 649c8aaf7e..6011181f0f 100644 --- a/sdk/rust/nym-sdk/examples/surb_reply.rs +++ b/sdk/rust/nym-sdk/examples/surb_reply.rs @@ -7,7 +7,7 @@ use tempfile::TempDir; #[tokio::main] async fn main() { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); // Specify some config options let config_dir: PathBuf = TempDir::new().unwrap().path().to_path_buf(); @@ -55,8 +55,7 @@ async fn main() { // parse sender_tag: we will use this to reply to sender without needing their Nym address let return_recipient: AnonymousSenderTag = message[0].sender_tag.unwrap(); println!( - "\nReceived the following message: {} \nfrom sender with surb bucket {}", - parsed, return_recipient + "\nReceived the following message: {parsed} \nfrom sender with surb bucket {return_recipient}" ); // reply to self with it: note we use `send_str_reply` instead of `send_str` diff --git a/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs b/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs index 4134ab325d..caf5d74020 100644 --- a/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs +++ b/sdk/rust/nym-sdk/examples/tcp_proxy_multistream.rs @@ -52,7 +52,7 @@ async fn main() -> anyhow::Result<()> { // Within the TcpProxyClient, individual client shutdown is triggered by the timeout. The final argument is how many clients to keep in reserve in the client pool when running the TcpProxy. let proxy_client = - tcp_proxy::NymProxyClient::new(server, "127.0.0.1", &listen_port, 45, Some(env), 2).await?; + tcp_proxy::NymProxyClient::new(server, "127.0.0.1", &listen_port, 80, Some(env), 3).await?; // For our disconnect() logic below let proxy_clone = proxy_client.clone(); @@ -80,7 +80,7 @@ async fn main() -> anyhow::Result<()> { println!("done. sending bytes"); // In the info traces you will see the different session IDs being set up, one for each TcpStream. - for i in 0..8 { + for i in 0..4 { let client_cancel_inner_token = client_cancel_token.clone(); if client_cancel_token.is_cancelled() { break; @@ -101,7 +101,7 @@ async fn main() -> anyhow::Result<()> { // Lets just send a bunch of messages to the server with variable delays between them, with a message and tcp connection ids to keep track of ordering on the server side (for illustrative purposes **only**; keeping track of anonymous replies is handled by the proxy under the hood with Single Use Reply Blocks (SURBs); for this illustration we want some kind of app-level message id, but irl most of the time you'll probably be parsing on e.g. the incoming response type instead) tokio::spawn(async move { - for i in 0..8 { + for i in 0..4 { if client_cancel_inner_token.is_cancelled() { break; } @@ -132,7 +132,7 @@ async fn main() -> anyhow::Result<()> { match bincode::deserialize::(&bytes) { Ok(msg) => { reply_counter += 1; - println!("<< conn {} received {}/8", msg.tcp_conn, reply_counter); + println!("<< conn {} received {}/4", msg.tcp_conn, reply_counter); } Err(e) => { println!("<< client received something that wasn't an example message of {} bytes. error: {}", bytes.len(), e); @@ -147,12 +147,18 @@ async fn main() -> anyhow::Result<()> { tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await; } + loop { + if example_cancel_token.is_cancelled() { + break; + } + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } Ok(()) } // emulate a series of small messages followed by a closing larger one fn gen_bytes_fixed(i: usize) -> Vec { - let amounts = [10, 15, 50, 1000, 10, 15, 500, 2000]; + let amounts = [10, 15, 50, 1000, 2000]; let len = amounts[i]; let mut rng = rand::thread_rng(); (0..len).map(|_| rng.gen::()).collect() diff --git a/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs b/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs index f2ca78c931..adce2e263f 100644 --- a/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs +++ b/sdk/rust/nym-sdk/examples/tcp_proxy_single_connection.rs @@ -45,7 +45,7 @@ async fn main() -> anyhow::Result<()> { let server_port = env::args() .nth(1) .expect("Server listen port not specified"); - let upstream_tcp_addr = format!("127.0.0.1:{}", server_port); + let upstream_tcp_addr = format!("127.0.0.1:{server_port}"); // This dir gets cleaned up at the end: NOTE if you switch env between tests without letting the file do the automatic cleanup, make sure to manually remove this directory up before running again, otherwise your client will attempt to use these keys for the new env let home_dir = dirs::home_dir().expect("Unable to get home directory"); @@ -55,9 +55,13 @@ async fn main() -> anyhow::Result<()> { let env = env_path.to_string(); let client_port = env::args().nth(3).expect("Port not specified"); - let mut proxy_server = - tcp_proxy::NymProxyServer::new(&upstream_tcp_addr, &conf_path, Some(env_path.clone())) - .await?; + let mut proxy_server = tcp_proxy::NymProxyServer::new( + &upstream_tcp_addr, + &conf_path, + Some(env_path.clone()), + None, + ) + .await?; let proxy_nym_addr = proxy_server.nym_address(); // We'll run the instance with a long timeout since we're sending everything down the same Tcp connection, so should be using a single session. @@ -151,7 +155,7 @@ async fn main() -> anyhow::Result<()> { // The assumption regarding integration is that you know what you're sending, and will do proper // framing before and after, know what data types you're expecting, etc; the proxies are just piping bytes // back and forth using tokio's `Bytecodec` under the hood. - let local_tcp_addr = format!("127.0.0.1:{}", client_port); + let local_tcp_addr = format!("127.0.0.1:{client_port}"); let stream = TcpStream::connect(local_tcp_addr).await?; let (read, mut write) = stream.into_split(); diff --git a/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs b/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs index 41656fe272..76209b1792 100644 --- a/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs +++ b/sdk/rust/nym-sdk/src/client_pool/mixnet_client_pool.rs @@ -1,5 +1,9 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use crate::mixnet::{MixnetClient, MixnetClientBuilder, NymNetworkDetails}; use anyhow::Result; +use nym_crypto::asymmetric::ed25519; use std::fmt; use std::sync::Arc; use tokio::sync::RwLock; @@ -43,7 +47,7 @@ impl fmt::Debug for ClientPool { "client_pool_reserve_number", &self.client_pool_reserve_number, ) - .field("clients", &format_args!("[{}]", clients_debug)); + .field("clients", &format_args!("[{clients_debug}]")); debug_struct.finish() } } @@ -108,6 +112,49 @@ impl ClientPool { } } + // Even though this is basically start() with an extra param since I think this + // will only be used for testing scenarios, and I didn't want to unnecessarily add + // another param to the function that will be used elsewhere, hence this is its own fn + pub async fn start_with_specified_gateway(&self, gateway: ed25519::PublicKey) -> Result<()> { + loop { + let spawned_clients = self.clients.read().await.len(); + let addresses = self; + debug!( + "Currently spawned clients: {}: {:?}", + spawned_clients, addresses + ); + if self.cancel_token.is_cancelled() { + break Ok(()); + } + if spawned_clients >= self.client_pool_reserve_number { + debug!("Got enough clients already: sleeping"); + } else { + info!( + "Clients in reserve = {}, reserve amount = {}, spawning new client", + spawned_clients, self.client_pool_reserve_number + ); + let client = loop { + let net = NymNetworkDetails::new_from_env(); + match MixnetClientBuilder::new_ephemeral() + .network_details(net) + .request_gateway(gateway.to_string()) + .build()? + .connect_to_mixnet() + .await + { + Ok(client) => break client, + Err(err) => { + warn!("Error creating client: {:?}, will retry in 100ms", err); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + } + }; + self.clients.write().await.push(Arc::new(client)); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + } + pub async fn disconnect_pool(&self) { info!("Triggering Client Pool disconnect"); self.cancel_token.cancel(); diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 8c21afc13d..90d1b218b6 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -23,7 +23,7 @@ use nym_client_core::client::key_manager::persistence::KeyStore; use nym_client_core::client::{ base_client::BaseClientBuilder, replies::reply_storage::ReplyStorageBackend, }; -use nym_client_core::config::{DebugConfig, ForgetMe, StatsReporting}; +use nym_client_core::config::{DebugConfig, ForgetMe, RememberMe, StatsReporting}; use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::gateways_for_init; use nym_client_core::init::setup_gateway; @@ -65,6 +65,7 @@ pub struct MixnetClientBuilder { storage: S, forget_me: ForgetMe, + remember_me: RememberMe, derivation_material: Option, } @@ -103,6 +104,7 @@ impl MixnetClientBuilder { #[cfg(unix)] connection_fd_callback: None, forget_me: Default::default(), + remember_me: Default::default(), derivation_material: None, }) } @@ -136,6 +138,7 @@ where gateway_endpoint_config_path: None, storage, forget_me: Default::default(), + remember_me: Default::default(), derivation_material: None, } } @@ -158,6 +161,7 @@ where gateway_endpoint_config_path: self.gateway_endpoint_config_path, storage, forget_me: self.forget_me, + remember_me: self.remember_me, derivation_material: self.derivation_material, } } @@ -183,6 +187,12 @@ where self } + #[must_use] + pub fn with_remember_me(mut self, remember_me: RememberMe) -> Self { + self.remember_me = remember_me; + self + } + /// Request a specific gateway instead of a random one. #[must_use] pub fn request_gateway(mut self, user_chosen_gateway: String) -> Self { @@ -323,6 +333,7 @@ where client.connection_fd_callback = self.connection_fd_callback; } client.forget_me = self.forget_me; + client.remember_me = self.remember_me; client.derivation_material = self.derivation_material; Ok(client) } @@ -379,6 +390,8 @@ where forget_me: ForgetMe, + remember_me: RememberMe, + /// The derivation material to use for the client keys, its up to the caller to save this for rederivation later derivation_material: Option, } @@ -419,6 +432,7 @@ where }; let forget_me = config.debug_config.forget_me; + let remember_me = config.debug_config.remember_me; Ok(DisconnectedMixnetClient { config, @@ -435,6 +449,7 @@ where #[cfg(unix)] connection_fd_callback: None, forget_me, + remember_me, derivation_material: None, }) } @@ -495,7 +510,7 @@ where Ok(active) => { if let Some(active) = active.registration { let id = active.details.gateway_id(); - debug!("currently selected gateway: {0}", id); + debug!("currently selected gateway: {id}"); } } } @@ -662,6 +677,7 @@ where BaseClientBuilder::new(base_config, self.storage, self.dkg_query_client) .with_wait_for_gateway(self.wait_for_gateway) .with_forget_me(&self.forget_me) + .with_remember_me(&self.remember_me) .with_derivation_material(self.derivation_material); if let Some(user_agent) = self.user_agent { @@ -821,6 +837,7 @@ where None, started_client.client_request_sender, started_client.forget_me, + started_client.remember_me, )) } } diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index fe542d9eb7..7c8089d324 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -11,7 +11,7 @@ use nym_client_core::client::{ inbound_messages::InputMessage, received_buffer::ReconstructedMessagesReceiver, }; -use nym_client_core::config::ForgetMe; +use nym_client_core::config::{ForgetMe, RememberMe}; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::ClientRequest; use nym_sphinx::addressing::clients::Recipient; @@ -61,6 +61,7 @@ pub struct MixnetClient { _buffered: Vec, pub(crate) client_request_sender: ClientRequestSender, pub(crate) forget_me: ForgetMe, + pub(crate) remember_me: RememberMe, } impl MixnetClient { @@ -77,6 +78,7 @@ impl MixnetClient { packet_type: Option, client_request_sender: ClientRequestSender, forget_me: ForgetMe, + remember_me: RememberMe, ) -> Self { Self { nym_address, @@ -91,6 +93,7 @@ impl MixnetClient { _buffered: Vec::new(), client_request_sender, forget_me, + remember_me, } } @@ -178,7 +181,9 @@ impl MixnetClient { } /// Gets the value of the currently used network topology. - pub async fn read_current_route_provider(&self) -> Option> { + pub async fn read_current_route_provider( + &self, + ) -> Option> { self.client_state .topology_accessor .current_route_provider() @@ -223,7 +228,14 @@ impl MixnetClient { log::debug!("Sending forget me request: {:?}", self.forget_me); match self.send_forget_me().await { Ok(_) => (), - Err(e) => error!("Failed to send forget me request: {}", e), + Err(e) => error!("Failed to send forget me request: {e}"), + }; + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + } else if self.remember_me.stats() { + log::debug!("Sending remember me request: {:?}", self.remember_me); + match self.send_remember_me().await { + Ok(_) => (), + Err(e) => error!("Failed to send remember me request: {e}"), }; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } @@ -245,7 +257,20 @@ impl MixnetClient { match self.client_request_sender.send(client_request).await { Ok(_) => Ok(()), Err(e) => { - error!("Failed to send forget me request: {}", e); + error!("Failed to send forget me request: {e}"); + Err(Error::MessageSendingFailure) + } + } + } + + pub async fn send_remember_me(&self) -> Result<()> { + let client_request = ClientRequest::RememberMe { + session_type: self.remember_me.session_type(), + }; + match self.client_request_sender.send(client_request).await { + Ok(_) => Ok(()), + Err(e) => { + error!("Failed to send forget me request: {e}"); Err(Error::MessageSendingFailure) } } diff --git a/sdk/rust/nym-sdk/src/mixnet/sink.rs b/sdk/rust/nym-sdk/src/mixnet/sink.rs index 5a0c1f4ab3..a3b8fd8ada 100644 --- a/sdk/rust/nym-sdk/src/mixnet/sink.rs +++ b/sdk/rust/nym-sdk/src/mixnet/sink.rs @@ -212,19 +212,18 @@ where cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - ready!(self.tx.poll_ready_unpin(cx)).map_err(|_| { - std::io::Error::new(std::io::ErrorKind::Other, "failed to send packet to mixnet") - })?; + ready!(self.tx.poll_ready_unpin(cx)) + .map_err(|_| std::io::Error::other("failed to send packet to mixnet"))?; let input_message = self .message_translator .to_input_message(buf) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + .map_err(std::io::Error::other)?; // Pass it to the mixnet sender - self.tx.start_send_unpin(input_message).map_err(|_| { - std::io::Error::new(std::io::ErrorKind::Other, "failed to send packet to mixnet") - })?; + self.tx + .start_send_unpin(input_message) + .map_err(|_| std::io::Error::other("failed to send packet to mixnet"))?; Poll::Ready(Ok(buf.len())) } @@ -233,9 +232,8 @@ where mut self: Pin<&mut Self>, cx: &mut Context, ) -> Poll> { - ready!(self.tx.poll_flush_unpin(cx)).map_err(|_| { - std::io::Error::new(std::io::ErrorKind::Other, "failed to send packet to mixnet") - })?; + ready!(self.tx.poll_flush_unpin(cx)) + .map_err(|_| std::io::Error::other("failed to send packet to mixnet"))?; Poll::Ready(Ok(())) } diff --git a/sdk/rust/nym-sdk/src/tcp_proxy.rs b/sdk/rust/nym-sdk/src/tcp_proxy.rs index beb078ff4a..a622e3f6cf 100644 --- a/sdk/rust/nym-sdk/src/tcp_proxy.rs +++ b/sdk/rust/nym-sdk/src/tcp_proxy.rs @@ -1,220 +1,281 @@ -//! use nym_sdk::tcp_proxy; -//! use rand::rngs::SmallRng; -//! use rand::{Rng, SeedableRng}; -//! use serde::{Deserialize, Serialize}; -//! use std::env; -//! use std::fs; -//! use std::sync::atomic::{AtomicU8, Ordering}; -//! use tokio::io::AsyncWriteExt; -//! use tokio::net::{TcpListener, TcpStream}; -//! use tokio::signal; +//! The TcpProxy Module of the Nym SDK which exposes a socket interface for the Mixnet +//! +//! # Basic Example +//! +//! ```no_run +//! use nym_sdk::client_pool::ClientPool; +//! use nym_sdk::mixnet::{IncludedSurbs, MixnetClientBuilder, MixnetMessageSender, NymNetworkDetails}; +//! use nym_sdk::tcp_proxy::utils::{MessageBuffer, Payload, ProxiedMessage}; +//! use anyhow::Result; +//! use dashmap::DashSet; +//! use nym_network_defaults::setup_env; +//! use nym_sphinx::addressing::Recipient; +//! use std::sync::Arc; +//! use tokio::{ +//! net::{TcpListener, TcpStream}, +//! sync::oneshot, +//! }; //! use tokio_stream::StreamExt; -//! use tokio_util::codec; +//! use tokio_util::codec::{BytesCodec, FramedRead}; //! use tokio_util::sync::CancellationToken; -//! use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - -//! #[derive(Serialize, Deserialize, Debug)] -//! struct ExampleMessage { -//! message_id: i8, -//! message_bytes: Vec, +//! use tracing::{debug, info, instrument}; +//! +//! const DEFAULT_CLOSE_TIMEOUT: u64 = 60; // seconds +//! const DEFAULT_LISTEN_HOST: &str = "127.0.0.1"; +//! const DEFAULT_LISTEN_PORT: &str = "8080"; +//! const DEFAULT_CLIENT_POOL_SIZE: usize = 2; +//! +//! #[derive(Clone)] +//! pub struct NymProxyClient { +//! server_address: Recipient, +//! listen_address: String, +//! listen_port: String, +//! close_timeout: u64, +//! conn_pool: ClientPool, +//! cancel_token: CancellationToken, //! } - -//! // This is a basic example which opens a single TCP connection and writes a bunch of messages between a client and an echo -//! // server, so only uses a single session under the hood and doesn't really show off the message ordering capabilities; this is mainly -//! // just a quick introductory illustration on how: -//! // - the mixnet does message ordering -//! // - the NymProxyClient and NymProxyServer can be hooked into and used to communicate between two otherwise pretty vanilla TcpStreams -//! // -//! // For a more irl example checkout tcp_proxy_multistream.rs -//! // -//! // Run this with: -//! // `cargo run --example tcp_proxy_single_connection ` e.g. -//! // `cargo run --example tcp_proxy_single_connection 8081 ../../../envs/canary.env 8080 ` -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! // Keep track of sent/received messages -//! let counter = AtomicU8::new(0); - -//! // Comment this out to just see println! statements from this example, as Nym client logging is very informative but quite verbose. -//! // The Message Decay related logging gives you an ideas of the internals of the proxy message ordering. To see the contents of the msg buffer, sphinx packet chunking, etc change the tracing::Level to DEBUG. -//! tracing_subscriber::registry() -//! .with(fmt::layer()) -//! .with(EnvFilter::new("nym_sdk::tcp_proxy=info")) -//! .init(); - -//! let server_port = env::args() -//! .nth(1) -//! .expect("Server listen port not specified"); -//! let upstream_tcp_addr = format!("127.0.0.1:{}", server_port); - -//! // This dir gets cleaned up at the end: NOTE if you switch env between tests without letting the file do the automatic cleanup, make sure to manually remove this directory up before running again, otherwise your client will attempt to use these keys for the new env -//! let home_dir = dirs::home_dir().expect("Unable to get home directory"); -//! let conf_path = format!("{}/tmp/nym-proxy-server-config", home_dir.display()); - -//! let env_path = env::args().nth(2).expect("Env file not specified"); -//! let env = env_path.to_string(); -//! let client_port = env::args().nth(3).expect("Port not specified"); - -//! let mut proxy_server = -//! tcp_proxy::NymProxyServer::new(&upstream_tcp_addr, &conf_path, Some(env_path.clone())) -//! .await?; -//! let proxy_nym_addr = proxy_server.nym_address(); - -//! // We'll run the instance with a long timeout since we're sending everything down the same Tcp connection, so should be using a single session. -//! // Within the TcpProxyClient, individual client shutdown is triggered by the timeout. -//! // The final argument is how many clients to keep in reserve in the client pool when running the TcpProxy. -//! let proxy_client = -//! tcp_proxy::NymProxyClient::new(*proxy_nym_addr, "127.0.0.1", &client_port, 5, Some(env), 1) -//! .await?; - -//! // For our disconnect() logic below -//! let proxy_clone = proxy_client.clone(); - -//! tokio::spawn(async move { -//! proxy_server.run_with_shutdown().await?; -//! Ok::<(), anyhow::Error>(()) -//! }); - -//! tokio::spawn(async move { -//! proxy_client.run().await?; -//! Ok::<(), anyhow::Error>(()) -//! }); - -//! let example_cancel_token = CancellationToken::new(); -//! let server_cancel_token = example_cancel_token.clone(); -//! let client_cancel_token = example_cancel_token.clone(); -//! let watcher_cancel_token = example_cancel_token.clone(); - -//! // Cancel listener thread -//! tokio::spawn(async move { -//! signal::ctrl_c().await?; -//! println!(":: CTRL_C received, shutting down + cleanup up proxy server config files"); -//! fs::remove_dir_all(conf_path)?; -//! watcher_cancel_token.cancel(); -//! proxy_clone.disconnect().await; -//! Ok::<(), anyhow::Error>(()) -//! }); - -//! // 'Server side' thread: echo back incoming as response to the messages sent in the 'client side' thread below -//! tokio::spawn(async move { -//! let listener = TcpListener::bind(upstream_tcp_addr).await?; +//! +//! impl NymProxyClient { +//! pub async fn new( +//! server_address: Recipient, +//! listen_address: &str, +//! listen_port: &str, +//! close_timeout: u64, +//! env: Option, +//! default_client_amount: usize, +//! ) -> Result { +//! debug!("Loading env file: {:?}", env); +//! setup_env(env); // Defaults to mainnet if empty +//! Ok(NymProxyClient { +//! server_address, +//! listen_address: listen_address.to_string(), +//! listen_port: listen_port.to_string(), +//! close_timeout, +//! conn_pool: ClientPool::new(default_client_amount), +//! cancel_token: CancellationToken::new(), +//! }) +//! } +//! +//! // server_address is the Nym address of the NymProxyServer to communicate with. +//! pub async fn new_with_defaults(server_address: Recipient, env: Option) -> Result { +//! NymProxyClient::new( +//! server_address, +//! DEFAULT_LISTEN_HOST, +//! DEFAULT_LISTEN_PORT, +//! DEFAULT_CLOSE_TIMEOUT, +//! env, +//! DEFAULT_CLIENT_POOL_SIZE, +//! ) +//! .await +//! } +//! +//! pub async fn run(&self) -> Result<()> { +//! info!("Connecting to mixnet server at {}", self.server_address); +//! +//! let listener = +//! TcpListener::bind(format!("{}:{}", self.listen_address, self.listen_port)).await?; +//! +//! let client_maker = self.conn_pool.clone(); +//! tokio::spawn(async move { +//! client_maker.start().await?; +//! Ok::<(), anyhow::Error>(()) +//! }); +//! //! loop { -//! if server_cancel_token.is_cancelled() { -//! break; -//! } -//! let (socket, _) = listener.accept().await.unwrap(); -//! let (read, mut write) = socket.into_split(); -//! let codec = codec::BytesCodec::new(); -//! let mut framed_read = codec::FramedRead::new(read, codec); -//! while let Some(Ok(bytes)) = framed_read.next().await { -//! match bincode::deserialize::(&bytes) { -//! Ok(msg) => { -//! println!( -//! "<< server received {}: {} bytes", -//! msg.message_id, -//! msg.message_bytes.len() -//! ); -//! let msg = ExampleMessage { -//! message_id: msg.message_id, -//! message_bytes: msg.message_bytes, -//! }; -//! let serialised = bincode::serialize(&msg)?; -//! write -//! .write_all(&serialised) -//! .await -//! .expect("couldnt send reply"); -//! println!( -//! ">> server sent {}: {} bytes", -//! msg.message_id, -//! msg.message_bytes.len() -//! ); -//! } -//! Err(e) => { -//! println!("<< server received something that wasn't an example message of {} bytes. error: {}", bytes.len(), e); -//! } +//! tokio::select! { +//! stream = listener.accept() => { +//! let (stream, _) = stream?; +//! tokio::spawn(NymProxyClient::handle_incoming( +//! stream, +//! self.server_address, +//! self.close_timeout, +//! self.conn_pool.clone(), +//! self.cancel_token.clone(), +//! )); +//! } +//! _ = self.cancel_token.cancelled() => { +//! break Ok(()); //! } //! } //! } -//! #[allow(unreachable_code)] -//! Ok::<(), anyhow::Error>(()) -//! }); - -//! // Just wait for Nym clients to connect, TCP clients to bind, etc. If there isn't a client in the pool (or you started it with 0) already then the TcpProxyClient just spins up an ephemeral client itself. -//! println!("waiting for everything to be set up.."); -//! tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; -//! println!("done. sending bytes"); - -//! // Now the client and server proxies are running we can create and pipe traffic to/from -//! // a socket on the same port as our ProxyClient instance as if we were just communicating -//! // between a client and host via a normal TcpStream - albeit with a decent amount of additional latency. -//! // -//! // The assumption regarding integration is that you know what you're sending, and will do proper -//! // framing before and after, know what data types you're expecting, etc; the proxies are just piping bytes -//! // back and forth using tokio's `Bytecodec` under the hood. -//! let local_tcp_addr = format!("127.0.0.1:{}", client_port); -//! let stream = TcpStream::connect(local_tcp_addr).await?; -//! let (read, mut write) = stream.into_split(); - -//! // 'Client side' thread; lets just send a bunch of messages to the server with variable delays between them, with an id to keep track of ordering in the printlns; the mixnet only guarantees message delivery, not ordering. You might not be necessarily streaming traffic in this manner IRL, but this example is a good illustration of how messages travel through the mixnet. -//! // - On the level of individual messages broken into multiple packets, the Proxy abstraction deals with making sure that everything is sent between the sockets in the //! corrent order. -//! // - On the level of different messages, this is not enforced: you might see in the logs that message 1 arrives at the server and is reconstructed after message 2. -//! tokio::spawn(async move { -//! let mut rng = SmallRng::from_entropy(); -//! for i in 0..10 { -//! if client_cancel_token.is_cancelled() { -//! break; -//! } -//! let random_bytes = gen_bytes_fixed(i as usize); -//! let msg = ExampleMessage { -//! message_id: i, -//! message_bytes: random_bytes, -//! }; -//! let serialised = bincode::serialize(&msg)?; -//! write -//! .write_all(&serialised) -//! .await -//! .expect("couldn't write to stream"); -//! println!(">> client sent {}: {} bytes", &i, msg.message_bytes.len()); -//! let delay = rng.gen_range(3.0..7.0); -//! tokio::time::sleep(tokio::time::Duration::from_secs_f64(delay)).await; -//! } -//! Ok::<(), anyhow::Error>(()) -//! }); - -//! let codec = codec::BytesCodec::new(); -//! let mut framed_read = codec::FramedRead::new(read, codec); -//! while let Some(Ok(bytes)) = framed_read.next().await { -//! match bincode::deserialize::(&bytes) { -//! Ok(msg) => { -//! println!( -//! "<< client received {}: {} bytes", -//! msg.message_id, -//! msg.message_bytes.len() -//! ); -//! counter.fetch_add(1, Ordering::SeqCst); -//! println!( -//! ":: messages received back: {:?}/10", -//! counter.load(Ordering::SeqCst) -//! ); -//! } -//! Err(e) => { -//! println!("<< client received something that wasn't an example message of {} bytes. error: {}", bytes.len(), e); -//! } -//! } //! } - -//! Ok(()) -//! } - -//! fn gen_bytes_fixed(i: usize) -> Vec { -//! let amounts = [158, 1088, 505, 1001, 150, 200, 3500, 500, 750, 100]; -//! let len = amounts[i]; -//! let mut rng = rand::thread_rng(); -//! (0..len).map(|_| rng.gen::()).collect() +//! +//! pub async fn disconnect(&self) { +//! self.cancel_token.cancel(); +//! self.conn_pool.disconnect_pool().await; +//! } +//! +//! // The main body of our logic, triggered on each accepted incoming tcp connection. To deal with assumptions about +//! // streaming we have to implement an abstract session for each set of outgoing messages atop each connection, with message +//! // IDs to deal with the fact that the mixnet does not enforce message ordering. +//! // +//! // There is an initial thread which does a bunch of setup logic +//! // - Create a random session ID +//! // - Create a Nym Client (and split into read/write clients for concurrent read/write) +//! // - Split incoming TcpStream into OwnedReadHalf and OwnedWriteHalf for concurrent read/write +//! // +//! // Then we spawn 2 tasks: +//! // - 'Outgoing' thread => frames incoming bytes from OwnedReadHalf and pipe through the mixnet & trigger session close. +//! // - 'Incoming' thread => orders incoming messages from the Mixnet via placing them in a MessageBuffer and using tick(), as well as manage session closing. +//! #[instrument(skip(stream, server_address, close_timeout, conn_pool, cancel_token))] +//! async fn handle_incoming( +//! stream: TcpStream, +//! server_address: Recipient, +//! close_timeout: u64, +//! conn_pool: ClientPool, +//! cancel_token: CancellationToken, +//! ) -> Result<()> { +//! // ID for creation of session abstraction; new session ID per new connection accepted by our tcp listener above. +//! let session_id = uuid::Uuid::new_v4(); +//! +//! // Used to communicate end of session between 'Outgoing' and 'Incoming' tasks +//! let (tx, mut rx) = oneshot::channel(); +//! +//! info!("Starting session: {}", session_id); +//! +//! let mut client = match conn_pool.get_mixnet_client().await { +//! Some(client) => { +//! info!("Grabbed client {} from pool", client.nym_address()); +//! client +//! } +//! None => { +//! info!("Not enough clients in pool, creating ephemeral client"); +//! let net = NymNetworkDetails::new_from_env(); +//! let client = MixnetClientBuilder::new_ephemeral() +//! .network_details(net) +//! .build()? +//! .connect_to_mixnet() +//! .await?; +//! info!( +//! "Using {} for the moment, created outside of the connection pool", +//! client.nym_address() +//! ); +//! client +//! } +//! }; +//! +//! // Split our tcpstream into OwnedRead and OwnedWrite halves for concurrent read/writing +//! let (read, mut write) = stream.into_split(); +//! // Since we're just trying to pipe whatever bytes our client/server are normally sending to each other, +//! // the bytescodec is fine to use here; we're trying to avoid modifying this stream e.g. in the process of Sphinx packet +//! // creation and adding padding to the payload whilst also sidestepping the need to manually manage an intermediate buffer of the +//! // incoming bytes from the tcp stream and writing them to our server with our Nym client. +//! let codec = BytesCodec::new(); +//! let mut framed_read = FramedRead::new(read, codec); +//! // Much like the tcpstream, split our Nym client into a sender and receiver for concurrent read/write +//! let sender = client.split_sender(); +//! // The server / service provider address our client is sending messages to will remain static +//! let server_addr = server_address; +//! // Store outgoing messages in instance of Dashset abstraction +//! let messages_account = Arc::new(DashSet::new()); +//! // Wrap in an Arc for memsafe concurrent access +//! let sent_messages_account = Arc::clone(&messages_account); +//! +//! // 'Outgoing' thread +//! tokio::spawn(async move { +//! let mut message_id = 0; +//! // While able to read from OwnedReadHalf of TcpStream: +//! // - increment our messageID - we need to ensure message ordering on both client and server. +//! // - create instance of ProxiedMessage abstraction with framed bytes: this is really just the message data payload in the form of those bytes +//! // & session and messageIDs. +//! // - Serialise + send message through the mixnet to the Service Provider. +//! // - Repeat these steps, but sending a message with a payload containing a Close signal for this session; since we have message ordering implemented +//! // we can fire off the session close signal without having to wait on making sure the server has received the rest of the messages. +//! // - Trigger our session timeout alert in the 'Incoming' thread select! loop via tx end of our oneshot channel. +//! while let Some(Ok(bytes)) = framed_read.next().await { +//! message_id += 1; +//! sent_messages_account.insert(message_id); +//! let message = +//! ProxiedMessage::new(Payload::Data(bytes.to_vec()), session_id, message_id); +//! let coded_message = bincode::serialize(&message)?; +//! sender +//! .send_message(server_addr, &coded_message, IncludedSurbs::Amount(100)) +//! .await?; +//! info!( +//! "Sent message with id {} for session {} of {} bytes", +//! message_id, +//! session_id, +//! bytes.len() +//! ); +//! } +//! message_id += 1; +//! let message = ProxiedMessage::new(Payload::Close, session_id, message_id); +//! +//! let coded_message = bincode::serialize(&message)?; +//! sender +//! .send_message(server_addr, &coded_message, IncludedSurbs::Amount(100)) +//! .await?; +//! +//! info!("Closing read end of session: {}", session_id); +//! tx.send(true) +//! .map_err(|_| anyhow::anyhow!("Could not send close signal"))?; +//! Ok::<(), anyhow::Error>(()) +//! }); +//! +//! // 'Incoming' thread +//! tokio::spawn(async move { +//! // Abstraction containing logic ordering: all our incoming messages need to be parsed based on their messageIDs per session. +//! // All the message-ordering and time-tracking methods are defined in utils.rs, mostly used in .tick(). +//! let mut msg_buffer = MessageBuffer::new(); +//! // Select!-ing one of following options: +//! // - rx is triggered by tx to log the session will end in ARGS.close_timeout time, break from this loop to pass to loop below +//! // - Deserialise incoming mixnet message, push to msg buffer and tick() to order and write to OwnedWriteHalf. +//! // - If the cancel_token is in cancelled state, break and kick down to the loop below. +//! // - Call tick() once per 100ms if neither of the above have occurred. +//! loop { +//! tokio::select! { +//! _ = &mut rx => { +//! info!("Closing write end of session: {} in {} seconds", session_id, close_timeout); +//! break +//! } +//! Some(message) = client.next() => { +//! let message = bincode::deserialize::(&message.message)?; +//! msg_buffer.push(message); +//! msg_buffer.tick(&mut write).await?; +//! }, +//! _ = cancel_token.cancelled() => { +//! info!("CTRL_C triggered in thread, triggering loop shutdown"); +//! break +//! }, +//! _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => { +//! msg_buffer.tick(&mut write).await?; +//! } +//! } +//! } +//! // Select!-ing one of following options: +//! // - Deserialise incoming mixnet message, push to msg buffer and tick() to order and write next messageID in line to OwnedWriteHalf. +//! // - If the cancel_token is in cancelled state, shutdown client for this thread. +//! // - Sleep for session timeout and return, kills thread with Ok(()). +//! loop { +//! tokio::select! { +//! Some(message) = client.next() => { +//! let message = bincode::deserialize::(&message.message)?; +//! msg_buffer.push(message); +//! msg_buffer.tick(&mut write).await?; +//! }, +//! _ = cancel_token.cancelled() => { +//! info!("CTRL_C triggered in thread, triggering client shutdown"); +//! client.disconnect().await; +//! return Ok::<(), anyhow::Error>(()) +//! }, +//! _ = tokio::time::sleep(tokio::time::Duration::from_secs(close_timeout)) => { +//! info!("Closing write end of session: {}", session_id); +//! info!("Triggering client shutdown"); +//! client.disconnect().await; +//! return Ok::<(), anyhow::Error>(()) +//! }, +//! } +//! } +//! }); +//! Ok(()) +//! } //! } +//! ``` mod tcp_proxy_client; mod tcp_proxy_server; +pub mod utils; pub use tcp_proxy_client::NymProxyClient; pub use tcp_proxy_server::NymProxyServer; +pub use utils::{DecayWrapper, MessageBuffer, Payload, ProxiedMessage}; diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs b/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs index 44686b438a..6633ce87a5 100644 --- a/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs +++ b/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs @@ -1,5 +1,6 @@ use anyhow::Result; use clap::Parser; +use nym_crypto::asymmetric::ed25519; use nym_sdk::tcp_proxy; #[derive(Parser, Debug)] @@ -15,11 +16,14 @@ struct Args { /// Optional env filepath - if none is supplied then the proxy defaults to using mainnet else just use a path to one of the supplied files in envs/ e.g. ./envs/sandbox.env #[clap(short, long)] env_path: Option, + + #[clap(short, long)] + gateway: Option, } #[tokio::main] async fn main() -> Result<()> { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); let args = Args::parse(); @@ -30,6 +34,7 @@ async fn main() -> Result<()> { &args.upstream_tcp_address, &conf_path, args.env_path.clone(), + args.gateway, ) .await?; diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs b/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs index 46ec3eceb5..85f9682e62 100644 --- a/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs +++ b/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs @@ -1,12 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use crate::client_pool::ClientPool; use crate::mixnet::{IncludedSurbs, MixnetClientBuilder, MixnetMessageSender, NymNetworkDetails}; -use std::sync::Arc; -#[path = "utils.rs"] -mod utils; +use crate::tcp_proxy::utils::{MessageBuffer, Payload, ProxiedMessage}; use anyhow::Result; use dashmap::DashSet; use nym_network_defaults::setup_env; use nym_sphinx::addressing::Recipient; +use std::sync::Arc; use tokio::{ net::{TcpListener, TcpStream}, sync::oneshot, @@ -15,7 +17,6 @@ use tokio_stream::StreamExt; use tokio_util::codec::{BytesCodec, FramedRead}; use tokio_util::sync::CancellationToken; use tracing::{debug, info, instrument}; -use utils::{MessageBuffer, Payload, ProxiedMessage}; const DEFAULT_CLOSE_TIMEOUT: u64 = 60; // seconds const DEFAULT_LISTEN_HOST: &str = "127.0.0.1"; diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_server.rs b/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_server.rs index 55e6273dbb..a45ca51363 100644 --- a/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_server.rs +++ b/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_server.rs @@ -1,9 +1,13 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use crate::mixnet::{ AnonymousSenderTag, MixnetClient, MixnetClientBuilder, MixnetClientSender, MixnetMessageSender, NymNetworkDetails, StoragePaths, }; use anyhow::Result; use dashmap::DashSet; +use nym_crypto::asymmetric::ed25519; use nym_network_defaults::setup_env; use nym_sphinx::addressing::Recipient; use std::path::PathBuf; @@ -12,6 +16,7 @@ use tokio::net::TcpStream; use tokio::sync::watch::Receiver; use tokio::sync::RwLock; use tokio_stream::StreamExt; +use tokio_util::sync::CancellationToken; use tracing::{debug, error, info}; #[allow(clippy::duplicate_mod)] #[path = "utils.rs"] @@ -26,6 +31,9 @@ pub struct NymProxyServer { mixnet_client_sender: Arc>, tx: tokio::sync::watch::Sender>, rx: tokio::sync::watch::Receiver>, + cancel_token: CancellationToken, + shutdown_tx: tokio::sync::mpsc::Sender<()>, + shutdown_rx: tokio::sync::mpsc::Receiver<()>, } impl NymProxyServer { @@ -33,20 +41,31 @@ impl NymProxyServer { upstream_address: &str, config_dir: &str, env: Option, + gateway: Option, ) -> Result { info!("Creating client"); // We're wanting to build a client with a constant address, vs the ephemeral in-memory data storage of the NymProxyClient clients. // Following a builder pattern, having to manually connect to the mixnet below. + // Optional selectable Gateway to use. let config_dir = PathBuf::from(config_dir); debug!("Loading env file: {:?}", env); setup_env(env); // Defaults to mainnet if empty let net = NymNetworkDetails::new_from_env(); let storage_paths = StoragePaths::new_from_dir(&config_dir)?; - let client = MixnetClientBuilder::new_with_default_storage(storage_paths) - .await? - .network_details(net) - .build()?; + + let client = if let Some(gateway) = gateway { + MixnetClientBuilder::new_with_default_storage(storage_paths) + .await? + .network_details(net) + .request_gateway(gateway.to_string()) + .build()? + } else { + MixnetClientBuilder::new_with_default_storage(storage_paths) + .await? + .network_details(net) + .build()? + }; let client = client.connect_to_mixnet().await?; @@ -57,6 +76,9 @@ impl NymProxyServer { let (tx, rx) = tokio::sync::watch::channel::>(None); + // Our shutdown signal channel + let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel(1); + info!("Client created: {}", client.nym_address()); Ok(NymProxyServer { @@ -66,31 +88,82 @@ impl NymProxyServer { mixnet_client_sender: sender, tx, rx, + cancel_token: CancellationToken::new(), + shutdown_tx, + shutdown_rx, }) } - pub fn nym_address(&self) -> &Recipient { - self.mixnet_client.nym_address() - } + pub async fn run_with_shutdown(&mut self) -> Result<()> { + let handle_token = self.cancel_token.child_token(); + let upstream_address = self.upstream_address.clone(); + let rx = self.rx(); + let mixnet_sender = self.mixnet_client_sender(); + let tx = self.tx.clone(); + let session_map = self.session_map().clone(); - pub fn mixnet_client_mut(&mut self) -> &mut MixnetClient { - &mut self.mixnet_client - } + let mut shutdown_rx = + std::mem::replace(&mut self.shutdown_rx, tokio::sync::mpsc::channel(1).1); - pub fn session_map(&self) -> &DashSet { - &self.session_map - } + // Then get the message stream: poll this for incoming messages + let message_stream = self.mixnet_client_mut(); - pub fn mixnet_client_sender(&self) -> Arc> { - Arc::clone(&self.mixnet_client_sender) - } + loop { + tokio::select! { + Some(()) = shutdown_rx.recv() => { + debug!("Received shutdown signal, stopping TcpProxyServer"); + handle_token.cancel(); + break; + } + // On our Mixnet client getting a new message: + // - Check if the attached sessionID exists. + // - If !sessionID, spawn a new session_handler() task. + // - Send the message down tx => rx in our handler. + message = message_stream.next() => { + if let Some(new_message) = message { + let message: ProxiedMessage = match bincode::deserialize(&new_message.message) { + Ok(msg) => { + debug!("received: {}", msg); + msg + }, + Err(e) => { + error!("Failed to deserialize ProxiedMessage: {}", e); + continue; + } + }; - pub fn tx(&self) -> tokio::sync::watch::Sender> { - self.tx.clone() - } + let session_id = message.session_id(); - pub fn rx(&self) -> tokio::sync::watch::Receiver> { - self.rx.clone() + if session_map.insert(session_id) { + debug!("Got message for a new session"); + + tokio::spawn(Self::session_handler( + upstream_address.clone(), + session_id, + rx.clone(), + mixnet_sender.clone(), + handle_token.clone() + )); + + info!("Spawned a new session handler: {}", session_id); + } + + debug!("Sending message for session {}", session_id); + + if let Some(sender_tag) = new_message.sender_tag { + if let Err(e) = tx.send(Some((message, sender_tag))) { + error!("Failed to send ProxiedMessage: {}", e); + } + } else { + error!("No sender tag found, we can't send a reply without it!"); + } + } + } + } + } + + self.shutdown_rx = shutdown_rx; + Ok(()) } // The main body of our logic, triggered on each received new sessionID. To deal with assumptions about @@ -110,6 +183,7 @@ impl NymProxyServer { session_id: Uuid, mut rx: Receiver>, sender: Arc>, + cancel_token: CancellationToken, ) -> Result<()> { let global_surb = Arc::new(RwLock::new(None)); let stream = TcpStream::connect(upstream_address).await?; @@ -181,6 +255,9 @@ impl NymProxyServer { } } } + _ = cancel_token.cancelled() => { + break; + } _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => { msg_buffer.tick(&mut write).await?; } @@ -191,39 +268,78 @@ impl NymProxyServer { Ok(()) } - pub async fn run_with_shutdown(&mut self) -> Result<()> { - // On our Mixnet client getting a new message: - // - Check if the attached sessionID exists. - // - If !sessionID, spawn a new session_handler() task. - // - Send the message down tx => rx in our handler. - while let Some(new_message) = &self.mixnet_client_mut().next().await { - let message: ProxiedMessage = bincode::deserialize(&new_message.message)?; - let session_id = message.session_id(); - // If we've already got message from an existing session, continue, else add it to the session mapping and spawn a new handler(). - if self.session_map().contains(&message.session_id()) { - debug!("Got message for an existing session"); - } else { - self.session_map().insert(message.session_id()); - debug!("Got message for a new session"); - tokio::spawn(Self::session_handler( - self.upstream_address.clone(), - session_id, - self.rx(), - self.mixnet_client_sender(), - )); - info!("Spawned a new session handler: {}", message.session_id()); + pub fn disconnect_signal(&self) -> tokio::sync::mpsc::Sender<()> { + self.shutdown_tx.clone() + } + + pub fn nym_address(&self) -> &Recipient { + self.mixnet_client.nym_address() + } + + pub fn mixnet_client_mut(&mut self) -> &mut MixnetClient { + &mut self.mixnet_client + } + + pub fn session_map(&self) -> &DashSet { + &self.session_map + } + + pub fn mixnet_client_sender(&self) -> Arc> { + Arc::clone(&self.mixnet_client_sender) + } + + pub fn tx(&self) -> tokio::sync::watch::Sender> { + self.tx.clone() + } + + pub fn rx(&self) -> tokio::sync::watch::Receiver> { + self.rx.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[tokio::test] + #[ignore] + async fn shutdown_works() -> Result<()> { + let config_dir = TempDir::new()?; + let mut server = match NymProxyServer::new( + "127.0.0.1:8000", + config_dir.path().to_str().unwrap(), + None, // Mainnet + None, // Random gateway + ) + .await + { + Ok(server) => server, + Err(err) => { + error!("{err}"); + // this is not an ideal way of checking it, but if test fails due to networking failures + // it should be fine to progress + if err.to_string().contains("nym api request failed") { + return Ok(()); + } + return Err(err); } + }; - debug!("Sending message for session {}", message.session_id()); + // Getter for shutdown signal tx + let shutdown_tx = server.disconnect_signal(); - if let Some(sender_tag) = new_message.sender_tag { - self.tx.send(Some((message, sender_tag)))? - } else { - error!("No sender tag found, we can't send a reply without it!") - } - } + let server_handle = tokio::spawn(async move { server.run_with_shutdown().await }); + + // Let it start up + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + + // Kill server + shutdown_tx.send(()).await?; + + // Wait for shutdown in handle + check Result != err + server_handle.await??; - tokio::signal::ctrl_c().await?; Ok(()) } } diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/utils.rs b/sdk/rust/nym-sdk/src/tcp_proxy/utils.rs index 884a63f2ab..1e8085aa83 100644 --- a/sdk/rust/nym-sdk/src/tcp_proxy/utils.rs +++ b/sdk/rust/nym-sdk/src/tcp_proxy/utils.rs @@ -1,7 +1,9 @@ -use std::{collections::HashSet, fmt, ops::Deref, time::Instant}; +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only use anyhow::Result; use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, fmt, ops::Deref, time::Instant}; use tokio::{io::AsyncWriteExt as _, net::tcp::OwnedWriteHalf}; use tracing::{debug, info}; use uuid::Uuid; @@ -11,7 +13,7 @@ const DEFAULT_DECAY: u64 = 6; // decay time in seconds // Keeps track of // - incoming and unsorted messages wrapped in DecayWrapper for keeping track of when they were received // - the expected next message ID (reset on .tick()) -#[derive(Debug)] +#[derive(Debug, Default)] pub struct MessageBuffer { buffer: Vec>, next_msg_id: u16, @@ -45,7 +47,7 @@ impl MessageBuffer { self.buffer.is_empty() } - pub fn iter(&self) -> std::slice::Iter> { + pub fn iter(&self) -> std::slice::Iter<'_, DecayWrapper> { self.buffer.iter() } @@ -148,9 +150,9 @@ impl Deref for DecayWrapper { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ProxiedMessage { - message: Payload, - session_id: Uuid, - message_id: u16, + pub message: Payload, + pub session_id: Uuid, + pub message_id: u16, } impl ProxiedMessage { @@ -175,7 +177,7 @@ impl ProxiedMessage { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub enum Payload { Data(Vec), Close, diff --git a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json index 6a334e69d7..f3c7b6aad6 100644 --- a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json +++ b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json @@ -1058,9 +1058,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", + "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", "license": "MIT", "dependencies": { "pump": "^3.0.0", diff --git a/service-providers/authenticator/Cargo.toml b/service-providers/authenticator/Cargo.toml deleted file mode 100644 index aed5a68870..0000000000 --- a/service-providers/authenticator/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "nym-authenticator" -version = "0.1.0" -authors.workspace = true -repository.workspace = true -homepage.workspace = true -documentation.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -anyhow = { workspace = true } -bincode = { workspace = true } -bs58 = { workspace = true } -bytes = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } -defguard_wireguard_rs = { workspace = true } -fastrand = { workspace = true } -futures = { workspace = true } -ipnetwork = { workspace = true } -log = { workspace = true } -rand = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "net"] } -tokio-stream = { workspace = true } -tokio-util = { workspace = true, features = ["codec"] } -url = { workspace = true } - -nym-authenticator-requests = { path = "../../common/authenticator-requests" } -nym-bin-common = { path = "../../common/bin-common", features = [ - "clap", - "output_format", -] } -nym-client-core = { path = "../../common/client-core", features = ["cli"] } -nym-config = { path = "../../common/config" } -nym-credentials-interface = { path = "../../common/credentials-interface" } -nym-credential-verification = { path = "../../common/credential-verification" } -nym-crypto = { path = "../../common/crypto" } -nym-gateway-requests = { path = "../../common/gateway-requests" } -nym-gateway-storage = { path = "../../common/gateway-storage" } -nym-id = { path = "../../common/nym-id" } -nym-network-defaults = { path = "../../common/network-defaults" } -nym-sdk = { path = "../../sdk/rust/nym-sdk" } -nym-service-providers-common = { path = "../common" } -nym-service-provider-requests-common = { path = "../../common/service-provider-requests-common" } -nym-sphinx = { path = "../../common/nymsphinx" } -nym-task = { path = "../../common/task" } -nym-types = { path = "../../common/types" } -nym-wireguard = { path = "../../common/wireguard" } -nym-wireguard-types = { path = "../../common/wireguard-types" } diff --git a/service-providers/authenticator/src/cli/add_gateway.rs b/service-providers/authenticator/src/cli/add_gateway.rs deleted file mode 100644 index 397a8be01e..0000000000 --- a/service-providers/authenticator/src/cli/add_gateway.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_bin_common::output_format::OutputFormat; -use nym_client_core::cli_helpers::client_add_gateway::{add_gateway, CommonClientAddGatewayArgs}; - -#[derive(clap::Args)] -pub(crate) struct Args { - #[command(flatten)] - common_args: CommonClientAddGatewayArgs, - - #[arg(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -impl AsRef for Args { - fn as_ref(&self) -> &CommonClientAddGatewayArgs { - &self.common_args - } -} - -pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> { - let output = args.output; - let res = add_gateway::(args, None).await?; - - println!("{}", output.format(&res)); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/ecash/import_coin_index_signatures.rs b/service-providers/authenticator/src/cli/ecash/import_coin_index_signatures.rs deleted file mode 100644 index c3b1aa922e..0000000000 --- a/service-providers/authenticator/src/cli/ecash/import_coin_index_signatures.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_import_coin_index_signatures::{ - import_coin_index_signatures, CommonClientImportCoinIndexSignaturesArgs, -}; - -pub(crate) async fn execute( - args: CommonClientImportCoinIndexSignaturesArgs, -) -> Result<(), AuthenticatorError> { - import_coin_index_signatures::(args).await?; - println!("successfully imported coin index signatures!"); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/ecash/import_credential.rs b/service-providers/authenticator/src/cli/ecash/import_credential.rs deleted file mode 100644 index e525eac518..0000000000 --- a/service-providers/authenticator/src/cli/ecash/import_credential.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_import_credential::{ - import_credential, CommonClientImportTicketBookArgs, -}; - -pub async fn execute(args: CommonClientImportTicketBookArgs) -> Result<(), AuthenticatorError> { - import_credential::(args).await?; - println!("successfully imported credential!"); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/ecash/import_expiration_date_signatures.rs b/service-providers/authenticator/src/cli/ecash/import_expiration_date_signatures.rs deleted file mode 100644 index 7db61d5238..0000000000 --- a/service-providers/authenticator/src/cli/ecash/import_expiration_date_signatures.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_import_expiration_date_signatures::{ - import_expiration_date_signatures, CommonClientImportExpirationDateSignaturesArgs, -}; - -pub(crate) async fn execute( - args: CommonClientImportExpirationDateSignaturesArgs, -) -> Result<(), AuthenticatorError> { - import_expiration_date_signatures::(args).await?; - println!("successfully imported expiration date signatures!"); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/ecash/import_master_verification_key.rs b/service-providers/authenticator/src/cli/ecash/import_master_verification_key.rs deleted file mode 100644 index d53ddacf4c..0000000000 --- a/service-providers/authenticator/src/cli/ecash/import_master_verification_key.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_import_master_verification_key::{ - import_master_verification_key, CommonClientImportMasterVerificationKeyArgs, -}; - -pub(crate) async fn execute( - args: CommonClientImportMasterVerificationKeyArgs, -) -> Result<(), AuthenticatorError> { - import_master_verification_key::(args).await?; - println!("successfully imported master verification key!"); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/ecash/mod.rs b/service-providers/authenticator/src/cli/ecash/mod.rs deleted file mode 100644 index 9272ed013c..0000000000 --- a/service-providers/authenticator/src/cli/ecash/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use clap::{Args, Subcommand}; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_import_coin_index_signatures::CommonClientImportCoinIndexSignaturesArgs; -use nym_client_core::cli_helpers::client_import_credential::CommonClientImportTicketBookArgs; -use nym_client_core::cli_helpers::client_import_expiration_date_signatures::CommonClientImportExpirationDateSignaturesArgs; -use nym_client_core::cli_helpers::client_import_master_verification_key::CommonClientImportMasterVerificationKeyArgs; - -pub(crate) mod import_coin_index_signatures; -pub(crate) mod import_credential; -pub(crate) mod import_expiration_date_signatures; -pub(crate) mod import_master_verification_key; -pub(crate) mod show_ticketbooks; - -#[derive(Args)] -#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct Ecash { - #[clap(subcommand)] - pub command: EcashCommands, -} - -impl Ecash { - pub async fn execute(self) -> Result<(), AuthenticatorError> { - match self.command { - EcashCommands::ShowTicketBooks(args) => show_ticketbooks::execute(args).await?, - EcashCommands::ImportTicketBook(args) => import_credential::execute(args).await?, - EcashCommands::ImportCoinIndexSignatures(args) => { - import_coin_index_signatures::execute(args).await? - } - EcashCommands::ImportExpirationDateSignatures(args) => { - import_expiration_date_signatures::execute(args).await? - } - EcashCommands::ImportMasterVerificationKey(args) => { - import_master_verification_key::execute(args).await? - } - } - Ok(()) - } -} - -#[derive(Subcommand)] -pub enum EcashCommands { - /// Display information associated with the imported ticketbooks, - ShowTicketBooks(show_ticketbooks::Args), - - /// Import a pre-generated ticketbook - ImportTicketBook(CommonClientImportTicketBookArgs), - - /// Import coin index signatures needed for ticketbooks - ImportCoinIndexSignatures(CommonClientImportCoinIndexSignaturesArgs), - - /// Import expiration date signatures needed for ticketbooks - ImportExpirationDateSignatures(CommonClientImportExpirationDateSignaturesArgs), - - /// Import master verification key needed for ticketbooks - ImportMasterVerificationKey(CommonClientImportMasterVerificationKeyArgs), -} diff --git a/service-providers/authenticator/src/cli/ecash/show_ticketbooks.rs b/service-providers/authenticator/src/cli/ecash/show_ticketbooks.rs deleted file mode 100644 index ca31d39802..0000000000 --- a/service-providers/authenticator/src/cli/ecash/show_ticketbooks.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_bin_common::output_format::OutputFormat; -use nym_client_core::cli_helpers::client_show_ticketbooks::{ - show_ticketbooks, CommonShowTicketbooksArgs, -}; - -#[derive(clap::Args)] -pub(crate) struct Args { - #[command(flatten)] - common_args: CommonShowTicketbooksArgs, - - #[arg(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -impl AsRef for Args { - fn as_ref(&self) -> &CommonShowTicketbooksArgs { - &self.common_args - } -} - -pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> { - let output = args.output; - let res = show_ticketbooks::(args).await?; - - println!("{}", output.format(&res)); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/init.rs b/service-providers/authenticator/src/cli/init.rs deleted file mode 100644 index 217c3160d0..0000000000 --- a/service-providers/authenticator/src/cli/init.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::{override_config, CliAuthenticatorClient, OverrideConfig}; -use clap::Args; -use nym_authenticator::{ - config::{default_config_directory, default_config_filepath, default_data_directory, Config}, - error::AuthenticatorError, -}; -use nym_bin_common::output_format::OutputFormat; -use nym_client_core::cli_helpers::client_init::{ - initialise_client, CommonClientInitArgs, InitResultsWithConfig, InitialisableClient, -}; -use serde::Serialize; -use std::{fmt::Display, fs, path::PathBuf}; - -impl InitialisableClient for CliAuthenticatorClient { - type InitArgs = Init; - - fn initialise_storage_paths(id: &str) -> Result<(), Self::Error> { - fs::create_dir_all(default_data_directory(id))?; - fs::create_dir_all(default_config_directory(id))?; - Ok(()) - } - - fn default_config_path(id: &str) -> PathBuf { - default_config_filepath(id) - } - - fn construct_config(init_args: &Self::InitArgs) -> Self::Config { - override_config( - Config::new(&init_args.common_args.id), - OverrideConfig::from(init_args.clone()), - ) - } -} - -#[derive(Args, Clone, Debug)] -pub(crate) struct Init { - #[command(flatten)] - common_args: CommonClientInitArgs, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -impl From for OverrideConfig { - fn from(init_config: Init) -> Self { - OverrideConfig { - nym_apis: init_config.common_args.nym_apis, - nyxd_urls: init_config.common_args.nyxd_urls, - enabled_credentials_mode: init_config.common_args.enabled_credentials_mode, - } - } -} - -impl AsRef for Init { - fn as_ref(&self) -> &CommonClientInitArgs { - &self.common_args - } -} - -#[derive(Debug, Serialize)] -pub struct InitResults { - #[serde(flatten)] - client_core: nym_client_core::init::types::InitResults, - client_address: String, -} - -impl InitResults { - fn new(res: InitResultsWithConfig) -> Self { - Self { - client_address: res.init_results.address.to_string(), - client_core: res.init_results, - } - } -} - -impl Display for InitResults { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{}", self.client_core)?; - write!(f, "Address of this authenticator: {}", self.client_address) - } -} - -pub(crate) async fn execute(args: Init) -> Result<(), AuthenticatorError> { - eprintln!("Initialising client..."); - - let output = args.output; - let res = initialise_client::(args, None).await?; - - let init_results = InitResults::new(res); - println!("{}", output.format(&init_results)); - - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/list_gateways.rs b/service-providers/authenticator/src/cli/list_gateways.rs deleted file mode 100644 index 9297d5cd5b..0000000000 --- a/service-providers/authenticator/src/cli/list_gateways.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_bin_common::output_format::OutputFormat; -use nym_client_core::cli_helpers::client_list_gateways::{ - list_gateways, CommonClientListGatewaysArgs, -}; - -#[derive(clap::Args)] -pub(crate) struct Args { - #[command(flatten)] - common_args: CommonClientListGatewaysArgs, - - #[arg(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -impl AsRef for Args { - fn as_ref(&self) -> &CommonClientListGatewaysArgs { - &self.common_args - } -} - -pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> { - let output = args.output; - let res = list_gateways::(args).await?; - - println!("{}", output.format(&res)); - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/mod.rs b/service-providers/authenticator/src/cli/mod.rs deleted file mode 100644 index 08aca6f720..0000000000 --- a/service-providers/authenticator/src/cli/mod.rs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::ecash::Ecash; -use clap::{CommandFactory, Parser, Subcommand}; -use log::error; -use nym_authenticator::{ - config::{helpers::try_upgrade_config, BaseClientConfig, Config}, - error::AuthenticatorError, -}; -use nym_bin_common::bin_info; -use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_client_core::cli_helpers::CliClient; -use std::sync::OnceLock; - -mod add_gateway; -mod build_info; -pub mod ecash; -mod init; -mod list_gateways; -mod peer_handler; -mod request; -mod run; -mod sign; -mod switch_gateway; - -pub(crate) struct CliAuthenticatorClient; - -impl CliClient for CliAuthenticatorClient { - const NAME: &'static str = "authenticator"; - type Error = AuthenticatorError; - type Config = Config; - - async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { - try_upgrade_config(id).await - } - - async fn try_load_current_config(id: &str) -> Result { - try_load_current_config(id).await - } -} - -fn pretty_build_info_static() -> &'static str { - static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); - PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) -} - -#[derive(Parser)] -#[command(author = "Nymtech", version, about, long_version = pretty_build_info_static())] -pub(crate) struct Cli { - /// Path pointing to an env file that configures the client. - #[arg(short, long)] - pub(crate) config_env_file: Option, - - /// Flag used for disabling the printed banner in tty. - #[arg(long)] - pub(crate) no_banner: bool, - - #[command(subcommand)] - command: Commands, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Subcommand)] -pub(crate) enum Commands { - /// Initialize an authenticator. Do this first! - Init(init::Init), - - /// Run the authenticator with the provided configuration and optionally override - /// parameters. - Run(run::Run), - - /// Make a dummy request to a running authenticator - Request(request::Request), - - /// Ecash-related functionalities - Ecash(Ecash), - - /// List all registered with gateways - ListGateways(list_gateways::Args), - - /// Add new gateway to this client - AddGateway(add_gateway::Args), - - /// Change the currently active gateway. Note that you must have already registered with the new gateway! - SwitchGateway(switch_gateway::Args), - - /// Sign to prove ownership of this authenticator - Sign(sign::Sign), - - /// Show build information of this binary - BuildInfo(build_info::BuildInfo), - - /// Generate shell completions - Completions(ArgShell), - - /// Generate Fig specification - GenerateFigSpec, -} - -// Configuration that can be overridden. -pub(crate) struct OverrideConfig { - nym_apis: Option>, - nyxd_urls: Option>, - enabled_credentials_mode: Option, -} - -pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { - config - .with_optional_base_custom_env( - BaseClientConfig::with_custom_nym_apis, - args.nym_apis, - nym_network_defaults::var_names::NYM_API, - nym_config::parse_urls, - ) - .with_optional_base_custom_env( - BaseClientConfig::with_custom_nyxd, - args.nyxd_urls, - nym_network_defaults::var_names::NYXD, - nym_config::parse_urls, - ) - .with_optional_base( - BaseClientConfig::with_disabled_credentials, - args.enabled_credentials_mode.map(|b| !b), - ) -} - -pub(crate) async fn execute(args: Cli) -> Result<(), AuthenticatorError> { - let bin_name = "nym-authenticator"; - - match args.command { - Commands::Init(m) => init::execute(m).await?, - Commands::Run(m) => run::execute(&m).await?, - Commands::Request(r) => request::execute(&r).await?, - Commands::Ecash(ecash) => ecash.execute().await?, - Commands::ListGateways(args) => list_gateways::execute(args).await?, - Commands::AddGateway(args) => add_gateway::execute(args).await?, - Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, - Commands::Sign(m) => sign::execute(&m).await?, - Commands::BuildInfo(m) => build_info::execute(m), - Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), - Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), - } - Ok(()) -} - -async fn try_load_current_config(id: &str) -> Result { - // try to load the config as is - if let Ok(cfg) = Config::read_from_default_path(id) { - return if !cfg.validate() { - Err(AuthenticatorError::ConfigValidationFailure) - } else { - Ok(cfg) - }; - } - - // we couldn't load it - try upgrading it from older revisions - try_upgrade_config(id).await?; - - let config = match Config::read_from_default_path(id) { - Ok(cfg) => cfg, - Err(err) => { - error!("Failed to load config for {id}. Are you sure you have run `init` before? (Error was: {err})"); - return Err(AuthenticatorError::FailedToLoadConfig(id.to_string())); - } - }; - - if !config.validate() { - return Err(AuthenticatorError::ConfigValidationFailure); - } - - Ok(config) -} diff --git a/service-providers/authenticator/src/cli/peer_handler.rs b/service-providers/authenticator/src/cli/peer_handler.rs deleted file mode 100644 index 99a260ff7e..0000000000 --- a/service-providers/authenticator/src/cli/peer_handler.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_sdk::TaskClient; -use nym_wireguard::peer_controller::{ - AddPeerControlResponse, PeerControlRequest, QueryBandwidthControlResponse, - QueryPeerControlResponse, RemovePeerControlResponse, -}; -use tokio::sync::mpsc; - -pub struct DummyHandler { - peer_rx: mpsc::Receiver, - task_client: TaskClient, -} - -impl DummyHandler { - pub fn new(peer_rx: mpsc::Receiver, task_client: TaskClient) -> Self { - DummyHandler { - peer_rx, - task_client, - } - } - - pub async fn run(mut self) { - while !self.task_client.is_shutdown() { - tokio::select! { - msg = self.peer_rx.recv() => { - if let Some(msg) = msg { - match msg { - PeerControlRequest::AddPeer { peer, client_id, response_tx } => { - log::info!("[DUMMY] Adding peer {:?} with client id {:?}", peer, client_id); - response_tx.send(AddPeerControlResponse { success: true }).ok(); - } - PeerControlRequest::RemovePeer { key, response_tx } => { - log::info!("[DUMMY] Removing peer {:?}", key); - response_tx.send(RemovePeerControlResponse { success: true }).ok(); - } - PeerControlRequest::QueryPeer{key, response_tx} => { - log::info!("[DUMMY] Querying peer {:?}", key); - response_tx.send(QueryPeerControlResponse { success: true, peer: None }).ok(); - } - PeerControlRequest::QueryBandwidth{key, response_tx} => { - log::info!("[DUMMY] Querying bandwidth for peer {:?}", key); - response_tx.send(QueryBandwidthControlResponse { success: true, bandwidth_data: None }).ok(); - } - } - } else { - break; - } - } - - _ = self.task_client.recv() => { - log::trace!("DummyHandler: Received shutdown"); - } - } - } - log::debug!("DummyHandler: Exiting"); - } -} diff --git a/service-providers/authenticator/src/cli/request.rs b/service-providers/authenticator/src/cli/request.rs deleted file mode 100644 index a3f39ddc91..0000000000 --- a/service-providers/authenticator/src/cli/request.rs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::try_load_current_config; -use crate::cli::AuthenticatorError; -use crate::cli::{override_config, OverrideConfig}; -use clap::{Args, Subcommand}; -use nym_authenticator_requests::latest::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, - request::{AuthenticatorRequest, AuthenticatorRequestData}, -}; -use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; -use nym_sdk::mixnet::{MixnetMessageSender, Recipient, TransmissionLane}; -use nym_task::TaskHandle; -use nym_wireguard_types::PeerPublicKey; -use std::net::{Ipv4Addr, Ipv6Addr}; -use std::str::FromStr; -use std::time::Duration; -use tokio::time::sleep; - -#[allow(clippy::struct_excessive_bools)] -#[derive(Args, Clone)] -pub(crate) struct Request { - #[command(flatten)] - common_args: CommonClientRunArgs, - - #[command(subcommand)] - request: RequestType, - - authenticator_recipient: String, -} - -impl From for OverrideConfig { - fn from(request_config: Request) -> Self { - OverrideConfig { - nym_apis: None, - nyxd_urls: request_config.common_args.nyxd_urls, - enabled_credentials_mode: request_config.common_args.enabled_credentials_mode, - } - } -} - -#[derive(Clone, Subcommand)] -pub(crate) enum RequestType { - Initial(Initial), - Final(Final), - QueryBandwidth(QueryBandwidth), -} - -#[derive(Args, Clone, Debug)] -pub(crate) struct Initial { - pub_key: String, -} - -#[derive(Args, Clone, Debug)] -pub(crate) struct Final { - pub_key: String, - private_ipv4: String, - private_ipv6: String, - mac: String, -} - -#[derive(Args, Clone, Debug)] -pub(crate) struct QueryBandwidth { - pub_key: String, -} - -impl TryFrom for AuthenticatorRequestData { - type Error = AuthenticatorError; - - fn try_from(value: RequestType) -> Result { - let ret = match value { - RequestType::Initial(req) => AuthenticatorRequestData::Initial(InitMessage::new( - PeerPublicKey::from_str(&req.pub_key)?, - )), - RequestType::Final(req) => AuthenticatorRequestData::Final(Box::new(FinalMessage { - gateway_client: GatewayClient { - pub_key: PeerPublicKey::from_str(&req.pub_key)?, - private_ips: IpPair::new( - Ipv4Addr::from_str(&req.private_ipv4)?, - Ipv6Addr::from_str(&req.private_ipv6)?, - ), - mac: ClientMac::from_str(&req.mac)?, - }, - credential: None, - })), - RequestType::QueryBandwidth(req) => { - AuthenticatorRequestData::QueryBandwidth(PeerPublicKey::from_str(&req.pub_key)?) - } - }; - Ok(ret) - } -} - -pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> { - let mut config = try_load_current_config(&args.common_args.id).await?; - config = override_config(config, OverrideConfig::from(args.clone())); - - let shutdown = TaskHandle::default(); - let mixnet_client = nym_authenticator::mixnet_client::create_mixnet_client( - &config.base, - shutdown.get_handle().named("nym_sdk::MixnetClient"), - None, - None, - false, - &config.storage_paths.common_paths, - ) - .await?; - - let request_data = AuthenticatorRequestData::try_from(args.request.clone())?; - let authenticator_recipient = Recipient::from_str(&args.authenticator_recipient)?; - let (request, _) = match request_data { - AuthenticatorRequestData::Initial(init_message) => { - AuthenticatorRequest::new_initial_request(init_message) - } - AuthenticatorRequestData::Final(final_message) => { - AuthenticatorRequest::new_final_request(*final_message) - } - AuthenticatorRequestData::QueryBandwidth(query_message) => { - AuthenticatorRequest::new_query_request(query_message) - } - AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - AuthenticatorRequest::new_topup_request(*top_up_message) - } - }; - mixnet_client - .split_sender() - .send(nym_sdk::mixnet::InputMessage::new_regular( - authenticator_recipient, - request.to_bytes().unwrap(), - TransmissionLane::General, - None, - )) - .await - .map_err(|source| AuthenticatorError::FailedToSendPacketToMixnet { source })?; - - log::info!("Sent request, sleeping 60 seconds or until killed"); - sleep(Duration::from_secs(60)).await; - - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/run.rs b/service-providers/authenticator/src/cli/run.rs deleted file mode 100644 index 67dc226523..0000000000 --- a/service-providers/authenticator/src/cli/run.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::peer_handler::DummyHandler; -use crate::cli::try_load_current_config; -use crate::cli::{override_config, OverrideConfig}; -use clap::Args; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; -use nym_crypto::asymmetric::x25519::KeyPair; -use nym_task::TaskHandle; -use nym_wireguard::WireguardGatewayData; -use rand::rngs::OsRng; -use std::sync::Arc; - -#[allow(clippy::struct_excessive_bools)] -#[derive(Args, Clone)] -pub(crate) struct Run { - #[command(flatten)] - common_args: CommonClientRunArgs, -} - -impl From for OverrideConfig { - fn from(run_config: Run) -> Self { - OverrideConfig { - nym_apis: None, - nyxd_urls: run_config.common_args.nyxd_urls, - enabled_credentials_mode: run_config.common_args.enabled_credentials_mode, - } - } -} - -pub(crate) async fn execute(args: &Run) -> Result<(), AuthenticatorError> { - let mut config = try_load_current_config(&args.common_args.id).await?; - config = override_config(config, OverrideConfig::from(args.clone())); - log::debug!("Using config: {:#?}", config); - - log::info!("Starting authenticator service provider"); - let (wireguard_gateway_data, peer_rx) = WireguardGatewayData::new( - config.authenticator.clone().into(), - Arc::new(KeyPair::new(&mut OsRng)), - ); - let task_handler = TaskHandle::default(); - let handler = DummyHandler::new(peer_rx, task_handler.fork("peer_handler")); - tokio::spawn(async move { - handler.run().await; - }); - - let mut server = nym_authenticator::Authenticator::new(config, wireguard_gateway_data, vec![]); - if let Some(custom_mixnet) = &args.common_args.custom_mixnet { - server = server.with_stored_topology(custom_mixnet)? - } - - server.run_service_provider().await -} diff --git a/service-providers/authenticator/src/cli/sign.rs b/service-providers/authenticator/src/cli/sign.rs deleted file mode 100644 index 30bd8ed1d1..0000000000 --- a/service-providers/authenticator/src/cli/sign.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::try_load_current_config; -use clap::Args; -use nym_authenticator::error::AuthenticatorError; -use nym_bin_common::output_format::OutputFormat; -use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::error::ClientCoreError; -use nym_crypto::asymmetric::ed25519; -use nym_types::helpers::ConsoleSigningOutput; - -#[derive(Args, Clone)] -pub(crate) struct Sign { - /// The id of the mixnode you want to sign with - #[arg(long)] - id: String, - - /// Signs a transaction-specific payload, that is going to be sent to the smart contract, with your identity key - #[arg(long)] - contract_msg: String, - - #[arg(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -fn print_signed_contract_msg( - private_key: &ed25519::PrivateKey, - raw_msg: &str, - output: OutputFormat, -) { - let trimmed = raw_msg.trim(); - eprintln!(">>> attempting to sign {trimmed}"); - - let Ok(decoded) = bs58::decode(trimmed).into_vec() else { - println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); - return; - }; - - eprintln!(">>> decoding the message..."); - - // we don't really care about what particular information is embedded inside of it, - // we just want to know if user correctly copied the string, i.e. whether it's a valid bs58 encoded json - if serde_json::from_slice::(&decoded).is_err() { - println!("it seems you have incorrectly copied the message to sign. Make sure you didn't accidentally skip any characters"); - return; - }; - - // if this is a valid json, it MUST be a valid string - let decoded_string = String::from_utf8(decoded.clone()).unwrap(); - let signature = private_key.sign(&decoded).to_base58_string(); - - let sign_output = ConsoleSigningOutput::new(decoded_string, signature); - println!("{}", output.format(&sign_output)); -} - -pub(crate) async fn execute(args: &Sign) -> Result<(), AuthenticatorError> { - let config = try_load_current_config(&args.id).await?; - - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); - let identity_keypair = key_store.load_identity_keypair().map_err(|source| { - AuthenticatorError::ClientCoreError(ClientCoreError::KeyStoreError { - source: Box::new(source), - }) - })?; - - print_signed_contract_msg( - identity_keypair.private_key(), - &args.contract_msg, - args.output, - ); - - Ok(()) -} diff --git a/service-providers/authenticator/src/cli/switch_gateway.rs b/service-providers/authenticator/src/cli/switch_gateway.rs deleted file mode 100644 index 24ce8d48e2..0000000000 --- a/service-providers/authenticator/src/cli/switch_gateway.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::cli::CliAuthenticatorClient; -use nym_authenticator::error::AuthenticatorError; -use nym_client_core::cli_helpers::client_switch_gateway::{ - switch_gateway, CommonClientSwitchGatewaysArgs, -}; - -#[derive(clap::Args, Clone, Debug)] -pub struct Args { - #[command(flatten)] - common_args: CommonClientSwitchGatewaysArgs, -} - -impl AsRef for Args { - fn as_ref(&self) -> &CommonClientSwitchGatewaysArgs { - &self.common_args - } -} - -pub(crate) async fn execute(args: Args) -> Result<(), AuthenticatorError> { - switch_gateway::(args).await -} diff --git a/service-providers/authenticator/src/config/helpers.rs b/service-providers/authenticator/src/config/helpers.rs deleted file mode 100644 index a529d50ae3..0000000000 --- a/service-providers/authenticator/src/config/helpers.rs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use log::trace; -use std::path::Path; - -use crate::error::AuthenticatorError; - -pub async fn try_upgrade_config>(_config_path: P) -> Result<(), AuthenticatorError> { - trace!("Attempting to upgrade config"); - - Ok(()) -} diff --git a/service-providers/authenticator/src/config/mod.rs b/service-providers/authenticator/src/config/mod.rs deleted file mode 100644 index 4b2d11fae9..0000000000 --- a/service-providers/authenticator/src/config/mod.rs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_bin_common::logging::LoggingSettings; -pub use nym_client_core::config::Config as BaseClientConfig; -use nym_client_core::{cli_helpers::CliClientConfig, config::disk_persistence::CommonClientPaths}; -use nym_config::{ - must_get_home, save_formatted_config_to_file, NymConfigTemplate, OptionalSet, - DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, DEFAULT_DATA_DIR, NYM_DIR, -}; -use nym_network_defaults::{ - WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, WG_TUN_DEVICE_NETMASK_V4, - WG_TUN_DEVICE_NETMASK_V6, -}; -use nym_service_providers_common::DEFAULT_SERVICE_PROVIDERS_DIR; -pub use persistence::AuthenticatorPaths; -use serde::{Deserialize, Serialize}; -use std::{ - io, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - path::{Path, PathBuf}, - str::FromStr, -}; -use template::CONFIG_TEMPLATE; - -pub mod helpers; -pub mod persistence; -pub mod template; - -const DEFAULT_AUTHENTICATOR_DIR: &str = "authenticator"; - -/// Derive default path to authenticator's config directory. -/// It should get resolved to `$HOME/.nym/service-providers/authenticator//config` -pub fn default_config_directory>(id: P) -> PathBuf { - must_get_home() - .join(NYM_DIR) - .join(DEFAULT_SERVICE_PROVIDERS_DIR) - .join(DEFAULT_AUTHENTICATOR_DIR) - .join(id) - .join(DEFAULT_CONFIG_DIR) -} - -/// Derive default path to authenticator's config file. -/// It should get resolved to `$HOME/.nym/service-providers/authenticator//config/config.toml` -pub fn default_config_filepath>(id: P) -> PathBuf { - default_config_directory(id).join(DEFAULT_CONFIG_FILENAME) -} - -/// Derive default path to authenticator's data directory where files, such as keys, are stored. -/// It should get resolved to `$HOME/.nym/service-providers/authenticator//data` -pub fn default_data_directory>(id: P) -> PathBuf { - must_get_home() - .join(NYM_DIR) - .join(DEFAULT_SERVICE_PROVIDERS_DIR) - .join(DEFAULT_AUTHENTICATOR_DIR) - .join(id) - .join(DEFAULT_DATA_DIR) -} - -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct Config { - #[serde(flatten)] - pub base: BaseClientConfig, - - #[serde(default)] - pub authenticator: Authenticator, - - pub storage_paths: AuthenticatorPaths, - - pub logging: LoggingSettings, -} - -impl NymConfigTemplate for Config { - fn template(&self) -> &'static str { - CONFIG_TEMPLATE - } -} - -impl CliClientConfig for Config { - fn common_paths(&self) -> &CommonClientPaths { - &self.storage_paths.common_paths - } - - fn core_config(&self) -> &BaseClientConfig { - &self.base - } - - fn default_store_location(&self) -> PathBuf { - self.default_location() - } - - fn save_to>(&self, path: P) -> io::Result<()> { - save_formatted_config_to_file(self, path) - } -} - -impl Config { - pub fn new>(id: S) -> Self { - Config { - base: BaseClientConfig::new(id.as_ref(), env!("CARGO_PKG_VERSION")), - authenticator: Default::default(), - storage_paths: AuthenticatorPaths::new_base(default_data_directory(id.as_ref())), - logging: Default::default(), - } - } - - #[allow(unused)] - pub fn with_data_directory>(mut self, data_directory: P) -> Self { - self.storage_paths = AuthenticatorPaths::new_base(data_directory); - self - } - - pub fn read_from_toml_file>(path: P) -> io::Result { - nym_config::read_config_from_toml_file(path) - } - - pub fn read_from_default_path>(id: P) -> io::Result { - Self::read_from_toml_file(default_config_filepath(id)) - } - - pub fn default_location(&self) -> PathBuf { - default_config_filepath(&self.base.client.id) - } - - #[allow(unused)] - pub fn save_to_default_location(&self) -> io::Result<()> { - let config_save_location: PathBuf = self.default_location(); - save_formatted_config_to_file(self, config_save_location) - } - - pub fn validate(&self) -> bool { - // no other sections have explicit requirements (yet) - self.base.validate() - } - - #[doc(hidden)] - pub fn set_no_poisson_process(&mut self) { - self.base.set_no_poisson_process() - } - - // poor man's 'builder' method - #[allow(unused)] - pub fn with_base(mut self, f: F, val: T) -> Self - where - F: Fn(BaseClientConfig, T) -> BaseClientConfig, - { - self.base = f(self.base, val); - self - } - - // helper methods to use `OptionalSet` trait. Those are defined due to very... ehm. 'specific' structure of this config - // (plz, lets refactor it) - pub fn with_optional_base(mut self, f: F, val: Option) -> Self - where - F: Fn(BaseClientConfig, T) -> BaseClientConfig, - { - self.base = self.base.with_optional(f, val); - self - } - - #[allow(unused)] - pub fn with_optional_base_env(mut self, f: F, val: Option, env_var: &str) -> Self - where - F: Fn(BaseClientConfig, T) -> BaseClientConfig, - T: FromStr, - ::Err: std::fmt::Debug, - { - self.base = self.base.with_optional_env(f, val, env_var); - self - } - - pub fn with_optional_base_custom_env( - mut self, - f: F, - val: Option, - env_var: &str, - parser: G, - ) -> Self - where - F: Fn(BaseClientConfig, T) -> BaseClientConfig, - G: Fn(&str) -> T, - { - self.base = self.base.with_optional_custom_env(f, val, env_var, parser); - self - } -} - -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -#[serde(default, deny_unknown_fields)] -pub struct Authenticator { - /// Socket address this node will use for binding its wireguard interface. - /// default: `0.0.0.0:51822` - pub bind_address: SocketAddr, - - /// Private IP address of the wireguard gateway. - /// default: `10.1.0.1` - pub private_ipv4: Ipv4Addr, - - /// Private IP address of the wireguard gateway. - /// default: `fc01::1` - pub private_ipv6: Ipv6Addr, - - /// Port announced to external clients wishing to connect to the wireguard interface. - /// Useful in the instances where the node is behind a proxy. - pub announced_port: u16, - - /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. - /// The maximum value for IPv4 is 32 - pub private_network_prefix_v4: u8, - - /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. - /// The maximum value for IPv6 is 128 - pub private_network_prefix_v6: u8, -} - -impl Default for Authenticator { - fn default() -> Self { - Self { - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_PORT), - private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, - private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, - announced_port: WG_PORT, - private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, - private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, - } - } -} - -impl From for nym_wireguard_types::Config { - fn from(value: Authenticator) -> Self { - nym_wireguard_types::Config { - bind_address: value.bind_address, - private_ipv4: value.private_ipv4, - private_ipv6: value.private_ipv6, - announced_port: value.announced_port, - private_network_prefix_v4: value.private_network_prefix_v4, - private_network_prefix_v6: value.private_network_prefix_v6, - } - } -} diff --git a/service-providers/authenticator/src/config/template.rs b/service-providers/authenticator/src/config/template.rs deleted file mode 100644 index d4b7eb7aa6..0000000000 --- a/service-providers/authenticator/src/config/template.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub(crate) const CONFIG_TEMPLATE: &str = - // While using normal toml marshalling would have been way simpler with less overhead, - // I think it's useful to have comments attached to the saved config file to explain behaviour of - // particular fields. - // Note: any changes to the template must be reflected in the appropriate structs. - r#" -# This is a TOML config file. -# For more information, see https://github.com/toml-lang/toml - -##### main base client config options ##### - -[client] -# Version of the client for which this configuration was created. -version = '{{ client.version }}' - -# Human readable ID of this particular client. -id = '{{ client.id }}' - -# Indicates whether this client is running in a disabled credentials mode, thus attempting -# to claim bandwidth without presenting bandwidth credentials. -disabled_credentials_mode = {{ client.disabled_credentials_mode }} - -# Addresses to nyxd validators via which the client can communicate with the chain. -nyxd_urls = [ - {{#each client.nyxd_urls }} - '{{this}}', - {{/each}} -] - -# Addresses to APIs running on validator from which the client gets the view of the network. -nym_api_urls = [ - {{#each client.nym_api_urls }} - '{{this}}', - {{/each}} -] - -[storage_paths] - -# Path to file containing private identity key. -keys.private_identity_key_file = '{{ storage_paths.keys.private_identity_key_file }}' - -# Path to file containing public identity key. -keys.public_identity_key_file = '{{ storage_paths.keys.public_identity_key_file }}' - -# Path to file containing private encryption key. -keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key_file }}' - -# Path to file containing public encryption key. -keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}' - -# Path to file containing key used for encrypting and decrypting the content of an -# acknowledgement so that nobody besides the client knows which packet it refers to. -keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' - -# Path to the database containing bandwidth credentials -credentials_database = '{{ storage_paths.credentials_database }}' - -# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. -reply_surb_database = '{{ storage_paths.reply_surb_database }}' - -# Path to the file containing information about gateways used by this client, -# i.e. details such as their public keys, owner addresses or the network information. -gateway_registrations = '{{ storage_paths.gateway_registrations }}' - -# Location of the file containing our allow.list -allowed_list_location = '{{ storage_paths.allowed_list_location }}' - -# Location of the file containing our unknown.list -unknown_list_location = '{{ storage_paths.unknown_list_location }}' - - -##### logging configuration options ##### - -[logging] - -# TODO - - -##### debug configuration options ##### -# The following options should not be modified unless you know EXACTLY what you are doing -# as if set incorrectly, they may impact your anonymity. - -[debug] - -[debug.traffic] -average_packet_delay = '{{ debug.traffic.average_packet_delay }}' -message_sending_average_delay = '{{ debug.traffic.message_sending_average_delay }}' - -[debug.acknowledgements] -average_ack_delay = '{{ debug.acknowledgements.average_ack_delay }}' - -[debug.cover_traffic] -loop_cover_traffic_average_delay = '{{ debug.cover_traffic.loop_cover_traffic_average_delay }}' - -"#; diff --git a/service-providers/authenticator/src/lib.rs b/service-providers/authenticator/src/lib.rs deleted file mode 100644 index ec7ca579f9..0000000000 --- a/service-providers/authenticator/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub use authenticator::{Authenticator, OnStartData}; -pub use config::Config; - -pub mod authenticator; -pub mod config; -pub mod error; -pub mod mixnet_client; -pub mod mixnet_listener; -mod peer_manager; diff --git a/service-providers/authenticator/src/main.rs b/service-providers/authenticator/src/main.rs deleted file mode 100644 index 37e46f9047..0000000000 --- a/service-providers/authenticator/src/main.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -mod cli; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - use clap::Parser; - - let args = cli::Cli::parse(); - nym_bin_common::logging::setup_logging(); - nym_network_defaults::setup_env(args.config_env_file.as_ref()); - - if !args.no_banner { - nym_bin_common::logging::maybe_print_banner(clap::crate_name!(), clap::crate_version!()); - } - - cli::execute(args).await?; - Ok(()) -} diff --git a/service-providers/authenticator/src/peer_manager.rs b/service-providers/authenticator/src/peer_manager.rs deleted file mode 100644 index 00f74deb67..0000000000 --- a/service-providers/authenticator/src/peer_manager.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::*; -use defguard_wireguard_rs::{host::Peer, key::Key}; -use futures::channel::oneshot; -use nym_authenticator_requests::{ - latest::registration::{GatewayClient, RemainingBandwidthData}, - traits::QueryBandwidthMessage, -}; -use nym_wireguard::{ - peer_controller::{ - AddPeerControlResponse, PeerControlRequest, QueryBandwidthControlResponse, - QueryPeerControlResponse, RemovePeerControlResponse, - }, - WireguardGatewayData, -}; -use nym_wireguard_types::PeerPublicKey; - -pub struct PeerManager { - pub(crate) wireguard_gateway_data: WireguardGatewayData, -} - -impl PeerManager { - pub fn new(wireguard_gateway_data: WireguardGatewayData) -> Self { - PeerManager { - wireguard_gateway_data, - } - } - pub async fn add_peer(&mut self, peer: Peer, client_id: Option) -> Result<()> { - let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::AddPeer { - peer, - client_id, - response_tx, - }; - self.wireguard_gateway_data - .peer_tx() - .send(msg) - .await - .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - - let AddPeerControlResponse { success } = response_rx.await.map_err(|_| { - AuthenticatorError::InternalError("no response for add peer".to_string()) - })?; - if !success { - return Err(AuthenticatorError::InternalError( - "adding peer could not be performed".to_string(), - )); - } - Ok(()) - } - - pub async fn _remove_peer(&mut self, client: &GatewayClient) -> Result<()> { - let key = Key::new(client.pub_key().to_bytes()); - let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::RemovePeer { key, response_tx }; - self.wireguard_gateway_data - .peer_tx() - .send(msg) - .await - .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - - let RemovePeerControlResponse { success } = response_rx.await.map_err(|_| { - AuthenticatorError::InternalError("no response for add peer".to_string()) - })?; - if !success { - return Err(AuthenticatorError::InternalError( - "removing peer could not be performed".to_string(), - )); - } - Ok(()) - } - - pub async fn query_peer(&mut self, public_key: PeerPublicKey) -> Result> { - let key = Key::new(public_key.to_bytes()); - let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::QueryPeer { key, response_tx }; - self.wireguard_gateway_data - .peer_tx() - .send(msg) - .await - .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - - let QueryPeerControlResponse { success, peer } = response_rx.await.map_err(|_| { - AuthenticatorError::InternalError("no response for query peer".to_string()) - })?; - if !success { - return Err(AuthenticatorError::InternalError( - "querying peer could not be performed".to_string(), - )); - } - Ok(peer) - } - - pub async fn query_bandwidth( - &mut self, - msg: Box, - ) -> Result> { - let key = Key::new(msg.pub_key().to_bytes()); - let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::QueryBandwidth { key, response_tx }; - self.wireguard_gateway_data - .peer_tx() - .send(msg) - .await - .map_err(|_| AuthenticatorError::PeerInteractionStopped)?; - - let QueryBandwidthControlResponse { - success, - bandwidth_data, - } = response_rx - .await - .map_err(|_| AuthenticatorError::InternalError("no response for query".to_string()))?; - if !success { - return Err(AuthenticatorError::InternalError( - "querying bandwidth could not be performed".to_string(), - )); - } - Ok(bandwidth_data) - } -} diff --git a/service-providers/ip-packet-router/Cargo.toml b/service-providers/ip-packet-router/Cargo.toml index 14cedf7743..c69d1caa20 100644 --- a/service-providers/ip-packet-router/Cargo.toml +++ b/service-providers/ip-packet-router/Cargo.toml @@ -17,7 +17,7 @@ clap.workspace = true etherparse = { workspace = true } futures = { workspace = true } log = { workspace = true } -nym-bin-common = { path = "../../common/bin-common", features = ["clap"] } +nym-bin-common = { path = "../../common/bin-common", features = ["clap", "basic_tracing"] } nym-client-core = { path = "../../common/client-core" } nym-config = { path = "../../common/config" } nym-crypto = { path = "../../common/crypto" } diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index 11c2a0e049..d322984773 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -4,7 +4,7 @@ use log::error; use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; use nym_client_core::cli_helpers::CliClient; -use nym_ip_packet_router::config::helpers::try_upgrade_config; +use nym_ip_packet_router::config::helpers::{try_upgrade_config, try_upgrade_config_by_id}; use nym_ip_packet_router::config::{BaseClientConfig, Config}; use nym_ip_packet_router::error::IpPacketRouterError; use std::sync::OnceLock; @@ -151,7 +151,7 @@ async fn try_load_current_config(id: &str) -> Result cfg, diff --git a/service-providers/ip-packet-router/src/cli/run.rs b/service-providers/ip-packet-router/src/cli/run.rs index 3e4010cb32..bf71e79dea 100644 --- a/service-providers/ip-packet-router/src/cli/run.rs +++ b/service-providers/ip-packet-router/src/cli/run.rs @@ -24,7 +24,7 @@ impl From for OverrideConfig { pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - log::debug!("Using config: {:#?}", config); + log::debug!("Using config: {config:#?}"); log::info!("Starting ip packet router service provider"); let mut server = nym_ip_packet_router::IpPacketRouter::new(config); diff --git a/service-providers/ip-packet-router/src/clients/connected_client_handler.rs b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs index 8e9c40ec40..551518a062 100644 --- a/service-providers/ip-packet-router/src/clients/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs @@ -69,8 +69,8 @@ impl ConnectedClientHandler { oneshot::Sender<()>, tokio::task::JoinHandle<()>, ) { - log::debug!("Starting connected client handler for: {}", client_id); - log::debug!("client version: {:?}", client_version); + log::debug!("Starting connected client handler for: {client_id}"); + log::debug!("client version: {client_version:?}"); let (close_tx, close_rx) = oneshot::channel(); let (forward_from_tun_tx, forward_from_tun_rx) = mpsc::unbounded_channel(); @@ -133,12 +133,16 @@ impl ConnectedClientHandler { let input_packet = self .input_message_creator .to_input_message(&bundled_packets) - .map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet { source })?; + .map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet { + source: Box::new(source), + })?; self.mixnet_client_sender .send(input_packet) .await - .map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet { source }) + .map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet { + source: Box::new(source), + }) } async fn run(mut self) -> Result<()> { diff --git a/service-providers/ip-packet-router/src/config/helpers.rs b/service-providers/ip-packet-router/src/config/helpers.rs index 6dd7209b87..5ca4a04f03 100644 --- a/service-providers/ip-packet-router/src/config/helpers.rs +++ b/service-providers/ip-packet-router/src/config/helpers.rs @@ -3,6 +3,7 @@ use crate::config::default_config_filepath; use crate::config::old_config_v1::ConfigV1; +use crate::config::old_config_v2::ConfigV2; use crate::error::IpPacketRouterError; use log::{info, trace}; use nym_client_core::cli_helpers::CliClientConfig; @@ -22,22 +23,45 @@ async fn try_upgrade_v1_config>( info!("It is going to get updated to the current specification."); let old_paths = old_config.storage_paths.clone(); - let updated = old_config.try_upgrade()?; + let updated_step1: ConfigV2 = old_config.try_into()?; v1_1_33::migrate_gateway_details( &old_paths.common_paths, - &updated.storage_paths.common_paths, + &updated_step1.storage_paths.common_paths, None, ) .await?; + let updated = updated_step1.try_upgrade()?; + + updated.save_to(config_path)?; + Ok(true) +} + +async fn try_upgrade_v2_config>( + config_path: P, +) -> Result { + // explicitly load it as v2 (which is incompatible with the current one) + let Ok(old_config) = ConfigV2::read_from_toml_file(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using v2 config template."); + info!("It is going to get updated to the current specification."); + + let updated = old_config.try_upgrade()?; + updated.save_to(config_path)?; Ok(true) } pub async fn try_upgrade_config>(config_path: P) -> Result<(), IpPacketRouterError> { trace!("Attempting to upgrade config"); - if try_upgrade_v1_config(config_path).await? { + if try_upgrade_v1_config(config_path.as_ref()).await? { + return Ok(()); + } + if try_upgrade_v2_config(config_path).await? { return Ok(()); } diff --git a/service-providers/ip-packet-router/src/config/mod.rs b/service-providers/ip-packet-router/src/config/mod.rs index 1f7aad3cc2..a3968200f3 100644 --- a/service-providers/ip-packet-router/src/config/mod.rs +++ b/service-providers/ip-packet-router/src/config/mod.rs @@ -22,6 +22,7 @@ use self::template::CONFIG_TEMPLATE; pub mod helpers; pub mod old_config_v1; +pub mod old_config_v2; mod persistence; mod template; diff --git a/service-providers/ip-packet-router/src/config/old_config_v1.rs b/service-providers/ip-packet-router/src/config/old_config_v1.rs index b7cddacf00..f6f2863085 100644 --- a/service-providers/ip-packet-router/src/config/old_config_v1.rs +++ b/service-providers/ip-packet-router/src/config/old_config_v1.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::persistence::IpPacketRouterPaths; -use crate::config::Config; use crate::config::{default_config_filepath, IpPacketRouter}; use crate::error::IpPacketRouterError; use nym_bin_common::logging::LoggingSettings; @@ -16,6 +15,8 @@ use std::io; use std::path::{Path, PathBuf}; use url::Url; +use super::old_config_v2::ConfigV2; + #[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)] pub struct IpPacketRouterPathsV1 { #[serde(flatten)] @@ -39,6 +40,22 @@ pub struct ConfigV1 { pub logging: LoggingSettings, } +impl TryFrom for ConfigV2 { + type Error = IpPacketRouterError; + + fn try_from(value: ConfigV1) -> Result { + Ok(ConfigV2 { + base: value.base.into(), + ip_packet_router: value.ip_packet_router.into(), + storage_paths: IpPacketRouterPaths { + common_paths: value.storage_paths.common_paths.upgrade_default()?, + ip_packet_router_description: value.storage_paths.ip_packet_router_description, + }, + logging: value.logging, + }) + } +} + impl ConfigV1 { pub fn read_from_toml_file>(path: P) -> io::Result { read_config_from_toml_file(path) @@ -47,18 +64,6 @@ impl ConfigV1 { pub fn read_from_default_path>(id: P) -> io::Result { Self::read_from_toml_file(default_config_filepath(id)) } - - pub fn try_upgrade(self) -> Result { - Ok(Config { - base: self.base.into(), - ip_packet_router: self.ip_packet_router.into(), - storage_paths: IpPacketRouterPaths { - common_paths: self.storage_paths.common_paths.upgrade_default()?, - ip_packet_router_description: self.storage_paths.ip_packet_router_description, - }, - logging: self.logging, - }) - } } #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] diff --git a/service-providers/ip-packet-router/src/config/old_config_v2.rs b/service-providers/ip-packet-router/src/config/old_config_v2.rs new file mode 100644 index 0000000000..9a1f91f11f --- /dev/null +++ b/service-providers/ip-packet-router/src/config/old_config_v2.rs @@ -0,0 +1,42 @@ +use std::{io, path::Path}; + +use crate::{config::Config, error::IpPacketRouterError}; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::old_config_v1_1_54::ConfigV1_1_54 as BaseConfigV1_1_54; +use nym_config::read_config_from_toml_file; +use serde::{Deserialize, Serialize}; + +use super::{default_config_filepath, IpPacketRouter, IpPacketRouterPaths}; + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV2 { + #[serde(flatten)] + pub base: BaseConfigV1_1_54, + + #[serde(default)] + pub ip_packet_router: IpPacketRouter, + + pub storage_paths: IpPacketRouterPaths, + + pub logging: LoggingSettings, +} + +impl ConfigV2 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + base: self.base.into(), + ip_packet_router: self.ip_packet_router, + storage_paths: self.storage_paths, + logging: self.logging, + }) + } +} diff --git a/service-providers/ip-packet-router/src/config/template.rs b/service-providers/ip-packet-router/src/config/template.rs index 5338b4b497..b40456dbe0 100644 --- a/service-providers/ip-packet-router/src/config/template.rs +++ b/service-providers/ip-packet-router/src/config/template.rs @@ -88,10 +88,6 @@ ip_packet_router_description = '{{ storage_paths.ip_packet_router_description }} [debug] -[debug.traffic] -average_packet_delay = '{{ debug.traffic.average_packet_delay }}' -message_sending_average_delay = '{{ debug.traffic.message_sending_average_delay }}' - [debug.acknowledgements] average_ack_delay = '{{ debug.acknowledgements.average_ack_delay }}' diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 17d3ff1e0c..05eae38cef 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -29,10 +29,10 @@ pub enum IpPacketRouterError { ConfigValidationFailure, #[error("failed to setup mixnet client: {source}")] - FailedToSetupMixnetClient { source: nym_sdk::Error }, + FailedToSetupMixnetClient { source: Box }, #[error("failed to connect to mixnet: {source}")] - FailedToConnectToMixnet { source: nym_sdk::Error }, + FailedToConnectToMixnet { source: Box }, #[error("the entity wrapping the ip packet router has disconnected")] DisconnectedParent, @@ -59,7 +59,7 @@ pub enum IpPacketRouterError { FailedToWritePacketToTun, #[error("failed to send packet to mixnet: {source}")] - FailedToSendPacketToMixnet { source: nym_sdk::Error }, + FailedToSendPacketToMixnet { source: Box }, #[error("failed to encode mixnet message: {source}")] FailedToEncodeMixnetMessage { diff --git a/service-providers/ip-packet-router/src/main.rs b/service-providers/ip-packet-router/src/main.rs index 17e5040ab3..f1987ed6ab 100644 --- a/service-providers/ip-packet-router/src/main.rs +++ b/service-providers/ip-packet-router/src/main.rs @@ -10,7 +10,7 @@ async fn main() -> anyhow::Result<()> { use clap::Parser; let args = cli::Cli::parse(); - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); nym_network_defaults::setup_env(args.config_env_file.as_ref()); if !args.no_banner { diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs index 99b87075c8..1d0b5a3413 100644 --- a/service-providers/ip-packet-router/src/messages/response/v8.rs +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -30,8 +30,7 @@ impl TryFrom for IpPacketResponseV8 { match response.response { Response::StaticConnect { .. } => { return Err(IpPacketRouterError::UnsupportedResponse(format!( - "Static connect response is not supported in version {}", - version + "Static connect response is not supported in version {version}" ))) } Response::DynamicConnect { request_id, reply } => IpPacketResponseDataV8::Control( diff --git a/service-providers/ip-packet-router/src/mixnet_client.rs b/service-providers/ip-packet-router/src/mixnet_client.rs index 43f5f00038..18bed08c24 100644 --- a/service-providers/ip-packet-router/src/mixnet_client.rs +++ b/service-providers/ip-packet-router/src/mixnet_client.rs @@ -27,7 +27,9 @@ pub(crate) async fn create_mixnet_client( let mut client_builder = nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths) .await - .map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient { source: err })? + .map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient { + source: Box::new(err), + })? .network_details(NymNetworkDetails::new_from_env()) .debug_config(debug_config) .custom_shutdown(shutdown) @@ -43,12 +45,16 @@ pub(crate) async fn create_mixnet_client( client_builder = client_builder.custom_topology_provider(topology_provider); } - let mixnet_client = client_builder - .build() - .map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient { source: err })?; + let mixnet_client = + client_builder + .build() + .map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient { + source: Box::new(err), + })?; - mixnet_client - .connect_to_mixnet() - .await - .map_err(|err| IpPacketRouterError::FailedToConnectToMixnet { source: err }) + mixnet_client.connect_to_mixnet().await.map_err(|err| { + IpPacketRouterError::FailedToConnectToMixnet { + source: Box::new(err), + } + }) } diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index fa8cf81a38..fe622f8aa0 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -310,7 +310,7 @@ impl MixnetListener { // Check if the client is connected if !self.connected_clients.is_client_connected(&client_id) { - log::info!("Client {} is not connected, cannot disconnect", client_id); + log::info!("Client {client_id} is not connected, cannot disconnect"); return Ok(Some(VersionedResponse { version, reply_to: client_id, @@ -322,7 +322,7 @@ impl MixnetListener { } // Disconnect the client - log::info!("Disconnecting client {}", client_id); + log::info!("Disconnecting client {client_id}"); self.connected_clients.disconnect_client(&client_id); Ok(Some(VersionedResponse { @@ -435,10 +435,11 @@ impl MixnetListener { let input_message = crate::util::create_message::create_input_message(&send_to, response_bytes); - self.mixnet_client - .send(input_message) - .await - .map_err(|err| IpPacketRouterError::FailedToSendPacketToMixnet { source: err }) + self.mixnet_client.send(input_message).await.map_err(|err| { + IpPacketRouterError::FailedToSendPacketToMixnet { + source: Box::new(err), + } + }) } // A single incoming request can trigger multiple responses, such as when data requests contain diff --git a/service-providers/ip-packet-router/src/non_linux_dummy.rs b/service-providers/ip-packet-router/src/non_linux_dummy.rs index abac0ad1d3..9197ae4cd1 100644 --- a/service-providers/ip-packet-router/src/non_linux_dummy.rs +++ b/service-providers/ip-packet-router/src/non_linux_dummy.rs @@ -6,6 +6,7 @@ use std::pin::Pin; use std::task::{Context, Poll}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +#[allow(dead_code)] pub(crate) struct DummyDevice; impl AsyncRead for DummyDevice { diff --git a/service-providers/ip-packet-router/src/util/parse_ip.rs b/service-providers/ip-packet-router/src/util/parse_ip.rs index 6b99a2ba89..0b9972b3f2 100644 --- a/service-providers/ip-packet-router/src/util/parse_ip.rs +++ b/service-providers/ip-packet-router/src/util/parse_ip.rs @@ -9,7 +9,7 @@ pub(crate) struct ParsedPacket<'a> { pub(crate) dst: Option, } -pub(crate) fn parse_packet(packet: &[u8]) -> Result { +pub(crate) fn parse_packet(packet: &[u8]) -> Result, IpPacketRouterError> { let headers = etherparse::SlicedPacket::from_ip(packet).map_err(|err| { log::warn!("Unable to parse incoming data as IP packet: {err}"); IpPacketRouterError::PacketParseFailed { source: err } diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index aece5f7091..ad7436e5de 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.54" +version = "1.1.62" authors.workspace = true edition.workspace = true rust-version = "1.70" @@ -26,7 +26,6 @@ futures = { workspace = true } humantime-serde = { workspace = true } ipnetwork = { workspace = true } log = { workspace = true } -pretty_env_logger = { workspace = true } publicsuffix = { workspace = true } rand = { workspace = true } regex = { workspace = true } @@ -44,7 +43,7 @@ zeroize = { workspace = true } # internal nym-async-file-watcher = { path = "../../common/async-file-watcher" } -nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "clap"] } +nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "clap", "basic_tracing"] } nym-client-core = { path = "../../common/client-core", features = ["cli", "fs-gateways-storage", "fs-surb-storage"] } nym-client-websocket-requests = { path = "../../clients/native/websocket-requests" } nym-config = { path = "../../common/config" } diff --git a/service-providers/network-requester/examples/query.rs b/service-providers/network-requester/examples/query.rs index 87f410ee75..a11eeff143 100644 --- a/service-providers/network-requester/examples/query.rs +++ b/service-providers/network-requester/examples/query.rs @@ -13,7 +13,7 @@ fn parse_response(received: Vec) -> Socks5Response { assert_eq!(received.len(), 1); let response: Response = Response::try_from_bytes(&received[0].message).unwrap(); match response.content { - ResponseContent::Control(control) => panic!("unexpected control response: {:?}", control), + ResponseContent::Control(control) => panic!("unexpected control response: {control:?}"), ResponseContent::ProviderData(data) => data, } } diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 6a061dd711..09938ac46a 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -47,7 +47,7 @@ impl From for OverrideConfig { pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - log::debug!("Using config: {:#?}", config); + log::debug!("Using config: {config:#?}"); if config.network_requester.open_proxy { println!( diff --git a/service-providers/network-requester/src/config/helpers.rs b/service-providers/network-requester/src/config/helpers.rs index 2b29776dea..3655b9ea76 100644 --- a/service-providers/network-requester/src/config/helpers.rs +++ b/service-providers/network-requester/src/config/helpers.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::default_config_filepath; -use crate::config::old::v5::ConfigV5; +use crate::config::old::v6::ConfigV6; use crate::config::old_config_v1_1_13::OldConfigV1; use crate::config::old_config_v1_1_20::ConfigV2; use crate::config::old_config_v1_1_20_2::ConfigV3; use crate::config::old_config_v1_1_33::ConfigV4; +use crate::config::old_config_v1_1_54::ConfigV5; use crate::config::Config; use crate::error::NetworkRequesterError; use log::{info, trace}; @@ -42,7 +43,8 @@ async fn try_upgrade_v1_config>( ) .await?; - let updated: Config = updated_step4.into(); + let updated_step5: ConfigV6 = updated_step4.into(); + let updated: Config = updated_step5.into(); updated.save_to(config_path)?; Ok(true) @@ -76,7 +78,8 @@ async fn try_upgrade_v2_config>( ) .await?; - let updated: Config = updated_step3.into(); + let updated_step4: ConfigV6 = updated_step3.into(); + let updated: Config = updated_step4.into(); updated.save_to(config_path)?; Ok(true) @@ -107,7 +110,8 @@ async fn try_upgrade_v3_config>( ) .await?; - let updated: Config = updated_step2.into(); + let updated_step3: ConfigV6 = updated_step2.into(); + let updated: Config = updated_step3.into(); updated.save_to(config_path)?; Ok(true) @@ -137,7 +141,8 @@ async fn try_upgrade_v4_config>( ) .await?; - let updated: Config = updated_step1.into(); + let updated_step2: ConfigV6 = updated_step1.into(); + let updated: Config = updated_step2.into(); updated.save_to(config_path)?; Ok(true) @@ -155,6 +160,25 @@ async fn try_upgrade_v5_config>( info!("It seems the client is using <= v5 config template."); info!("It is going to get updated to the current specification."); + let updated_step1: ConfigV6 = old_config.into(); + let updated: Config = updated_step1.into(); + updated.save_to(config_path)?; + + Ok(true) +} + +async fn try_upgrade_v6_config>( + config_path: P, +) -> Result { + // explicitly load it as v6 (which is incompatible with the current one) + let Ok(old_config) = ConfigV6::read_from_toml_file(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v6 config template."); + info!("It is going to get updated to the current specification."); + let updated: Config = old_config.into(); updated.save_to(config_path)?; @@ -177,7 +201,10 @@ pub async fn try_upgrade_config>( if try_upgrade_v4_config(config_path.as_ref()).await? { return Ok(()); } - if try_upgrade_v5_config(config_path).await? { + if try_upgrade_v5_config(config_path.as_ref()).await? { + return Ok(()); + } + if try_upgrade_v6_config(config_path).await? { return Ok(()); } diff --git a/service-providers/network-requester/src/config/mod.rs b/service-providers/network-requester/src/config/mod.rs index 4fdb5170a2..6284186c95 100644 --- a/service-providers/network-requester/src/config/mod.rs +++ b/service-providers/network-requester/src/config/mod.rs @@ -32,6 +32,7 @@ pub use old::v1 as old_config_v1_1_13; pub use old::v2 as old_config_v1_1_20; pub use old::v3 as old_config_v1_1_20_2; pub use old::v4 as old_config_v1_1_33; +pub use old::v5 as old_config_v1_1_54; const DEFAULT_NETWORK_REQUESTERS_DIR: &str = "network-requester"; diff --git a/service-providers/network-requester/src/config/old/mod.rs b/service-providers/network-requester/src/config/old/mod.rs index 3a529cd6c0..4eae34bac2 100644 --- a/service-providers/network-requester/src/config/old/mod.rs +++ b/service-providers/network-requester/src/config/old/mod.rs @@ -6,3 +6,4 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; diff --git a/service-providers/network-requester/src/config/old/v5.rs b/service-providers/network-requester/src/config/old/v5.rs index d02a540548..d8b4c6d99d 100644 --- a/service-providers/network-requester/src/config/old/v5.rs +++ b/service-providers/network-requester/src/config/old/v5.rs @@ -3,10 +3,9 @@ use crate::config::persistence::old::v3::NetworkRequesterPathsV3; use crate::config::persistence::NetworkRequesterPaths; -use crate::config::Config; use crate::config::{default_config_filepath, Debug, NetworkRequester}; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::config::Config as BaseClientConfig; +use nym_client_core::config::old_config_v1_1_54::ConfigV1_1_54 as BaseConfigV1_1_54; use nym_config::read_config_from_toml_file; use nym_config::serde_helpers::de_maybe_stringified; use nym_network_defaults::mainnet; @@ -16,6 +15,8 @@ use std::path::Path; use std::time::Duration; use url::Url; +use super::v6::ConfigV6; + pub const DEFAULT_STANDARD_LIST_UPDATE_INTERVAL: Duration = Duration::from_secs(30 * 60); #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] @@ -27,7 +28,7 @@ pub struct ConfigV5 { // and then just make type alias for the current one, i.e. `type Config = ConfigV2`. // then in 'old' configs we could simply use the underlying type as opposed to the alias for easier upgrades. #[serde(flatten)] - pub base: BaseClientConfig, + pub base: BaseConfigV1_1_54, #[serde(default)] pub network_requester: NetworkRequesterV5, @@ -51,10 +52,10 @@ impl ConfigV5 { } } -impl From for Config { +impl From for ConfigV6 { fn from(value: ConfigV5) -> Self { - Config { - base: value.base, + ConfigV6 { + base: value.base.into(), network_requester: value.network_requester.into(), storage_paths: NetworkRequesterPaths { common_paths: value.storage_paths.common_paths, diff --git a/service-providers/network-requester/src/config/old/v6.rs b/service-providers/network-requester/src/config/old/v6.rs new file mode 100644 index 0000000000..f50e4ddb77 --- /dev/null +++ b/service-providers/network-requester/src/config/old/v6.rs @@ -0,0 +1,50 @@ +use std::{io, path::Path}; + +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::Config as BaseClientConfig; +use nym_config::read_config_from_toml_file; +use serde::{Deserialize, Serialize}; + +use crate::config::{ + default_config_filepath, Config, Debug, NetworkRequester, NetworkRequesterPaths, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV6 { + #[serde(flatten)] + pub base: BaseClientConfig, + + #[serde(default)] + pub network_requester: NetworkRequester, + + pub storage_paths: NetworkRequesterPaths, + + #[serde(default)] + pub network_requester_debug: Debug, + + pub logging: LoggingSettings, +} + +impl ConfigV6 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + #[allow(dead_code)] + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } +} + +impl From for Config { + fn from(value: ConfigV6) -> Self { + Config { + base: value.base, + network_requester: value.network_requester, + storage_paths: value.storage_paths, + network_requester_debug: value.network_requester_debug, + logging: value.logging, + } + } +} diff --git a/service-providers/network-requester/src/config/template.rs b/service-providers/network-requester/src/config/template.rs index 053dca47d4..ac1f52f37b 100644 --- a/service-providers/network-requester/src/config/template.rs +++ b/service-providers/network-requester/src/config/template.rs @@ -101,10 +101,6 @@ upstream_exit_policy_url = '{{ network_requester.upstream_exit_policy_url }}' [debug] -[debug.traffic] -average_packet_delay = '{{ debug.traffic.average_packet_delay }}' -message_sending_average_delay = '{{ debug.traffic.message_sending_average_delay }}' - [debug.acknowledgements] average_ack_delay = '{{ debug.acknowledgements.average_ack_delay }}' diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 6ba73731c4..513e187d64 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -576,7 +576,9 @@ async fn create_mixnet_client( let mut client_builder = nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths) .await - .map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })? + .map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { + source: Box::new(err), + })? .network_details(NymNetworkDetails::new_from_env()) .debug_config(debug_config) .custom_shutdown(shutdown) @@ -591,12 +593,16 @@ async fn create_mixnet_client( client_builder = client_builder.custom_topology_provider(topology_provider); } - let mixnet_client = client_builder - .build() - .map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })?; + let mixnet_client = + client_builder + .build() + .map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { + source: Box::new(err), + })?; - mixnet_client - .connect_to_mixnet() - .await - .map_err(|err| NetworkRequesterError::FailedToConnectToMixnet { source: err }) + mixnet_client.connect_to_mixnet().await.map_err(|err| { + NetworkRequesterError::FailedToConnectToMixnet { + source: Box::new(err), + } + }) } diff --git a/service-providers/network-requester/src/error.rs b/service-providers/network-requester/src/error.rs index 8b15743f2b..e4870d64c8 100644 --- a/service-providers/network-requester/src/error.rs +++ b/service-providers/network-requester/src/error.rs @@ -30,10 +30,10 @@ pub enum NetworkRequesterError { ConfigValidationFailure, #[error("failed to setup mixnet client: {source}")] - FailedToSetupMixnetClient { source: nym_sdk::Error }, + FailedToSetupMixnetClient { source: Box }, #[error("failed to connect to mixnet: {source}")] - FailedToConnectToMixnet { source: nym_sdk::Error }, + FailedToConnectToMixnet { source: Box }, #[error("the entity wrapping the network requester has disconnected")] DisconnectedParent, diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index d43cf2a180..686019b575 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use clap::{crate_name, crate_version, Parser}; -use nym_bin_common::logging::{maybe_print_banner, setup_logging}; +use nym_bin_common::logging::{maybe_print_banner, setup_tracing_logger}; use nym_network_defaults::setup_env; mod cli; @@ -21,7 +21,7 @@ async fn main() -> anyhow::Result<()> { if !args.no_banner { maybe_print_banner(crate_name!(), crate_version!()); } - setup_logging(); + setup_tracing_logger(); cli::execute(args).await?; diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000000..8ebb62df5f --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,18 @@ +sonar.projectKey=nymtech_nym +sonar.organization=nymtech + + +# This is the name and version displayed in the SonarCloud UI. +#sonar.projectName=nym +#sonar.projectVersion=1.0 + + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +#sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 + +sonar.c.file.suffixes=- +sonar.cpp.file.suffixes=- +sonar.objc.file.suffixes=- diff --git a/sqlx-pool-guard/Cargo.toml b/sqlx-pool-guard/Cargo.toml new file mode 100644 index 0000000000..cef202e4df --- /dev/null +++ b/sqlx-pool-guard/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "sqlx-pool-guard" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[lints] +workspace = true + +[dependencies] +tracing.workspace = true + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +workspace = true +features = ["runtime-tokio-rustls", "sqlite"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +workspace = true +features = ["rt-multi-thread", "macros", "time", "fs"] + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +proc_pidinfo.workspace = true + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = [ + "Win32", + "Win32_System", + "Win32_System_Memory", + "Win32_System_Threading", + "Win32_Storage_FileSystem", + "Wdk_System_SystemInformation", +] } + +[dev-dependencies] +tempfile.workspace = true +tracing-subscriber.workspace = true diff --git a/sqlx-pool-guard/src/apple.rs b/sqlx-pool-guard/src/apple.rs new file mode 100644 index 0000000000..a67ed1a447 --- /dev/null +++ b/sqlx-pool-guard/src/apple.rs @@ -0,0 +1,43 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{io, path::Path}; + +use proc_pidinfo::{ + ProcFDInfo, ProcFDType, VnodeFdInfoWithPath, proc_pidfdinfo_self, proc_pidinfo_list_self, +}; + +/// Check if there are no open file descriptors for the given files. +/// +/// Uses `proc_pidinfo` (`sys/proc_info.h`) +/// See: http://blog.palominolabs.com/2012/06/19/getting-the-files-being-used-by-a-process-on-mac-os-x/ +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { + let fd_list = proc_pidinfo_list_self::()?; + + for fd in fd_list + .iter() + .filter(|s| s.fd_type() == Ok(ProcFDType::VNODE)) + { + let Some(vnode) = proc_pidfdinfo_self::(fd.proc_fd) + .inspect_err(|e| { + tracing::warn!("proc_pidfdinfo_self::() failure: {e}"); + }) + .ok() + .flatten() + else { + continue; + }; + + if let Ok(true) = vnode + .path() + .map(|vnode_path| file_paths.contains(&vnode_path)) + .inspect_err(|e| { + tracing::warn!("vnode.path() failure: {e:?}"); + }) + { + return Ok(false); + } + } + + Ok(true) +} diff --git a/sqlx-pool-guard/src/lib.rs b/sqlx-pool-guard/src/lib.rs new file mode 100644 index 0000000000..3e16683de3 --- /dev/null +++ b/sqlx-pool-guard/src/lib.rs @@ -0,0 +1,180 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + io, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + time::Duration, +}; + +#[cfg(windows)] +#[path = "windows.rs"] +mod imp; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "apple.rs"] +mod imp; + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[path = "linux.rs"] +mod imp; + +/// Max number of retry attempts +const CHECK_FILES_CLOSED_MAX_ATTEMPTS: u8 = 20; + +/// Delay between file checks +const CHECK_FILES_CLOSED_RETRY_DELAY: Duration = Duration::from_millis(100); + +/// `sqlx::SqlitePool` wrapper providing a workaround for the [known bug](https://github.com/launchbadge/sqlx/issues/3217). +/// In principle after requesting to close the sqlite pool, the wrapper monitors open file descriptor and polls periodically until all database files are closed. +#[derive(Debug, Clone)] +pub struct SqlitePoolGuard { + /// Path to sqlite database file. + database_path: PathBuf, + + /// Inner connection pool. + connection_pool: sqlx::SqlitePool, +} + +impl SqlitePoolGuard { + /// Create new instance providing path to database and connection pool + pub fn new(connection_pool: sqlx::SqlitePool) -> Self { + let database_path = connection_pool + .connect_options() + .get_filename() + .to_path_buf(); + + Self { + database_path, + connection_pool, + } + } + + /// Returns database path + pub fn database_path(&self) -> &Path { + &self.database_path + } + + /// Close udnerlying sqlite pool and wait for files to be closed before returning. + pub async fn close(&self) { + // Avoid waiting for db files once the pool is marked closed to ensure that we don't wait on some other sqlite pool to close the database. + if !self.connection_pool.is_closed() { + tracing::info!("Closing sqlite pool: {}", self.database_path.display()); + self.close_pool_inner().await.ok(); + } + } + + async fn close_pool_inner(&self) -> std::io::Result<()> { + self.connection_pool.close().await; + + self.wait_for_db_files_close().await.inspect_err(|e| { + tracing::error!("Failed to wait for file to close: {e}"); + }) + } + + /// Returns all database files, including shm and wal files. + fn all_database_files(&self) -> Vec { + let mut database_files = vec![]; + let canonical_path = self + .database_path + .canonicalize() + .inspect_err(|e| { + tracing::error!( + "Failed to canonicalize path: {}. Cause: {e}", + self.database_path.display() + ); + }) + .unwrap_or(self.database_path.clone()); + + if let Some(ext) = canonical_path.extension() { + for added_ext in ["-shm", "-wal"] { + let mut new_ext = ext.to_owned(); + new_ext.push(added_ext); + database_files.push(canonical_path.with_extension(new_ext)); + } + } + database_files.push(canonical_path); + database_files + } + + /// Wait for database files to be closed before returning. + async fn wait_for_db_files_close(&self) -> std::io::Result<()> { + let database_files = self.all_database_files(); + let paths: Vec<&Path> = database_files.iter().map(PathBuf::as_path).collect(); + + for _ in 0..CHECK_FILES_CLOSED_MAX_ATTEMPTS { + match imp::check_files_closed(&paths) + .await + .inspect_err(|e| tracing::error!("imp::check_files_closed() failure: {e}")) + { + Ok(false) | Err(_) => tokio::time::sleep(CHECK_FILES_CLOSED_RETRY_DELAY).await, + Ok(true) => return Ok(()), + } + } + + Err(io::Error::new( + io::ErrorKind::TimedOut, + "timed out waiting for sqlite files to be closed", + )) + } +} + +impl Deref for SqlitePoolGuard { + type Target = sqlx::SqlitePool; + + fn deref(&self) -> &Self::Target { + &self.connection_pool + } +} + +impl DerefMut for SqlitePoolGuard { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.connection_pool + } +} +#[cfg(test)] +mod tests { + use sqlx::{ + ConnectOptions, Executor, + sqlite::{SqliteAutoVacuum, SqliteSynchronous}, + }; + + use super::*; + + #[tokio::test] + async fn test_wait_close() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + + let temp_dir = tempfile::tempdir().unwrap(); + let database_path = temp_dir.path().join("storage.db"); + + let opts = sqlx::sqlite::SqliteConnectOptions::new() + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .synchronous(SqliteSynchronous::Normal) + .auto_vacuum(SqliteAutoVacuum::Incremental) + .filename(database_path.clone()) + .create_if_missing(true) + .disable_statement_logging(); + let connection_pool = sqlx::SqlitePool::connect_with(opts).await.unwrap(); + + connection_pool + .execute("create table test (col int)") + .await + .unwrap(); + + let guard = SqlitePoolGuard::new(connection_pool); + assert!( + guard + .wait_for_db_files_close() + .await + .err() + .is_some_and(|e| e.kind() == io::ErrorKind::TimedOut) + ); + + assert!(guard.close_pool_inner().await.is_ok()); + tokio::fs::remove_file(database_path).await.unwrap(); + } +} diff --git a/sqlx-pool-guard/src/linux.rs b/sqlx-pool-guard/src/linux.rs new file mode 100644 index 0000000000..e0f26de9e5 --- /dev/null +++ b/sqlx-pool-guard/src/linux.rs @@ -0,0 +1,36 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{io, path::Path}; + +static PROC_SELF_FD_DIR: &str = "/proc/self/fd/"; + +/// Check if there are no open file descriptors for the given files. +/// +/// Linux, Android: uses `/proc/self/fd/` to list open file descriptors +/// See: https://stackoverflow.com/a/59797198/351305 +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { + let mut dir = tokio::fs::read_dir(PROC_SELF_FD_DIR).await?; + + while let Ok(Some(entry)) = dir.next_entry().await { + if entry + .file_type() + .await + .inspect_err(|e| tracing::warn!("entry.file_type() failure: {e}")) + .is_ok_and(|entry_type| entry_type.is_symlink()) + { + match tokio::fs::read_link(entry.path()).await { + Ok(resolved_path) => { + if file_paths.contains(&resolved_path.as_ref()) { + return Ok(false); + } + } + Err(e) => { + tracing::error!("Failed to read symlink: {e}"); + } + } + } + } + + Ok(true) +} diff --git a/sqlx-pool-guard/src/windows.rs b/sqlx-pool-guard/src/windows.rs new file mode 100644 index 0000000000..6d6d1c695f --- /dev/null +++ b/sqlx-pool-guard/src/windows.rs @@ -0,0 +1,201 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + ffi::{OsString, c_uchar, c_ulong, c_ushort, c_void}, + io, + os::windows::ffi::OsStringExt, + path::{Path, PathBuf}, +}; + +use windows::{ + Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS}, + Win32::{ + Foundation::{HANDLE, MAX_PATH, NTSTATUS, STATUS_INFO_LENGTH_MISMATCH}, + Storage::FileSystem::{ + FILE_NAME_NORMALIZED, FILE_TYPE_DISK, GetFileType, GetFinalPathNameByHandleW, + }, + System::{ + Memory::{ + GetProcessHeap, HEAP_FLAGS, HEAP_ZERO_MEMORY, HeapAlloc, HeapFree, HeapReAlloc, + }, + Threading::GetCurrentProcessId, + }, + }, +}; + +/// Private information class used to retrieve open file handles +const SYSTEM_HANDLE_INFORMATION_CLASS: SYSTEM_INFORMATION_CLASS = SYSTEM_INFORMATION_CLASS(0x10); + +/// Initial buffer size holding the handle info +/// The number is based on what I observe on a pretty standard Windows 11 +const SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE: usize = 2_500_000; + +/// Max retry attempts for querying system handle information before giving up +const MAX_RETRY_ATTEMPTS: u32 = 5; + +/// Check if there are no open handles to the given files. +/// +/// Uses undocumented NT API to obtain open handles on the system. +/// See: https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland +pub async fn check_files_closed(file_paths: &[&Path]) -> io::Result { + let current_pid = unsafe { GetCurrentProcessId() }; + let handle_table_info = query_system_handle_table()?; + + // Convert returned data into slice + let num_handles = unsafe { (*handle_table_info.inner).number_of_handles }; + let proc_entries = unsafe { + std::slice::from_raw_parts( + (*handle_table_info.as_mut_ptr()).handles.as_ptr(), + num_handles as usize, + ) + }; + + // Iterate over open file handle entries + for entry in proc_entries { + if entry.unique_process_id == current_pid { + let file_handle = HANDLE(entry.handle_value as _); + + // Filter everything except disk files + if unsafe { GetFileType(file_handle) } == FILE_TYPE_DISK { + // Obtain canonical path for file handle + let mut file_handle_path = vec![0u16; MAX_PATH as usize]; + let num_chars_without_nul = unsafe { + GetFinalPathNameByHandleW( + file_handle, + &mut file_handle_path, + FILE_NAME_NORMALIZED, + ) as usize + }; + + if num_chars_without_nul > 0 { + let path_str = OsString::from_wide(&file_handle_path[0..num_chars_without_nul]); + let file_handle_pathbuf = PathBuf::from(path_str); + if file_paths.contains(&file_handle_pathbuf.as_path()) { + return Ok(false); + } + } + } + } + } + + Ok(true) +} + +fn query_system_handle_table() -> io::Result> { + // Allocate info struct on heap with some initial value + let mut reserved_memory = SYSTEM_HANDLE_INFORMATION_INITIAL_SIZE; + let mut handle_table_info = HeapGuard::::new(reserved_memory)?; + + // Request system handle information + let mut status: NTSTATUS = NTSTATUS::default(); + for _ in 0..MAX_RETRY_ATTEMPTS { + let mut return_len = reserved_memory as u32; + status = unsafe { + NtQuerySystemInformation( + SYSTEM_HANDLE_INFORMATION_CLASS, + handle_table_info.as_mut_ptr() as _, + return_len, + &mut return_len, + ) + }; + + // Buffer is too small, resize memory and retry again. + if status == STATUS_INFO_LENGTH_MISMATCH { + // Allocate a bit more memory since the size of table can change between calls + let resize_to = (return_len as usize) * 3 / 2; + + tracing::trace!( + "Buffer is too small ({reserved_memory}), returned length: {return_len}, resizing buffer to: {resize_to}" + ); + + reserved_memory = resize_to; + handle_table_info.reallocate(reserved_memory)?; + } else { + break; + } + } + + Ok(status.ok().map(|_| handle_table_info)?) +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SystemHandleInformation { + pub number_of_handles: c_ulong, + pub handles: [SystemHandleTableEntryInfo; 1], +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct SystemHandleTableEntryInfo { + pub unique_process_id: c_ulong, + pub object_type_index: c_uchar, + pub handle_attributes: c_uchar, + pub handle_value: c_ushort, + pub object: *mut c_void, + pub granted_access: c_ulong, +} + +/// Managed heap memory +struct HeapGuard { + inner: *mut T, + process_heap: HANDLE, +} + +impl HeapGuard { + /// Allocate new memory using `HealAlloc` + fn new(length: usize) -> io::Result { + let process_heap = unsafe { GetProcessHeap()? }; + let inner: *mut T = unsafe { HeapAlloc(process_heap, HEAP_ZERO_MEMORY, length) as _ }; + + if inner.is_null() { + Err(io::Error::other("Failed to allocate memory")) + } else { + Ok(Self { + inner, + process_heap, + }) + } + } + + /// Reallocate existing chunk of memory + /// + /// On success: the internal memory pointer is replaced. + /// On failure: the internal memory pointer remains the same and still valid. + fn reallocate(&mut self, new_length: usize) -> io::Result<()> { + let new_ptr: *mut T = unsafe { + HeapReAlloc( + self.process_heap, + HEAP_ZERO_MEMORY, + Some(self.inner as _), + new_length, + ) as _ + }; + + if new_ptr.is_null() { + Err(io::Error::other("Failed to reallocate memory")) + } else { + self.inner = new_ptr; + Ok(()) + } + } + + fn as_mut_ptr(&self) -> *mut T { + self.inner + } +} + +impl Drop for HeapGuard { + fn drop(&mut self) { + #[allow(clippy::expect_used)] + unsafe { + HeapFree( + self.process_heap, + HEAP_FLAGS(0), + Some(self.inner as *mut c_void), + ) + } + .expect("HeapFree failure"); + } +} diff --git a/testnet-faucet/.env.sample b/testnet-faucet/.env.sample deleted file mode 100644 index 04b75efa9c..0000000000 --- a/testnet-faucet/.env.sample +++ /dev/null @@ -1,5 +0,0 @@ -VALIDATOR_ADDRESS= -MNEMONIC= -TESTNET_URL_1= -ACCOUNT_ADDRESS= - diff --git a/testnet-faucet/.eslintrc b/testnet-faucet/.eslintrc deleted file mode 100644 index 30a481bf55..0000000000 --- a/testnet-faucet/.eslintrc +++ /dev/null @@ -1,66 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "node": true - }, - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2019, - "sourceType": "module" - }, - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "plugins": ["react", "react-hooks"], - "rules": { - "import/prefer-default-export": "off", - "react/prop-types": "off", - "react/jsx-filename-extension": "off", - "react/jsx-props-no-spreading": "off" - }, - "overrides": [ - { - "files": "**/*.+(ts|tsx)", - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json" - }, - "plugins": ["@typescript-eslint/eslint-plugin"], - "extends": [ - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - ], - "rules": { - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off", - "no-use-before-define": [0], - "@typescript-eslint/no-use-before-define": [1], - "import/no-unresolved": 0, - "quotes": "off", - "@typescript-eslint/quotes": [ - 2, - "single", - { - "avoidEscape": true - } - ], - "@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }] - } - } - ], - "settings": { - "import/resolver": { - "root-import": { - "rootPathPrefix": "@", - "rootPathSuffix": "src", - "extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"] - } - } - } - } - \ No newline at end of file diff --git a/testnet-faucet/.gitignore b/testnet-faucet/.gitignore deleted file mode 100644 index 62d067b06e..0000000000 --- a/testnet-faucet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -dist -.parcel-cache \ No newline at end of file diff --git a/testnet-faucet/README.md b/testnet-faucet/README.md deleted file mode 100644 index e14a06362c..0000000000 --- a/testnet-faucet/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Nym Testnet Token Faucet Page - -## Installation - -`yarn install` - -## Environment variables - -Create a file in the root directory called `.env`. Copy everything from the `.env.sample` file into the `.env` file and add the required values` - -## Development environment - -`yarn dev` - -## Build - -`yarn build` diff --git a/testnet-faucet/package.json b/testnet-faucet/package.json deleted file mode 100644 index e6d896622d..0000000000 --- a/testnet-faucet/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "testnet-faucet", - "version": "1.0.0", - "description": "testnet faucet for token aquisition", - "license": "MIT", - "scripts": { - "dev": "yarn parcel ./src/index.html", - "build": "yarn parcel build ./src/index.html" - }, - "dependencies": { - "@emotion/react": "^11.6.0", - "@emotion/styled": "^11.6.0", - "@hookform/resolvers": "^2.8.3", - "@mui/icons-material": "^5.1.1", - "@mui/material": "^5.1.1", - "@nymproject/nym-validator-client": "^0.18.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.20.1", - "yup": "^0.32.11" - }, - "devDependencies": { - "@types/react": "^17.0.35", - "@types/react-dom": "^17.0.11", - "@typescript-eslint/eslint-plugin": "^5.12.0", - "@typescript-eslint/parser": "^5.12.0", - "buffer": "^6.0.3", - "crypto-browserify": "^3.12.0", - "eslint": "^8.9.0", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0", - "events": "^3.3.0", - "parcel": "^2.4.0", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "stream-browserify": "^3.0.0", - "typescript": "^4.5.5" - } -} diff --git a/testnet-faucet/src/App.tsx b/testnet-faucet/src/App.tsx deleted file mode 100644 index 4d8866a968..0000000000 --- a/testnet-faucet/src/App.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { AppBar, Container, Toolbar } from '@mui/material' -import logo from './images/nym-logo.svg' -import { NymThemeProvider } from './theme' -import { Form } from './components/form' -import { Header } from './components/header' -import { GlobalContextProvider } from './context' -import { Subheader } from './components/subheader' - -export const App = () => { - return ( - - - - - - - - - - -
- -
- - - - ) -} diff --git a/testnet-faucet/src/components/balance.tsx b/testnet-faucet/src/components/balance.tsx deleted file mode 100644 index 8e7c7d89aa..0000000000 --- a/testnet-faucet/src/components/balance.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useContext } from 'react' -import { CardHeader, Typography } from '@mui/material' -import { CancelOutlined, CheckCircleOutline } from '@mui/icons-material' -import { GlobalContext } from '../context' - -export const Balance = () => { - const { tokensAreAvailable } = useContext(GlobalContext) - - return ( - - {tokensAreAvailable - ? 'Tokens are available' - : 'Tokens are not currently available'} - - } - avatar={ - tokensAreAvailable ? ( - - ) : ( - - ) - } - /> - ) -} diff --git a/testnet-faucet/src/components/form/index.tsx b/testnet-faucet/src/components/form/index.tsx deleted file mode 100644 index 586d37a503..0000000000 --- a/testnet-faucet/src/components/form/index.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useContext } from 'react' -import { - Alert, - Button, - CircularProgress, - TextField, - useMediaQuery, -} from '@mui/material' -import { Box } from '@mui/system' -import { yupResolver } from '@hookform/resolvers/yup' -import { useForm, SubmitHandler } from 'react-hook-form' -import { validationSchema } from './validationSchema' -import { majorToMinor } from '../../utils' -import { EnumRequestType, GlobalContext } from '../../context' -import { TokenTransferComplete } from '../token-transfer' - -type TFormData = { - address: string - amount: string -} - -export const Form = ({ withInputField }: { withInputField?: boolean }) => { - const matches = useMediaQuery('(max-width:500px)') - - const { - register, - handleSubmit, - setValue, - formState: { errors, isSubmitting }, - } = useForm({ - resolver: yupResolver(validationSchema), - defaultValues: { address: '', amount: '101' }, - }) - - const { requestTokens, loadingState, error, tokensAreAvailable } = - useContext(GlobalContext) - - const resetForm = () => { - setValue('address', '') - setValue('amount', '101') - } - - const onSubmit: SubmitHandler = async (data) => { - const unymts = majorToMinor(data.amount) - await requestTokens({ - address: data.address, - unymts: unymts.toString(), - nymts: data.amount, - }) - resetForm() - } - - return ( - - - - - - - - - {error && {error}} - - - ) -} diff --git a/testnet-faucet/src/components/form/validationSchema.ts b/testnet-faucet/src/components/form/validationSchema.ts deleted file mode 100644 index 4aaeca8a6c..0000000000 --- a/testnet-faucet/src/components/form/validationSchema.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as Yup from 'yup' -import { validAmount } from '../../utils' - -export const validationSchema = Yup.object().shape({ - address: Yup.string().strict().trim('Cannot have leading space').required(), - amount: Yup.string() - .required() - .test( - 'valid-amount', - 'A valid amount is required. (max 101 nymt)', - (amount) => { - if (!amount) return false - return validAmount(amount) - } - ), -}) diff --git a/testnet-faucet/src/components/header.tsx b/testnet-faucet/src/components/header.tsx deleted file mode 100644 index ddde5a30c8..0000000000 --- a/testnet-faucet/src/components/header.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Grid, Typography } from '@mui/material' -import { Box } from '@mui/system' -import { Balance } from './balance' - -export const Header = () => { - return ( - - - - - Nym testnet Sandbox faucet - - - - - - - - ) -} diff --git a/testnet-faucet/src/components/subheader.tsx b/testnet-faucet/src/components/subheader.tsx deleted file mode 100644 index ee1ab59758..0000000000 --- a/testnet-faucet/src/components/subheader.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import { Typography } from '@mui/material' - -export const Subheader = () => { - return ( - - Send 101 NYMT tokens to your address - - ) -} diff --git a/testnet-faucet/src/components/token-transfer.tsx b/testnet-faucet/src/components/token-transfer.tsx deleted file mode 100644 index d98d2908a8..0000000000 --- a/testnet-faucet/src/components/token-transfer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useContext } from 'react' -import { Card, CardHeader, Link, Typography } from '@mui/material' -import { GlobalContext, urls } from '../context/index' - -export const TokenTransferComplete = () => { - const { tokenTransfer } = useContext(GlobalContext) - - if (tokenTransfer) { - return ( - `1px solid ${theme.palette.common.white}`, - p: 2, - overflow: 'auto', - }} - > - - - Successfully transferred {tokenTransfer.amount} NYMT to - {' '} - - {tokenTransfer.address} - - - } - /> - - ) - } - return null -} diff --git a/testnet-faucet/src/context/index.tsx b/testnet-faucet/src/context/index.tsx deleted file mode 100644 index 74d7af06d2..0000000000 --- a/testnet-faucet/src/context/index.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { createContext, useEffect, useState } from 'react' -import ClientValidator, { - nativeToPrintable, -} from '@nymproject/nym-validator-client' -import { handleError } from '../utils' -import { useCookie } from '..//hooks/useCookie' - -const { VALIDATOR_ADDRESS, MNEMONIC, TESTNET_URL_1, ACCOUNT_ADDRESS } = - process.env - -export const urls = { - blockExplorer: 'https://sandbox-blocks.nymtech.net', -} - -type TGlobalContext = { - getBalance: () => void - requestTokens: ({ - address, - unymts, - }: { - address: string - unymts: string - nymts: string - }) => void - loadingState: TLoadingState - balance?: string - tokenTransfer?: { address: string; amount: string } - error?: string - tokensAreAvailable: boolean -} - -type TLoadingState = { - isLoading: boolean - requestType?: EnumRequestType -} - -export enum EnumRequestType { - balance = 'balance', - tokens = 'tokens', -} - -export const GlobalContext = createContext({} as TGlobalContext) - -export const GlobalContextProvider: FCWithChildren = ({ children }) => { - const [validator, setValidator] = useState() - const [loadingState, setLoadingState] = useState({ - isLoading: false, - requestType: undefined, - }) - const [balance, setBalance] = useState() - const [error, setError] = useState() - const [tokenTransfer, setTokenTransfer] = - useState<{ address: string; amount: string }>() - - const getValidator = async () => { - const Validator = await ClientValidator.connect( - VALIDATOR_ADDRESS, - MNEMONIC, - [TESTNET_URL_1], - 'nymt' - ) - setValidator(Validator) - } - - useEffect(() => { - getValidator() - }, []) - - useEffect(() => { - if (error) console.error(error) - }, [error]) - - const [hasMadePreviousRequest, setHasMadePreviousRequest] = useCookie( - 'hasUsedFaucet', - false - ) - - const getBalance = async () => { - setLoadingState({ isLoading: true, requestType: EnumRequestType.balance }) - try { - const balance = await validator?.getBalance(ACCOUNT_ADDRESS) - const tokens = nativeToPrintable(balance?.amount || '') - setBalance(tokens) - } catch (e) { - setError(`An error occured while getting the balance: ${e}`) - } finally { - setLoadingState({ isLoading: false, requestType: undefined }) - } - } - - useEffect(() => { - if (validator || tokenTransfer) getBalance() - }, [validator, tokenTransfer]) - - const resetGlobalState = () => { - setError(undefined) - setTokenTransfer(undefined) - } - - const requestTokens = async ({ - address, - unymts, - nymts, - }: { - address: string - unymts: string - nymts: string - }) => { - resetGlobalState() - setLoadingState({ isLoading: true, requestType: EnumRequestType.tokens }) - - if (!hasMadePreviousRequest) { - try { - await validator?.send(ACCOUNT_ADDRESS, address, [ - { amount: unymts, denom: 'unymt' }, - ]) - setTokenTransfer({ address, amount: nymts }) - setHasMadePreviousRequest(true, 1) - } catch (e) { - setError(handleError(e as Error)) - } - } else { - setError('Tokens are not currently available') - } - - setLoadingState({ isLoading: false, requestType: undefined }) - } - - const tokensAreAvailable = - !hasMadePreviousRequest && Boolean(balance && +balance >= 101) - - return ( - - {children} - - ) -} diff --git a/testnet-faucet/src/hooks/useCookie.ts b/testnet-faucet/src/hooks/useCookie.ts deleted file mode 100644 index ac99760553..0000000000 --- a/testnet-faucet/src/hooks/useCookie.ts +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useState } from 'react' - -const getItem = (key: string) => - document.cookie.split('; ').reduce((total, currentCookie) => { - const item = currentCookie.split('=') - const storedKey = item[0] - const storedValue = item[1] - - return key === storedKey ? decodeURIComponent(storedValue) : total - }, '') - -const setItem = (key: string, value: string, numberOfMinutes: number) => { - const now = new Date() - - // set the time to be now + numberOfMinutes - now.setTime(now.getTime() + numberOfMinutes * 60 * 1000) - - document.cookie = `${key}=${value}; expires=${now.toUTCString()}; path=/` -} - -export const useCookie = (key: string, defaultValue: any) => { - const getCookie = () => getItem(key) || defaultValue - const [cookie, setCookie] = useState(getCookie()) - - const updateCookie = (value: string, numberOfDays: number) => { - setCookie(value) - setItem(key, value, numberOfDays) - } - - return [cookie, updateCookie] -} diff --git a/testnet-faucet/src/hooks/useLocalStorage.ts b/testnet-faucet/src/hooks/useLocalStorage.ts deleted file mode 100644 index 3bc0e06c62..0000000000 --- a/testnet-faucet/src/hooks/useLocalStorage.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from 'react' - -export function useLocalStorage(key: string, initialValue: T) { - const [storedValue, setStoredValue] = useState(() => { - if (typeof window === 'undefined') { - return initialValue - } - try { - const item = window.localStorage.getItem(key) - - return item ? JSON.parse(item) : initialValue - } catch (error) { - console.log(error) - return initialValue - } - }) - - const setValue = (value: T | ((val: T) => T)) => { - try { - const valueToStore = - value instanceof Function ? value(storedValue) : value - - setStoredValue(valueToStore) - - if (typeof window !== 'undefined') { - window.localStorage.setItem(key, JSON.stringify(valueToStore)) - } - } catch (error) { - console.log(error) - } - } - return [storedValue, setValue] as const -} diff --git a/testnet-faucet/src/images/favicon.png b/testnet-faucet/src/images/favicon.png deleted file mode 100644 index a8f287c83d..0000000000 Binary files a/testnet-faucet/src/images/favicon.png and /dev/null differ diff --git a/testnet-faucet/src/images/nym-logo.svg b/testnet-faucet/src/images/nym-logo.svg deleted file mode 100644 index ddc2f91228..0000000000 --- a/testnet-faucet/src/images/nym-logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/testnet-faucet/src/index.html b/testnet-faucet/src/index.html deleted file mode 100644 index 8f4a28f745..0000000000 --- a/testnet-faucet/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Nym Testnet Faucet - - -
- - - diff --git a/testnet-faucet/src/index.tsx b/testnet-faucet/src/index.tsx deleted file mode 100644 index 76438b16a8..0000000000 --- a/testnet-faucet/src/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReactDOM from 'react-dom' -import { App } from './App' - -const app = document.getElementById('app') -ReactDOM.render(, app) diff --git a/testnet-faucet/src/theme/index.tsx b/testnet-faucet/src/theme/index.tsx deleted file mode 100644 index dff02c9aba..0000000000 --- a/testnet-faucet/src/theme/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react' -import { createTheme, ThemeProvider } from '@mui/material/styles' -import { CssBaseline } from '@mui/material' -import { getDesignTokens } from './theme' - -export const NymThemeProvider: FCWithChildren = ({ children }) => { - const theme = createTheme(getDesignTokens('dark')) - return ( - - - {children} - - ) -} diff --git a/testnet-faucet/src/theme/mui-theme.ts b/testnet-faucet/src/theme/mui-theme.ts deleted file mode 100644 index 6e9cd1063f..0000000000 --- a/testnet-faucet/src/theme/mui-theme.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */ -import { - Theme, - ThemeOptions, - Palette, - PaletteOptions, -} from '@mui/material/styles' -import { PaletteMode } from '@mui/material' - -/** - * If you are unfamiliar with Material UI theming, please read the following first: - * - https://mui.com/customization/theming/ - * - https://mui.com/customization/palette/ - * - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette - * - * This file adds typings to the theme using Typescript's module augmentation. - * - * Read the following if you are unfamiliar with module augmentation and declaration merging. Then - * look at the recommendations from Material UI docs for implementation: - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation - * - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces - * - https://mui.com/customization/palette/#adding-new-colors - * - * - * IMPORTANT: - * - * The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than - * `type Foo = { ... }` - this is necessary to merge the definitions. - */ - -declare module '@mui/material/styles' { - /** - * This interface defines a palette used across Nym for branding - */ - interface NymPalette { - highlight: string; - text: { - nav: string; - footer: string; - } - } - - interface NymPaletteVariant { - mode: PaletteMode - background: { - main: string; - paper: string; - warn: string; - } - text: { - main: string; - muted: string; - warn: string; - } - topNav: { - background: string; - } - nav: { - background: string; - hover: string; - } - } - - /** - * A palette definition only for the Network Explorer that extends the Nym palette - */ - interface NetworkExplorerPalette { - networkExplorer: { - map: { - stroke: string; - fills: string[]; - } - background: { - tertiary: string; - } - topNav: { - background: string; - socialIcons: string; - appBar: string; - } - nav: { - selected: { - main: string; - nested: string; - } - background: string; - hover: string; - text: string; - } - footer: { - socialIcons: string; - } - } - } - - interface NymPaletteAndNetworkExplorerPalette { - nym: NymPalette; - } - - type NymPaletteAndNetworkExplorerPaletteOptions = - Partial - - /** - * Add anything not palette related to the theme here - */ - interface NymTheme {} - - /** - * This augments the definitions of the MUI Theme with the Nym theme, as well as - * a partial `ThemeOptions` type used by `createTheme` - * - * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below - */ - interface Theme extends NymTheme {} - interface ThemeOptions extends Partial {} - interface Palette extends NymPaletteAndNetworkExplorerPalette {} - interface PaletteOptions extends NymPaletteAndNetworkExplorerPaletteOptions {} -} diff --git a/testnet-faucet/src/theme/theme.ts b/testnet-faucet/src/theme/theme.ts deleted file mode 100644 index 2b08fff5e0..0000000000 --- a/testnet-faucet/src/theme/theme.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { PaletteMode } from '@mui/material' -import { - PaletteOptions, - NymPalette, - ThemeOptions, - createTheme, - NymPaletteVariant, -} from '@mui/material/styles' - -//----------------------------------------------------------------------------------------------- -// Nym palette type definitions -// - -/** - * The Nym palette. - * - * IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct - * colours for dark/light mode. - */ -const nymPalette: NymPalette = { - /** emphasises important elements */ - highlight: '#FB6E4E', - text: { - nav: '#F2F2F2', - /** footer text colour */ - footer: '#666B77', - }, -} - -const darkMode: NymPaletteVariant = { - mode: 'dark', - background: { - main: '#070B15', - paper: '#242C3D', - }, - text: { - main: '#F2F2F2', - }, - topNav: { - background: '#111826', - }, - nav: { - background: '#242C3D', - hover: '#111826', - }, -} - -const lightMode: NymPaletteVariant = { - mode: 'light', - background: { - main: '#F2F2F2', - paper: '#FFFFFF', - }, - text: { - main: '#666666', - }, - topNav: { - background: '#111826', - }, - nav: { - background: '#242C3D', - hover: '#111826', - }, -} - -//----------------------------------------------------------------------------------------------- -// Nym palettes for light and dark mode -// - -/** - * Map a Nym palette variant onto the MUI palette - */ -const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({ - text: { - primary: variant.text.main, - }, - primary: { - main: nymPalette.highlight, - contrastText: '#fff', - }, - background: { - default: variant.background.main, - paper: variant.background.paper, - }, -}) - -/** - * Returns the Network Explorer palette for light mode. - */ -const createLightModePalette = (): PaletteOptions => ({ - nym: { - ...nymPalette, - }, - ...variantToMUIPalette(lightMode), -}) - -/** - * Returns the Network Explorer palette for dark mode. - */ -const createDarkModePalette = (): PaletteOptions => ({ - nym: { - ...nymPalette, - }, - ...variantToMUIPalette(darkMode), -}) - -/** - * IMPORANT: if you need to get the default MUI theme, use the following - * - * import { createTheme as systemCreateTheme } from '@mui/system'; - * - * // get the MUI system defaults for light mode - * const systemTheme = systemCreateTheme({ palette: { mode: 'light' } }); - * - * - * return { - * // change `primary` to default MUI `success` - * primary: { - * main: systemTheme.palette.success.main, - * }, - * nym: { - * ...nymPalette, - * }, - * }; - */ - -//----------------------------------------------------------------------------------------------- -// Nym theme overrides -// - -/** - * Gets the theme options to be passed to `createTheme`. - * - * Based on pattern from https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette. - * - * @param mode The theme mode: 'light' or 'dark' - */ -export const getDesignTokens = (mode: PaletteMode): ThemeOptions => { - // first, create the palette from user's choice of light or dark mode - const { palette } = createTheme({ - palette: { - mode, - ...(mode === 'light' - ? createLightModePalette() - : createDarkModePalette()), - }, - }) - - // then customise theme and components - return { - typography: { - fontFamily: [ - 'Open Sans', - 'sans-serif', - 'BlinkMacSystemFont', - 'Roboto', - 'Oxygen', - 'Ubuntu', - 'Helvetica Neue', - ].join(','), - fontSize: 14, - fontWeightRegular: 600, - }, - transitions: { - duration: { - shortest: 150, - shorter: 200, - short: 250, - standard: 300, - complex: 375, - enteringScreen: 225, - leavingScreen: 195, - }, - easing: { - easeIn: 'cubic-bezier(0.4, 0, 1, 1)', - }, - }, - palette, - } -} diff --git a/testnet-faucet/src/types.d.ts b/testnet-faucet/src/types.d.ts deleted file mode 100644 index c4ffacb1c2..0000000000 --- a/testnet-faucet/src/types.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -declare module '*.svg' { - const content: any - export default content -} - -namespace NodeJS { - export interface Process { - env: { - VALIDATOR_ADDRESS: string - MNEMONIC: string - TESTNET_URL_1: string - TESTNET_URL_2: string - ACCOUNT_ADDRESS: string - } - } -} diff --git a/testnet-faucet/src/utils.ts b/testnet-faucet/src/utils.ts deleted file mode 100644 index d09be35409..0000000000 --- a/testnet-faucet/src/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/currency' - -export const majorToMinor = (raw: string): number => { - const native = printableBalanceToNative(raw) - return parseInt(native) -} - -export const validAmount = (amount: string): boolean => { - if (isNaN(+amount)) return false - if (+amount > 101) return false - return true -} - -export const handleError = (error: Error) => { - if (error.message.includes('invalid address')) - return 'Invalid address. Please check and try again' - if (error.message.includes('insufficient funds')) - return 'The faucet is running dry. Please try again in a minute' -} diff --git a/testnet-faucet/tsconfig.json b/testnet-faucet/tsconfig.json deleted file mode 100644 index 93ed170c67..0000000000 --- a/testnet-faucet/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "noFallthroughCasesInSwitch": true, - "skipLibCheck": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": false, - "jsx": "react-jsx", - "sourceMap": true, - "baseUrl": "." - }, - "exclude": ["node_modules", "dist"] -} diff --git a/testnet-faucet/typings/FC.d.ts b/testnet-faucet/typings/FC.d.ts deleted file mode 100644 index 926141e0cb..0000000000 --- a/testnet-faucet/typings/FC.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type FCWithChildren

= React.FC> diff --git a/testnet-faucet/yarn.lock b/testnet-faucet/yarn.lock deleted file mode 100644 index 139a1474dd..0000000000 --- a/testnet-faucet/yarn.lock +++ /dev/null @@ -1,3713 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" - integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== - dependencies: - "@babel/highlight" "^7.16.0" - -"@babel/helper-module-imports@^7.12.13": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" - integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-plugin-utils@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/highlight@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" - integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/plugin-syntax-jsx@^7.12.13": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.0.tgz#f9624394317365a9a88c82358d3f8471154698f1" - integrity sha512-8zv2+xiPHwly31RK4RmnEYY5zziuF3O7W2kIDW+07ewWDh6Oi0dRq8kwvulRkFgt6DB97RlKs5c1y068iPlCUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7": - version "7.26.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" - integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/types@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" - integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" - to-fast-properties "^2.0.0" - -"@confio/ics23@^0.6.3": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@confio/ics23/-/ics23-0.6.5.tgz#9c21a61089d4c3c2429875a69d6d9cd8c87512aa" - integrity sha512-1GdPMsaP/l8JSF4P4HWFLBhdcxHcJT8lS0nknBYNSZ1XrJOsJKUy6EkOwd9Pa1qJkXzY2gyNv7MdHR+AIwSTAg== - dependencies: - js-sha512 "^0.8.0" - protobufjs "^6.8.8" - ripemd160 "^2.0.2" - sha.js "^2.4.11" - -"@cosmjs/amino@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.25.6.tgz#cdf9632253bfab7b1d2ef967124953d7bf16351f" - integrity sha512-9dXN2W7LHjDtJUGNsQ9ok0DfxeN3ca/TXnxCR3Ikh/5YqBqxI8Gel1J9PQO9L6EheYyh045Wff4bsMaLjyEeqQ== - dependencies: - "@cosmjs/crypto" "^0.25.6" - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/utils" "^0.25.6" - -"@cosmjs/cosmwasm-launchpad@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-launchpad/-/cosmwasm-launchpad-0.25.6.tgz#b03a96c8d5b5b4742ec4c56b4a01b1f4f33f7f57" - integrity sha512-rzpYg/A8tvXbY6p89LabPB1mqCtTUv+33nN+s+VWMH0oOl0LSIgLE0yIT61kwTaf2dWQvRVeFaiRLFC72/w/zw== - dependencies: - "@cosmjs/crypto" "^0.25.6" - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/launchpad" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/utils" "^0.25.6" - pako "^2.0.2" - -"@cosmjs/cosmwasm-stargate@^0.25.5": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.25.6.tgz#26a788213833aaffc4bc1a87df650741e0b8fd63" - integrity sha512-STaxur5Xk5hqndLEztpa1TqvpdQyg2xD//2ZNFw2fgxf1JQtP0RfFbA3bnBkYwbGARx5cnng8QeZmSUcXnTVxA== - dependencies: - "@cosmjs/amino" "^0.25.6" - "@cosmjs/cosmwasm-launchpad" "^0.25.6" - "@cosmjs/crypto" "^0.25.6" - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/proto-signing" "^0.25.6" - "@cosmjs/stargate" "^0.25.6" - "@cosmjs/tendermint-rpc" "^0.25.6" - "@cosmjs/utils" "^0.25.6" - long "^4.0.0" - pako "^2.0.2" - protobufjs "~6.10.2" - -"@cosmjs/crypto@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.25.6.tgz#695d2d0d2195bdbdd5825d415385646244900bbb" - integrity sha512-ec+YcQLrg2ibcxtNrh4FqQnG9kG9IE/Aik2NH6+OXQdFU/qFuBTxSFcKDgzzBOChwlkXwydllM9Jjbp+dgIzRw== - dependencies: - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/utils" "^0.25.6" - bip39 "^3.0.2" - bn.js "^4.11.8" - elliptic "^6.5.3" - js-sha3 "^0.8.0" - libsodium-wrappers "^0.7.6" - ripemd160 "^2.0.2" - sha.js "^2.4.11" - -"@cosmjs/encoding@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.25.6.tgz#da741a33eaf063a6d3611d7d68db5ca3938e0ef5" - integrity sha512-0imUOB8XkUstI216uznPaX1hqgvLQ2Xso3zJj5IV5oJuNlsfDj9nt/iQxXWbJuettc6gvrFfpf+Vw2vBZSZ75g== - dependencies: - base64-js "^1.3.0" - bech32 "^1.1.4" - readonly-date "^1.0.0" - -"@cosmjs/json-rpc@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.25.6.tgz#4f888630e84ee114b164758ec5b48f134068656c" - integrity sha512-Mn9og3/IEzC6jWoYXs0ONqFJc8HxVjXzrZPLgaRRdMZEUBvctxdhynau1wbE4LdkYQHbu4aiRu1q1jMYGFAj4A== - dependencies: - "@cosmjs/stream" "^0.25.6" - xstream "^11.14.0" - -"@cosmjs/launchpad@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/launchpad/-/launchpad-0.25.6.tgz#c75f5d21be57af55fcb892f929520fa97f2d5bcc" - integrity sha512-4Yhn4cX50UE6jZz/hWqKeeCmvrlrz0BBwOdYX/29k25FqP+oLAow1xKm6UxgYuuAq8Pg/bUvswxSqwegZJTb6g== - dependencies: - "@cosmjs/amino" "^0.25.6" - "@cosmjs/crypto" "^0.25.6" - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/utils" "^0.25.6" - axios "^0.21.1" - fast-deep-equal "^3.1.3" - -"@cosmjs/math@^0.25.5", "@cosmjs/math@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.25.6.tgz#25c7b106aaded889a5b80784693caa9e654b0c28" - integrity sha512-Fmyc9FJ8KMU34n7rdapMJrT/8rx5WhMw2F7WLBu7AVLcBh0yWsXIcMSJCoPHTOnMIiABjXsnrrwEaLrOOBfu6A== - dependencies: - bn.js "^4.11.8" - -"@cosmjs/proto-signing@^0.25.5", "@cosmjs/proto-signing@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.25.6.tgz#d9fc57b8e0a46cda97e192bd0435157b24949ff8" - integrity sha512-JpQ+Vnv9s6i3x8f3Jo0lJZ3VMnj3R5sMgX+8ti1LtB7qEYRR85qbDrEG9hDGIKqJJabvrAuCHnO6hYi0vJEJHA== - dependencies: - "@cosmjs/amino" "^0.25.6" - long "^4.0.0" - protobufjs "~6.10.2" - -"@cosmjs/socket@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.25.6.tgz#7876bc24e1f16315fbb9e97bd63dea65ba90647d" - integrity sha512-hu+pW3Fy0IuhstXgxnZ2Iq0RUnGYoTWfqrxbTsgXBJge4MpEQs2YwGXgJZPMJXedBQivG0tU3r/Wvam0EWuRkQ== - dependencies: - "@cosmjs/stream" "^0.25.6" - isomorphic-ws "^4.0.1" - ws "^7" - xstream "^11.14.0" - -"@cosmjs/stargate@^0.25.5", "@cosmjs/stargate@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.25.6.tgz#ed7428aafd8fe1c12dc4b86f7967deb88117f61a" - integrity sha512-+LM1sK6vGuotJF9fBCBlaDL/yJhfzCV6KbsaWt3teHAKZhKYOd/9mKGiB4H9bfg4h3r+e+FZGiNkH/mdXkcgEw== - dependencies: - "@confio/ics23" "^0.6.3" - "@cosmjs/amino" "^0.25.6" - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/proto-signing" "^0.25.6" - "@cosmjs/stream" "^0.25.6" - "@cosmjs/tendermint-rpc" "^0.25.6" - "@cosmjs/utils" "^0.25.6" - long "^4.0.0" - protobufjs "~6.10.2" - -"@cosmjs/stream@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.25.6.tgz#1bf7536ed919be3fd7fbffa477c98ef5a93eac70" - integrity sha512-2mXIzf+WaFd+GSrRaJJETVXeZoC5sosuKChFERxSY8zXQ/f3OaG9J6m+quHpPbU3nAIEtnF1jgBVqJiD+NKwGQ== - dependencies: - xstream "^11.14.0" - -"@cosmjs/tendermint-rpc@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.25.6.tgz#8198a08b0d0e1d6580618f3f22db83366329c9c3" - integrity sha512-wsvxTI7DReWJu+SVlXLblzh5NJppnh1mljuaTHodMf7HBxrXdbcCcBAO8oMbMgEqOASEY5G+z51wktxhrn9RtA== - dependencies: - "@cosmjs/crypto" "^0.25.6" - "@cosmjs/encoding" "^0.25.6" - "@cosmjs/json-rpc" "^0.25.6" - "@cosmjs/math" "^0.25.6" - "@cosmjs/socket" "^0.25.6" - "@cosmjs/stream" "^0.25.6" - axios "^0.21.1" - readonly-date "^1.0.0" - xstream "^11.14.0" - -"@cosmjs/utils@^0.25.6": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.25.6.tgz#934d9a967180baa66163847616a74358732227ca" - integrity sha512-ofOYiuxVKNo238vCPPlaDzqPXy2AQ/5/nashBo5rvPZJkxt9LciGfUEQWPCOb1BIJDNx2Dzu0z4XCf/dwzl0Dg== - -"@emotion/babel-plugin@^11.3.0": - version "11.3.0" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7" - integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA== - dependencies: - "@babel/helper-module-imports" "^7.12.13" - "@babel/plugin-syntax-jsx" "^7.12.13" - "@babel/runtime" "^7.13.10" - "@emotion/hash" "^0.8.0" - "@emotion/memoize" "^0.7.5" - "@emotion/serialize" "^1.0.2" - babel-plugin-macros "^2.6.1" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "^4.0.3" - -"@emotion/cache@^11.6.0": - version "11.6.0" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.6.0.tgz#65fbdbbe4382f1991d8b20853c38e63ecccec9a1" - integrity sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ== - dependencies: - "@emotion/memoize" "^0.7.4" - "@emotion/sheet" "^1.1.0" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" - stylis "^4.0.10" - -"@emotion/hash@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" - integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== - -"@emotion/is-prop-valid@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz#cbd843d409dfaad90f9404e7c0404c55eae8c134" - integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw== - dependencies: - "@emotion/memoize" "^0.7.4" - -"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.5.tgz#2c40f81449a4e554e9fc6396910ed4843ec2be50" - integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== - -"@emotion/react@^11.6.0": - version "11.6.0" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.6.0.tgz#61fcb95c1e01255734c2c721cb9beabcf521eb0f" - integrity sha512-23MnRZFBN9+D1lHXC5pD6z4X9yhPxxtHr6f+iTGz6Fv6Rda0GdefPrsHL7otsEf+//7uqCdT5QtHeRxHCERzuw== - dependencies: - "@babel/runtime" "^7.13.10" - "@emotion/cache" "^11.6.0" - "@emotion/serialize" "^1.0.2" - "@emotion/sheet" "^1.1.0" - "@emotion/utils" "^1.0.0" - "@emotion/weak-memoize" "^0.2.5" - hoist-non-react-statics "^3.3.1" - -"@emotion/serialize@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965" - integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== - dependencies: - "@emotion/hash" "^0.8.0" - "@emotion/memoize" "^0.7.4" - "@emotion/unitless" "^0.7.5" - "@emotion/utils" "^1.0.0" - csstype "^3.0.2" - -"@emotion/sheet@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2" - integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== - -"@emotion/styled@^11.6.0": - version "11.6.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.6.0.tgz#9230d1a7bcb2ebf83c6a579f4c80e0664132d81d" - integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw== - dependencies: - "@babel/runtime" "^7.13.10" - "@emotion/babel-plugin" "^11.3.0" - "@emotion/is-prop-valid" "^1.1.1" - "@emotion/serialize" "^1.0.2" - "@emotion/utils" "^1.0.0" - -"@emotion/unitless@^0.7.5": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" - integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== - -"@emotion/utils@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" - integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== - -"@emotion/weak-memoize@^0.2.5": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" - integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== - -"@eslint/eslintrc@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3" - integrity sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.3.1" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@hookform/resolvers@^2.8.3": - version "2.8.3" - resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.8.3.tgz#d362360f6057bcca7318ad22d49e5f0c47290e27" - integrity sha512-9G6/vCUNvg3O4MeAK4l749syOz/2r1DF/Pq3Njuk3sI4ObHoRyV0nxio5NrNLGC+7N1Ixu9vJ/loNhaUNXTX6A== - -"@humanwhocodes/config-array@^0.9.2": - version "0.9.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.3.tgz#f2564c744b387775b436418491f15fce6601f63e" - integrity sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@^0.3.9": - version "0.3.15" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" - integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" - integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ== - -"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3" - integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw== - -"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367" - integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg== - -"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399" - integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA== - -"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f" - integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA== - -"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407" - integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ== - -"@mui/base@5.0.0-alpha.55": - version "5.0.0-alpha.55" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.55.tgz#564d6c9374b4cfe86a4493512356047636076d4f" - integrity sha512-caPa04xwZF5Gv7qkto32xRBwubNgkjbXQngqp8PN10DQ/XcLtoe4PqrSPjwWBH0iNUZSRDf2HPP71tIU7bdR7Q== - dependencies: - "@babel/runtime" "^7.16.3" - "@emotion/is-prop-valid" "^1.1.1" - "@mui/utils" "^5.1.1" - "@popperjs/core" "^2.4.4" - clsx "^1.1.1" - prop-types "^15.7.2" - react-is "^17.0.2" - -"@mui/icons-material@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.1.1.tgz#f3f44275855e924cdf5e379da87299d633854b53" - integrity sha512-tLM1/QhVAgcetEscZa8BlM1IRRaoNxjhFzQOIs5wAuuVhHSrB8zZCKugpZVIZ1nKyQqLgVEa9TbtWpo5jLrnRQ== - dependencies: - "@babel/runtime" "^7.16.3" - -"@mui/material@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.1.1.tgz#72ba1ce8c253697df96e24cd1f555d5c2569376d" - integrity sha512-3mhuKlWnTa1r5cJ8mV66NXXmOB6Ck564oq4X8Ai0CeHqj0f6xCBHOgWXQtX6Cc8Yhf81MJkaN92AECVUpUHqLQ== - dependencies: - "@babel/runtime" "^7.16.3" - "@mui/base" "5.0.0-alpha.55" - "@mui/system" "^5.1.1" - "@mui/types" "^7.1.0" - "@mui/utils" "^5.1.1" - "@types/react-transition-group" "^4.4.4" - clsx "^1.1.1" - csstype "^3.0.9" - hoist-non-react-statics "^3.3.2" - prop-types "^15.7.2" - react-is "^17.0.2" - react-transition-group "^4.4.2" - -"@mui/private-theming@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.1.1.tgz#3e9e91d81a8e69dcb5c4499236e26d684cd4a919" - integrity sha512-h+MGzBVSH7GgXou4aIraJhakygTYIWvvxvTm81Y6RmwRcrzv8szDQeRDiM7iOVjqsS33dXfMkTi7csRCgeErsg== - dependencies: - "@babel/runtime" "^7.16.3" - "@mui/utils" "^5.1.1" - prop-types "^15.7.2" - -"@mui/styled-engine@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.1.1.tgz#eb6a54546e65527786ab40aef9e2f20cc2fa00f5" - integrity sha512-vThhmTezPjBcn6CEeVuFqB3wgANnxHgYXn0wsr+OIgevkgSHeRfVn6mpSa66oTFGb+paPtH4ASqeUvL5Sscg4w== - dependencies: - "@babel/runtime" "^7.16.3" - "@emotion/cache" "^11.6.0" - prop-types "^15.7.2" - -"@mui/system@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.1.1.tgz#64bf13c9137fdd858834f3921ded276b6b9767c4" - integrity sha512-RWaM/7wAvSOX39r13in3KrLXWsd0cSkk1P/MOCW2eVY13MJIAuDUl5ZoF1uos9kWWJJge+lE77XWmYqXYrxPLw== - dependencies: - "@babel/runtime" "^7.16.3" - "@mui/private-theming" "^5.1.1" - "@mui/styled-engine" "^5.1.1" - "@mui/types" "^7.1.0" - "@mui/utils" "^5.1.1" - clsx "^1.1.1" - csstype "^3.0.9" - prop-types "^15.7.2" - -"@mui/types@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.0.tgz#5ed928c5a41cfbf9a4be82ea3bbdc47bcc9610d5" - integrity sha512-Hh7ALdq/GjfIwLvqH3XftuY3bcKhupktTm+S6qRIDGOtPtRuq2L21VWzOK4p7kblirK0XgGVH5BLwa6u8z/6QQ== - -"@mui/utils@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.1.1.tgz#3cb2c049731dacb3830336bc8011141e84434aad" - integrity sha512-rqakHf0IMaasDo1EcYqkx13VTxeoQoGf/3RxQuazQFKzF7d2uylFwNyb6bnUJGNe2/akiIMk/qiub58sYrwxVQ== - dependencies: - "@babel/runtime" "^7.16.3" - "@types/prop-types" "^15.7.4" - "@types/react-is" "^16.7.1 || ^17.0.0" - prop-types "^15.7.2" - react-is "^17.0.2" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@nymproject/nym-validator-client@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@nymproject/nym-validator-client/-/nym-validator-client-0.18.0.tgz#4dd72bafdf6c72b603242f32c0bb9a1f9e475b98" - integrity sha512-FO1T15S2BJVuMoPA2yOaH40aD3hKJPKJVyX5ix7eJEbBWIdsYNoVeVc/soHhaAU1FIy2uU0G/FZQkaUYJGGb7Q== - dependencies: - "@cosmjs/cosmwasm-stargate" "^0.25.5" - "@cosmjs/math" "^0.25.5" - "@cosmjs/proto-signing" "^0.25.5" - "@cosmjs/stargate" "^0.25.5" - axios "^0.21.1" - -"@parcel/bundler-default@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.4.0.tgz#a3b88131601821514cd932b3b2ada226d7d3404d" - integrity sha512-RaXlxo0M51739Ko3bsOJpDBZlJ+cqkDoBTozNeSc65jS2TMBIBWLMapm8095qmty39OrgYNhzjgPiIlKDS/LWA== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/cache@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.4.0.tgz#aa83243a00ee861183c6a1ce880292c6c1175022" - integrity sha512-oOudoAafrCAHQY0zkU7gVHG1pAGBUz9rht7Tx4WupTmAH0O0F5UnZs6XbjoBJaPHg+CYUXK7v9wQcrNA72E3GA== - dependencies: - "@parcel/fs" "2.4.0" - "@parcel/logger" "2.4.0" - "@parcel/utils" "2.4.0" - lmdb "2.2.4" - -"@parcel/codeframe@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.4.0.tgz#c9a78a558f9a5c628c162d5774e2223d236d9f20" - integrity sha512-PJ3W9Z0sjoS2CANyo50c+LEr9IRZrtu0WsVPSYZ5ZYRuSXrSa/6PcAlnkyDk2+hi7Od8ncT2bmDexl0Oar3Jyg== - dependencies: - chalk "^4.1.0" - -"@parcel/compressor-raw@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.4.0.tgz#25c701d5a3a4cea843f8d0806b9192a8b107f5d8" - integrity sha512-ZErX14fTc0gKIgtnuqW7Clfln4dpXWfUaJQQIf5C3x/LkpUeEhdXeKntkvSxOddDk2JpIKDwqzAxEMZUnDo4Nw== - dependencies: - "@parcel/plugin" "2.4.0" - -"@parcel/config-default@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.4.0.tgz#a9ec50b99923d6f589b44bf9fd476425879102aa" - integrity sha512-pFOPBXPO6HGqNWTLkcK5i8haMOrRgUouUhcWPGWDpN9IPUYFK2E/O1E/uyMjIA1mSL3FnazI+jJwZ45NhKPpIA== - dependencies: - "@parcel/bundler-default" "2.4.0" - "@parcel/compressor-raw" "2.4.0" - "@parcel/namer-default" "2.4.0" - "@parcel/optimizer-css" "2.4.0" - "@parcel/optimizer-htmlnano" "2.4.0" - "@parcel/optimizer-image" "2.4.0" - "@parcel/optimizer-svgo" "2.4.0" - "@parcel/optimizer-terser" "2.4.0" - "@parcel/packager-css" "2.4.0" - "@parcel/packager-html" "2.4.0" - "@parcel/packager-js" "2.4.0" - "@parcel/packager-raw" "2.4.0" - "@parcel/packager-svg" "2.4.0" - "@parcel/reporter-dev-server" "2.4.0" - "@parcel/resolver-default" "2.4.0" - "@parcel/runtime-browser-hmr" "2.4.0" - "@parcel/runtime-js" "2.4.0" - "@parcel/runtime-react-refresh" "2.4.0" - "@parcel/runtime-service-worker" "2.4.0" - "@parcel/transformer-babel" "2.4.0" - "@parcel/transformer-css" "2.4.0" - "@parcel/transformer-html" "2.4.0" - "@parcel/transformer-image" "2.4.0" - "@parcel/transformer-js" "2.4.0" - "@parcel/transformer-json" "2.4.0" - "@parcel/transformer-postcss" "2.4.0" - "@parcel/transformer-posthtml" "2.4.0" - "@parcel/transformer-raw" "2.4.0" - "@parcel/transformer-react-refresh-wrap" "2.4.0" - "@parcel/transformer-svg" "2.4.0" - -"@parcel/core@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.4.0.tgz#c0533760f9d4f04757c69152aa9a6219828ac1ee" - integrity sha512-EWZ2UWtIuwDc3fgsKyyTLpNNPoG8Yk2L117ICWF/+cqY8z/wJHm2KwLbeplDeq524shav0GJ9O4CemP3JPx0Nw== - dependencies: - "@parcel/cache" "2.4.0" - "@parcel/diagnostic" "2.4.0" - "@parcel/events" "2.4.0" - "@parcel/fs" "2.4.0" - "@parcel/graph" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/logger" "2.4.0" - "@parcel/package-manager" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - "@parcel/workers" "2.4.0" - abortcontroller-polyfill "^1.1.9" - base-x "^3.0.8" - browserslist "^4.6.6" - clone "^2.1.1" - dotenv "^7.0.0" - dotenv-expand "^5.1.0" - json-source-map "^0.6.1" - json5 "^2.2.0" - msgpackr "^1.5.4" - nullthrows "^1.1.1" - semver "^5.7.1" - -"@parcel/css-darwin-arm64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-darwin-arm64/-/css-darwin-arm64-1.7.3.tgz#dcc0286f79b17cba945ff53915a9d34a1ce62c47" - integrity sha512-m3HDY+Rh8HJxmLELKAvCpF59vLS7FWtgBODHxl8G9Jl2CnGtXpXvdpyeMxNsTE+2QuPC+a5QT7IeZAKb2Gjmxg== - -"@parcel/css-darwin-x64@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-darwin-x64/-/css-darwin-x64-1.7.3.tgz#fac0d5705606b2261562879ee153b65c7201c9c5" - integrity sha512-LuhweXKxVwrz/hjAOm9XNRMSL+p23px20nhSCASkyUP7Higaxza948W3TSQdoL3YyR+wQxQH8Yj+R/T8Tz3E3g== - -"@parcel/css-linux-arm-gnueabihf@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm-gnueabihf/-/css-linux-arm-gnueabihf-1.7.3.tgz#43fa89d88954aba565ff2bbcd010e408d6ca6782" - integrity sha512-/pd9Em18zMvt7eDZAMpNBEwF7c4VPVhAtBOZ59ClFrsXCTDNYP7mSy0cwNgtLelCRZCGAQmZNBDNQPH7vO3rew== - -"@parcel/css-linux-arm64-gnu@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm64-gnu/-/css-linux-arm64-gnu-1.7.3.tgz#8dd5d52a2cd0d2450a2b639956bf955277aa760a" - integrity sha512-5aKiEhQK40riO4iVKzRqISzgYK+7Z7i3e6JTSz+/BHuQyHEUaBe/RuJ8Z0BDQtFz0HmWQlrQCd+7hd0Xgd8vYQ== - -"@parcel/css-linux-arm64-musl@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-linux-arm64-musl/-/css-linux-arm64-musl-1.7.3.tgz#2a784ffacf398a7422c98345eed8410d8afff9bb" - integrity sha512-Wf7/aIueDED2JqBMfZvzbBAFSaPmd3TR28bD2pmP7CI/jZnm9vHVKMdOLgt9NKSSSjdGrp+VM410CsrUM7xcOw== - -"@parcel/css-linux-x64-gnu@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-linux-x64-gnu/-/css-linux-x64-gnu-1.7.3.tgz#102751d452642d078bd40a6c5c5b19d65da810ba" - integrity sha512-0ZADbuFklUrHC1p2uPY4BPcN07jUTMqJzr/SSdnGN2XiXgiVZGcDCMHUj0DvC9Vwy11DDM6Rnw4QBbKHG+QGjQ== - -"@parcel/css-linux-x64-musl@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-linux-x64-musl/-/css-linux-x64-musl-1.7.3.tgz#563090ba9825d7de7352700effd9e0b3a8d74cbb" - integrity sha512-mFWWM8lX2OIID81YQuDDt9zTqof0B7UcEcs0huE7Zbs60uLEEQupdf8iH0yh5EOhxPt3sRcQnGXf2QTrXdjIMA== - -"@parcel/css-win32-x64-msvc@1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css-win32-x64-msvc/-/css-win32-x64-msvc-1.7.3.tgz#d69551721b74457c92bd625d566a6d1f20b7d268" - integrity sha512-KUFEMQcoP7DG3QbsN21OxhjHkfQ1BARn7D9puX75bV5N1F1kv557aaLkQZiMsgiYOL4tmJvsdQXutG7x++3j4Q== - -"@parcel/css@^1.7.2": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@parcel/css/-/css-1.7.3.tgz#e8ac640888c0317fe15b329df169ce60b2fe92cb" - integrity sha512-rgdRX4Uk31EvzH/mUScL0wdXtkci3U5N1W2pgam+9S10vQy4uONhWBepZ1tUCjONHLacGXr1jp3LbG/HI7LiTw== - dependencies: - detect-libc "^1.0.3" - optionalDependencies: - "@parcel/css-darwin-arm64" "1.7.3" - "@parcel/css-darwin-x64" "1.7.3" - "@parcel/css-linux-arm-gnueabihf" "1.7.3" - "@parcel/css-linux-arm64-gnu" "1.7.3" - "@parcel/css-linux-arm64-musl" "1.7.3" - "@parcel/css-linux-x64-gnu" "1.7.3" - "@parcel/css-linux-x64-musl" "1.7.3" - "@parcel/css-win32-x64-msvc" "1.7.3" - -"@parcel/diagnostic@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.4.0.tgz#0a94287851aee60e30f1e7f10582be274e0d1cf4" - integrity sha512-TjWO/b2zMFhub5ouwGjazMm7iAUvdmXBfWmjrg4TBhUbhoQwBnyWfvMDtAYo7PcvXfxVPgPZv86Nv6Ym5H6cHQ== - dependencies: - json-source-map "^0.6.1" - nullthrows "^1.1.1" - -"@parcel/events@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.4.0.tgz#7526ea17dd72d97d7f4d3717285e85a36f5ead8f" - integrity sha512-DEaEtFbhOhNAEmiXJ3MyF8Scq+sNDKiTyLax4lAC5/dpE5GvwfNnoD17C2+0gDuuDpdQkdHfXfvr50aYFt7jcw== - -"@parcel/fs-search@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/fs-search/-/fs-search-2.4.0.tgz#1f278eb56ee054521ccb7e685886cd386e5efba7" - integrity sha512-W/Vu6wbZk4wuB6AVdMkyymwh/S8Peed/PgJgSsApYD6lSTD315I6OuEdxZh3lWY+dqQdog/NJ7dvi/hdpH/Iqw== - dependencies: - detect-libc "^1.0.3" - -"@parcel/fs@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.4.0.tgz#d8a34e63356ce66e3e34a958fae052d48acd2d28" - integrity sha512-CnUlWGUJ52SJVQi8QnaAPPQZOADmHMV9D9aX9GLcDm5XLT3Em7vmesG4bNLdMLwzYuzAtenhcWmuRCACuYztHw== - dependencies: - "@parcel/fs-search" "2.4.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - "@parcel/watcher" "^2.0.0" - "@parcel/workers" "2.4.0" - -"@parcel/graph@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-2.4.0.tgz#a0a94baf102f456ba7c9e4e235fb677bbe0f9286" - integrity sha512-5TZIAfDITkJCzgH4j4OQhnIvjV9IFwWqNBJanRl5QQTmKvdcODS3WbnK1SOJ+ZltcLVXMB+HNXmL0bX0tVolcw== - dependencies: - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/hash@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/hash/-/hash-2.4.0.tgz#4f85e42d94aa3c458a7d0b484852f5466799be1b" - integrity sha512-nB+wYNUhe6+G8M7vQhdeFXtpYJYwJgBHOPZ7Hd9O2jdlamWjDbw0t/u1dJbYvGJ8ZDtLDwiItawQVpuVdskQ9g== - dependencies: - detect-libc "^1.0.3" - xxhash-wasm "^0.4.2" - -"@parcel/logger@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.4.0.tgz#762b9431183557132c91419f2f7e5443837a4222" - integrity sha512-DqfU0Zcs/0a7VBk+MsjJ80C66w4kM9EbkO3G12NIyEjNeG50ayW2CE9rUuJ91JaM9j0NFM1P82eyLpQPFFaVPw== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/events" "2.4.0" - -"@parcel/markdown-ansi@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.4.0.tgz#688fa5e5f4765bde83f49fe298d8a2b416b3446f" - integrity sha512-gPUP1xikxHiu2kFyPy35pfuVkFgAmcywO8YDQj7iYcB+k7l4QPpIYFYGXn2QADV4faf66ncMeTD4uYV8c0GqjQ== - dependencies: - chalk "^4.1.0" - -"@parcel/namer-default@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.4.0.tgz#df1571f4f9104ae9bdb77887693b4b7f9d5c86a6" - integrity sha512-DfL+Gx0Tyoa0vsgRpNybXjuKbWNw8MTVpy7Dk7r0btfVsn1jy3SSwlxH4USf76gb00/pK6XBsMp9zn7Z8ePREQ== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/node-resolver-core@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.4.0.tgz#394f186fc7d431f98ac72b9d3fe140e04a21dd7d" - integrity sha512-qiN97XcfW2fYNoYuVEhNKuVPEJKj5ONQl0fqr/NEMmYvWz3bVKjgiXNJwW558elZvCI08gEbdxgyThpuFFQeKQ== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/optimizer-css@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.4.0.tgz#50403ee54c0c165d279f7ec11aabdf3e1ed3d94d" - integrity sha512-LQmjjOGsHEHKTJqfHR2eJyhWhLXvHP0uOAU+qopBttYYlB2J/vMK9RYAye5cyAb8bQmV8wAdi2mq9rnt7FMSPw== - dependencies: - "@parcel/css" "^1.7.2" - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - browserslist "^4.6.6" - nullthrows "^1.1.1" - -"@parcel/optimizer-htmlnano@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.4.0.tgz#620b3e7089de97c9dc619952b22d45b461b12747" - integrity sha512-02EbeElLgNOAYhGU7fFBahpoKrX5G/yzahpaoKB/ypScM4roSsAMBkGcluboR5L10YRsvfvJEpxvfGyDA3tPmw== - dependencies: - "@parcel/plugin" "2.4.0" - htmlnano "^2.0.0" - nullthrows "^1.1.1" - posthtml "^0.16.5" - svgo "^2.4.0" - -"@parcel/optimizer-image@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.4.0.tgz#2717210bd2e0a9c58af08394011cdd2f3c1172ce" - integrity sha512-Q4onaBMPkDyYxPzrb8ytBUftaQZFepj9dSUgq+ETuHDzkgia0tomDPfCqrw6ld0qvYyANzXTP5+LC4g0i5yh+A== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - "@parcel/workers" "2.4.0" - detect-libc "^1.0.3" - -"@parcel/optimizer-svgo@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.4.0.tgz#7abfaaa3e6ba3ade4d85a85705a8b307487d2feb" - integrity sha512-mwvGuCqVuNCAuMlp2maFE/Uz9ud1T1AuX0f6cCRczjFYiwZuIr/0iDdfFzSziOkVo1MRAGAZNa0dRR/UzCZtVg== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - svgo "^2.4.0" - -"@parcel/optimizer-terser@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.4.0.tgz#433965117d54c4cf113af3a6b056ff0766367468" - integrity sha512-PdCgRgXNSY6R1HTV9VG2MHp1CgUbP5pslCyxvlbUmQAS6bvEpMOpn3qSd+U28o7mGE/qXIhvpDyi808sb+MEcg== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - terser "^5.2.0" - -"@parcel/package-manager@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.4.0.tgz#ab4d7a53059355dc4f17e16c97540b6c7d70c5f6" - integrity sha512-21AEfAQnZbHRVViTn7QsPGe/CiGaFaDUH5f0m8qVC7fDjjhC8LM8blkqU72goaO9FbaLMadtEf2txhzly7h/bg== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/fs" "2.4.0" - "@parcel/logger" "2.4.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - "@parcel/workers" "2.4.0" - semver "^5.7.1" - -"@parcel/packager-css@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.4.0.tgz#02198953674172c20a1c00418fdc7a1671f62fad" - integrity sha512-LmPDWzkXi60Oy3WrPF0jPKQxeTwW5hmNBgrcXJMHSu+VcXdaQZNzNxVzhnZkJUbDd2z9vAUrUGzdLh8TquC8iQ== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/packager-html@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.4.0.tgz#a14a4e8de19dc16c5e2c611c7d0cce7b9af9aef1" - integrity sha512-OPMIQ1uHYQFpRPrsmm5BqONbAyzjlhVsPRAzHlcBrglG4BTUeOR2ow4MUKblHmVVqc3QHnfZG4nHHtFkeuNQ3A== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - posthtml "^0.16.5" - -"@parcel/packager-js@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.4.0.tgz#033e80a161c14793ac47b74da84a2c8a22b23b76" - integrity sha512-cfslIH43CJFgBS9PmdFaSnbInMCoejsFCnxtJa2GeUpjCXSfelPRp0OPx7m8n+fap4czftPhoxBALeDUElOZGQ== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - globals "^13.2.0" - nullthrows "^1.1.1" - -"@parcel/packager-raw@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.4.0.tgz#bdb2576f154e897947bf6331d7711d4a6258d1e8" - integrity sha512-SFfw7chMFITj3J26ZVDJxbO6xwtPFcFBm1js8cwWMgzwuwS6CEc43k5+Abj+2/EqHU9kNJU9eWV5vT6lQwf3HA== - dependencies: - "@parcel/plugin" "2.4.0" - -"@parcel/packager-svg@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.4.0.tgz#573653b582472aaad77beb9a9cb1421d744bd3b5" - integrity sha512-DwkgrdLEQop+tu9Ocr1ZaadmpsbSgVruJPr80xq1LaB0Jiwrl9HjHStMNH1laNFueK1yydxhnj9C2JQfW28qag== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - posthtml "^0.16.4" - -"@parcel/plugin@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.4.0.tgz#fc940f4eb58dc271e53aa0043b68cc565853e390" - integrity sha512-ehFUAL2+h27Lv+cYbbXA74UGy8C+eglUjcpvASOOjVRFuD6poMAMliKkKAXBhQaFx/Rvhz27A2PIPv9lL2i4UQ== - dependencies: - "@parcel/types" "2.4.0" - -"@parcel/reporter-cli@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.4.0.tgz#37ba4e12b1999c04beeaf98cb6809cd4794acae4" - integrity sha512-Q9bIFMaGvQgypCDxdMEKOwrJzIHAXScKkuFsqTHnUL6mmH3Mo2CoEGAq/wpMXuPhXRn1dPJcHgTNDwZ2fSzz0A== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - chalk "^4.1.0" - term-size "^2.2.1" - -"@parcel/reporter-dev-server@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.4.0.tgz#233e595680aa86ae599715589865b282abdf63bd" - integrity sha512-24h++wevs7XYuX4dKa4PUfLSstvn3g7udajFv6CeQoME+dR25RL/wH/2LUbhV5ilgXXab76rWIndSqp78xHxPA== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - -"@parcel/resolver-default@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.4.0.tgz#1f45b95443078f10d178b32782da95a1bea71eb6" - integrity sha512-K7pIIFmGm1hjg/7Mzkg99i8tfCClKfBUTuc2R5j8cdr2n0mCAi4/f2mFf5svLrb5XZrnDgoQ05tHKklLEfUDUw== - dependencies: - "@parcel/node-resolver-core" "2.4.0" - "@parcel/plugin" "2.4.0" - -"@parcel/runtime-browser-hmr@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.4.0.tgz#369b688ce3ed95109c0fcff9157b678c5ee033db" - integrity sha512-swPFtvxGoCA9LEjU/pHPNjxG1l0fte8447zXwRN/AaYrtjNu9Ww117OSKCyvCnE143E79jZOFStodTQGFuH+9A== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - -"@parcel/runtime-js@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.4.0.tgz#bcf6d540247bda286156a876efdbf9103ce03978" - integrity sha512-67OOvmkDdtmgzZVP/EyAzoXhJ/Ug3LUVUt7idg9arun5rdJptqEb3Um3wmH0zjcNa9jMbJt7Kl5x1wA8dJgPYg== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/runtime-react-refresh@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.4.0.tgz#fe305175228c8d19d8fba9bed5542fad549723ce" - integrity sha512-flnr+bf06lMZPbXZZLLaFNrPHvYpfuXTVovEghyUW46qLVpaHj33dpsU/LqZplIuHgBp2ibgrKhr/hY9ell68w== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - react-refresh "^0.9.0" - -"@parcel/runtime-service-worker@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.4.0.tgz#07375693acbd9a5940e6230409099e0861362cc3" - integrity sha512-RgM5QUqW22WzstW03CtV+Oih8VGVuwsf94Cc4hLouU2EAD0NUJgATWbFocZVTZIBTKELAWh2gjpSQDdnL4Ur+A== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/source-map@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@parcel/source-map/-/source-map-2.0.0.tgz#41cf004109bbf277ceaf096a58838ff6a59af774" - integrity sha512-njoUJpj2646NebfHp5zKJeYD1KwhsfQIoU9TnCTHmF9fGOaPbClmeq12G6/4ZqGASftRq+YhhukFBi/ncWKGvw== - dependencies: - detect-libc "^1.0.3" - globby "^11.0.3" - -"@parcel/transformer-babel@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.4.0.tgz#05d1661293debbccacd11218c3729b7e5a2dfe1c" - integrity sha512-iWDa7KzJTMP3HNmrYxiYq/S6redk2qminx/9MwmKIN9jzm8mgts2Lj9lOg/t66YaDGky6JAvw4DhB2qW4ni6yQ== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - browserslist "^4.6.6" - json5 "^2.2.0" - nullthrows "^1.1.1" - semver "^5.7.0" - -"@parcel/transformer-css@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.4.0.tgz#7b7e35ccbe343ff0c768a330712d1756ca7f7b4c" - integrity sha512-D2u48LuiQsQvbknABE0wVKFp9r6yCgWrHKEP1J6EJ31c49nXGXDHrpHJJwqq9BvAs/124eBI5mSsehTJyFEMwg== - dependencies: - "@parcel/css" "^1.7.2" - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - browserslist "^4.6.6" - nullthrows "^1.1.1" - -"@parcel/transformer-html@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.4.0.tgz#9b342f4041d319d9759607be0aec4c1ff4ad7739" - integrity sha512-2/8X/o5QaCNVPr4wkxLCUub7v/YVvVN2L5yCEcTatNeFhNg/2iz7P2ekfqOaoDCHWZEOBT1VTwPbdBt+TMM71Q== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/plugin" "2.4.0" - nullthrows "^1.1.1" - posthtml "^0.16.5" - posthtml-parser "^0.10.1" - posthtml-render "^3.0.0" - semver "^5.7.1" - -"@parcel/transformer-image@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.4.0.tgz#3ce2603343aaf045f9dba6cd1b98ee5df060450d" - integrity sha512-JZkQvGGoGiD0AVKLIbAYYUWxepMmUaWZ4XXx71MmS/kA7cUDwTZ0CXq63YnSY1m+DX+ClTuTN8mBlwe2dkcGbA== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/workers" "2.4.0" - nullthrows "^1.1.1" - -"@parcel/transformer-js@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.4.0.tgz#e07310ef85a4c14e3d285c3c85e73c581043dfaf" - integrity sha512-eeLHFwv3jT3GmIxpLC7B8EXExGK0MFaK91HXljOMh6l8a+GlQYw27MSFQVtoXr0Olx9Uq2uvjXP1+zSsq3LQUQ== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/utils" "2.4.0" - "@parcel/workers" "2.4.0" - "@swc/helpers" "^0.3.6" - browserslist "^4.6.6" - detect-libc "^1.0.3" - nullthrows "^1.1.1" - regenerator-runtime "^0.13.7" - semver "^5.7.1" - -"@parcel/transformer-json@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.4.0.tgz#575497fb64029071cadcf1309884c0ec4e5def3c" - integrity sha512-3nR+d39mbURoXIypDfVCaxpwL65qMV+h8SLD78up2uhaRGklHQfN7GuemR7L+mcVAgNrmwVvZHhyNjdgYwWqqg== - dependencies: - "@parcel/plugin" "2.4.0" - json5 "^2.2.0" - -"@parcel/transformer-postcss@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.4.0.tgz#8b968be108e2e0280c3ae025df29b14e83ec8c50" - integrity sha512-ijIa2x+dbKnJhr7zO5WlXkvuj832fDoGksMBk2DX3u2WMrbh2rqVWPpGFsDhESx7EAy38nUoV/5KUdrNqUmCEA== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - clone "^2.1.1" - nullthrows "^1.1.1" - postcss-value-parser "^4.2.0" - semver "^5.7.1" - -"@parcel/transformer-posthtml@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.4.0.tgz#89d9f2e96a69d52fe56ca9710e9a177a61097f43" - integrity sha512-xoL3AzgtVeRRAo6bh0AHAYm9bt1jZ+HiH86/7oARj/uJs6Wd8kXK/DZf6fH+F87hj4e7bnjmDDc0GPVK0lPz1w== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - nullthrows "^1.1.1" - posthtml "^0.16.5" - posthtml-parser "^0.10.1" - posthtml-render "^3.0.0" - semver "^5.7.1" - -"@parcel/transformer-raw@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.4.0.tgz#ee65c296073ce3876b1380be20721105e3a6d9c7" - integrity sha512-fciFbNrzj0kLlDgr6OsI0PUv414rVygDWAsgbCCq4BexDkuemMs9f9FjMctx9B2VZlctE8dTT4RGkuQumTIpUg== - dependencies: - "@parcel/plugin" "2.4.0" - -"@parcel/transformer-react-refresh-wrap@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.4.0.tgz#23d1a39acddbbe71802c962f88ef376548c94cf8" - integrity sha512-9+f6sGOWkf0jyUQ1CuFWk+04Mq3KTOCU9kRiwCHX1YdUCv5uki6r9XUSpqiYodrV+L6w9CCwLvGMLCDHxtCxMg== - dependencies: - "@parcel/plugin" "2.4.0" - "@parcel/utils" "2.4.0" - react-refresh "^0.9.0" - -"@parcel/transformer-svg@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.4.0.tgz#07f7a19e7da1ec115b2fb49e364c18cc0f445ddf" - integrity sha512-D+yzVtSxtQML3d26fd/g4E/xYW68+OMbMUVLXORtoYMU42fnXQkJP6jGOdqy8Td+WORNY7EwVtQnESLwhBmolw== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/plugin" "2.4.0" - nullthrows "^1.1.1" - posthtml "^0.16.5" - posthtml-parser "^0.10.1" - posthtml-render "^3.0.0" - semver "^5.7.1" - -"@parcel/types@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.4.0.tgz#e900ba9d9c14cc8bc783724e7c66f04b87965b28" - integrity sha512-nysGIbBEnp+7R+tKTysdcTFOZDTCodsiXFeAhYQa5bhiOnG1l9gzhxQnE2OsdsgvMm40IOsgKprqvM/DbdLfnQ== - dependencies: - "@parcel/cache" "2.4.0" - "@parcel/diagnostic" "2.4.0" - "@parcel/fs" "2.4.0" - "@parcel/package-manager" "2.4.0" - "@parcel/source-map" "^2.0.0" - "@parcel/workers" "2.4.0" - utility-types "^3.10.0" - -"@parcel/utils@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.4.0.tgz#47f979e10f82caac160a6526db2f31c5713ca0d7" - integrity sha512-sdNo+mZqDZT8LJYB6WWRKa4wFVZcK6Zb5Jh6Du76QvXXwHbPIQNZgJBb6gd/Rbk4GLOp2tW7MnBfq6zP9E9E2g== - dependencies: - "@parcel/codeframe" "2.4.0" - "@parcel/diagnostic" "2.4.0" - "@parcel/hash" "2.4.0" - "@parcel/logger" "2.4.0" - "@parcel/markdown-ansi" "2.4.0" - "@parcel/source-map" "^2.0.0" - chalk "^4.1.0" - -"@parcel/watcher@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.2.tgz#46bef14584497147bad5247cfb41f80b24d24dfb" - integrity sha512-WGJY55/mTAGse2C9VVi2oo+p05oJ0kiSHmOjV33+ywgKgUkUh6B/qFQ5kBO/9mH686qqtV3k2zH1QNm+XX4+lw== - dependencies: - node-addon-api "^3.2.1" - node-gyp-build "^4.3.0" - -"@parcel/workers@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.4.0.tgz#393d5fe7942220b8846f626688e341c1be2bf1fa" - integrity sha512-eSFyvEoXXPgFzQfKIlpkUjpHfIbezUCRFTPKyJAKCxvU5DSXOpb1kz5vDESWQ4qTZXKnrKvxS1PPWN6bam9z0g== - dependencies: - "@parcel/diagnostic" "2.4.0" - "@parcel/logger" "2.4.0" - "@parcel/types" "2.4.0" - "@parcel/utils" "2.4.0" - chrome-trace-event "^1.0.2" - nullthrows "^1.1.1" - -"@popperjs/core@^2.4.4": - version "2.10.2" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590" - integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ== - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== - -"@swc/helpers@^0.3.6": - version "0.3.8" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.3.8.tgz#5b9ecf4ee480ca00f1ffbc2d1a5d4eed0d1afe81" - integrity sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg== - -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - -"@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== - -"@types/lodash@^4.14.175": - version "4.14.177" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578" - integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw== - -"@types/long@^4.0.1": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" - integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== - -"@types/node@11.11.6": - version "11.11.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a" - integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ== - -"@types/node@>=13.7.0": - version "16.11.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.9.tgz#879be3ad7af29f4c1a5c433421bf99fab7047185" - integrity sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A== - -"@types/node@^13.7.0": - version "13.13.52" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" - integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/prop-types@*", "@types/prop-types@^15.7.4": - version "15.7.4" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" - integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== - -"@types/react-dom@^17.0.11": - version "17.0.11" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" - integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== - dependencies: - "@types/react" "*" - -"@types/react-is@^16.7.1 || ^17.0.0": - version "17.0.3" - resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a" - integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== - dependencies: - "@types/react" "*" - -"@types/react-transition-group@^4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" - integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@^17.0.35": - version "17.0.35" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.35.tgz#217164cf830267d56cd1aec09dcf25a541eedd4c" - integrity sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/scheduler@*": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" - integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== - -"@typescript-eslint/eslint-plugin@^5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.0.tgz#bb46dd7ce7015c0928b98af1e602118e97df6c70" - integrity sha512-fwCMkDimwHVeIOKeBHiZhRUfJXU8n6xW1FL9diDxAyGAFvKcH4csy0v7twivOQdQdA0KC8TDr7GGRd3L4Lv0rQ== - dependencies: - "@typescript-eslint/scope-manager" "5.12.0" - "@typescript-eslint/type-utils" "5.12.0" - "@typescript-eslint/utils" "5.12.0" - debug "^4.3.2" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.2.0" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/parser@^5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.12.0.tgz#0ca669861813df99ce54916f66f524c625ed2434" - integrity sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog== - dependencies: - "@typescript-eslint/scope-manager" "5.12.0" - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/typescript-estree" "5.12.0" - debug "^4.3.2" - -"@typescript-eslint/scope-manager@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.12.0.tgz#59619e6e5e2b1ce6cb3948b56014d3a24da83f5e" - integrity sha512-GAMobtIJI8FGf1sLlUWNUm2IOkIjvn7laFWyRx7CLrv6nLBI7su+B7lbStqVlK5NdLvHRFiJo2HhiDF7Ki01WQ== - dependencies: - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/visitor-keys" "5.12.0" - -"@typescript-eslint/type-utils@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.12.0.tgz#aaf45765de71c6d9707c66ccff76ec2b9aa31bb6" - integrity sha512-9j9rli3zEBV+ae7rlbBOotJcI6zfc6SHFMdKI9M3Nc0sy458LJ79Os+TPWeBBL96J9/e36rdJOfCuyRSgFAA0Q== - dependencies: - "@typescript-eslint/utils" "5.12.0" - debug "^4.3.2" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.12.0.tgz#5b4030a28222ee01e851836562c07769eecda0b8" - integrity sha512-JowqbwPf93nvf8fZn5XrPGFBdIK8+yx5UEGs2QFAYFI8IWYfrzz+6zqlurGr2ctShMaJxqwsqmra3WXWjH1nRQ== - -"@typescript-eslint/typescript-estree@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.0.tgz#cabf545fd592722f0e2b4104711e63bf89525cd2" - integrity sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ== - dependencies: - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/visitor-keys" "5.12.0" - debug "^4.3.2" - globby "^11.0.4" - is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.12.0.tgz#92fd3193191621ab863add2f553a7b38b65646af" - integrity sha512-k4J2WovnMPGI4PzKgDtQdNrCnmBHpMUFy21qjX2CoPdoBcSBIMvVBr9P2YDP8jOqZOeK3ThOL6VO/sy6jtnvzw== - dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.12.0" - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/typescript-estree" "5.12.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/visitor-keys@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.0.tgz#1ac9352ed140b07ba144ebf371b743fdf537ec16" - integrity sha512-cFwTlgnMV6TgezQynx2c/4/tx9Tufbuo9LPzmWqyRC3QC4qTGkAG1C6pBr0/4I10PAI/FlYunI3vJjIcu+ZHMg== - dependencies: - "@typescript-eslint/types" "5.12.0" - eslint-visitor-keys "^3.0.0" - -abortcontroller-polyfill@^1.1.9: - version "1.7.3" - resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" - integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== - -acorn-jsx@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.5.0, acorn@^8.7.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - -ajv@^6.10.0, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-includes@^3.1.3, array-includes@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" - integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - is-string "^1.0.7" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.flatmap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" - integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.19.0" - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - -babel-plugin-macros@^2.6.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" - integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== - dependencies: - "@babel/runtime" "^7.7.2" - cosmiconfig "^6.0.0" - resolve "^1.12.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base-x@^3.0.8: - version "3.0.9" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" - integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== - dependencies: - safe-buffer "^5.0.1" - -base64-js@^1.3.0, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bech32@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" - integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== - -bip39@^3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0" - integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw== - dependencies: - "@types/node" "11.11.6" - create-hash "^1.1.0" - pbkdf2 "^3.0.9" - randombytes "^2.0.1" - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" - integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== - -bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" - integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== - dependencies: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.4" - inherits "^2.0.4" - parse-asn1 "^5.1.6" - readable-stream "^3.6.2" - safe-buffer "^5.2.1" - -browserslist@^4.6.6: - version "4.18.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" - integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== - dependencies: - caniuse-lite "^1.0.30001280" - electron-to-chromium "^1.3.896" - escalade "^3.1.1" - node-releases "^2.0.1" - picocolors "^1.0.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -caniuse-lite@^1.0.30001280: - version "1.0.30001282" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd" - integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg== - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -clsx@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" - integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^7.0.0, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -convert-source-map@^1.5.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -cosmiconfig@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" - integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^7.0.2: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-select@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" - integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== - dependencies: - boolbase "^1.0.0" - css-what "^5.0.0" - domhandler "^4.2.0" - domutils "^2.6.0" - nth-check "^2.0.0" - -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== - -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -csstype@^3.0.2, csstype@^3.0.9: - version "3.0.10" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" - integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== - -debug@^4.1.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-helpers@^5.0.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -dom-serializer@^1.0.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" - integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" - integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== - -domhandler@^4.2.0, domhandler@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" - integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w== - dependencies: - domelementtype "^2.2.0" - -domutils@^2.6.0, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -dotenv-expand@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" - integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== - -dotenv@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" - integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== - -electron-to-chromium@^1.3.896: - version "1.3.903" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.903.tgz#e2d3c3809f4ef05fdbe5cc88969dfc94b1bd15b9" - integrity sha512-+PnYAyniRRTkNq56cqYDLq9LyklZYk0hqoDy9GpcU11H5QjRmFZVDbxtgHUMK/YzdNTcn1XWP5gb+hFlSCr20g== - -elliptic@^6.5.3, elliptic@^6.5.4: - version "6.5.7" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" - integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.19.0, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" - is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-plugin-react-hooks@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" - integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== - -eslint-plugin-react@^7.28.0: - version "7.28.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" - integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== - dependencies: - array-includes "^3.1.4" - array.prototype.flatmap "^1.2.5" - doctrine "^2.1.0" - estraverse "^5.3.0" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.0.4" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.0" - object.values "^1.1.5" - prop-types "^15.7.2" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.6" - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^8.9.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.9.0.tgz#a2a8227a99599adc4342fd9b854cb8d8d6412fdb" - integrity sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q== - dependencies: - "@eslint/eslintrc" "^1.1.0" - "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== - dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.3.0" - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -events@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.1.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-root@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" - integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatted@^3.1.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== - -follow-redirects@^1.14.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-port@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" - integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.3: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^13.2.0: - version "13.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== - dependencies: - type-fest "^0.20.2" - -globals@^13.6.0, globals@^13.9.0: - version "13.12.1" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" - integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.2.tgz#2a235d34f4d8036219f7e34929b5de9e18166b8b" - integrity sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ== - dependencies: - define-properties "^1.1.3" - -globby@^11.0.3: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^11.0.4: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" - integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -htmlnano@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.0.tgz#07376faa064f7e1e832dfd91e1a9f606b0bc9b78" - integrity sha512-thKQfhcp2xgtsWNE27A2bliEeqVL5xjAgGn0wajyttvFFsvFWWah1ntV9aEX61gz0T6MBQ5xK/1lXuEumhJTcg== - dependencies: - cosmiconfig "^7.0.1" - posthtml "^0.16.5" - timsort "^0.3.0" - -htmlparser2@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5" - integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.2" - domutils "^2.8.0" - entities "^3.0.1" - -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4: - version "5.1.9" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" - integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== - -ignore@^5.1.8, ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - -is-core-module@^2.2.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-json@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff" - integrity sha1-a+Fm0USCihMdaGiRuYPfYsOUkf8= - -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - -is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-weakref@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2" - integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ== - dependencies: - call-bind "^1.0.0" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== - -js-sha3@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - -js-sha512@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4" - integrity sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/json-source-map/-/json-source-map-0.6.1.tgz#e0b1f6f4ce13a9ad57e2ae165a24d06e62c79a0f" - integrity sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json5@^2.2.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -"jsx-ast-utils@^2.4.1 || ^3.0.0": - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" - integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== - dependencies: - array-includes "^3.1.3" - object.assign "^4.1.2" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -libsodium-wrappers@^0.7.6: - version "0.7.9" - resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#4ffc2b69b8f7c7c7c5594a93a4803f80f6d0f346" - integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ== - dependencies: - libsodium "^0.7.0" - -libsodium@^0.7.0: - version "0.7.9" - resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.9.tgz#4bb7bcbf662ddd920d8795c227ae25bbbfa3821b" - integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A== - -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -lmdb@2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.2.4.tgz#6494d5a1d1db152e0be759edcfa06893e4cbdb53" - integrity sha512-gto+BB2uEob8qRiTlOq+R3uX0YNHsX9mjxj9Sbdue/LIKqu6IlZjrsjKeGyOMquc/474GEqFyX2pdytpydp0rQ== - dependencies: - msgpackr "^1.5.4" - nan "^2.14.2" - node-gyp-build "^4.2.3" - ordered-binary "^1.2.4" - weak-lru-cache "^1.2.2" - -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -msgpackr-extract@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d" - integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A== - dependencies: - node-gyp-build-optional-packages "5.0.7" - optionalDependencies: - "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2" - "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2" - "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2" - "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2" - "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2" - "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2" - -msgpackr@^1.5.4: - version "1.10.1" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.10.1.tgz#51953bb4ce4f3494f0c4af3f484f01cfbb306555" - integrity sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ== - optionalDependencies: - msgpackr-extract "^3.0.2" - -nan@^2.14.2: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== - -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -node-addon-api@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== - -node-gyp-build-optional-packages@5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" - integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w== - -node-gyp-build@^4.2.3, node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== - -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== - -nth-check@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" - integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== - dependencies: - boolbase "^1.0.0" - -nullthrows@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" - integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-inspect@^1.11.0, object-inspect@^1.9.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" - integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.entries@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" - integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.fromentries@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" - integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.hasown@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5" - integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.19.1" - -object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -ordered-binary@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.2.4.tgz#51d3a03af078a0bdba6c7bc8f4fedd1f5d45d83e" - integrity sha512-A/csN0d3n+igxBPfUrjbV5GC69LWj2pjZzAAeeHXLukQ4+fytfP4T1Lg0ju7MSPSwq7KtHkGaiwO8URZN5IpLg== - -pako@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" - integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg== - -parcel@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.4.0.tgz#9ce7ecee5e24ffe630e14d50801e26de334e89c1" - integrity sha512-dPWpu4RnxG9HqiLvaF8COEWEnT/KrigrC6PyPaQ0zEgpBfp7/jzXZFBVaZk2N+lpvrbNEYMjN9bv5UQGJJszIw== - dependencies: - "@parcel/config-default" "2.4.0" - "@parcel/core" "2.4.0" - "@parcel/diagnostic" "2.4.0" - "@parcel/events" "2.4.0" - "@parcel/fs" "2.4.0" - "@parcel/logger" "2.4.0" - "@parcel/package-manager" "2.4.0" - "@parcel/reporter-cli" "2.4.0" - "@parcel/reporter-dev-server" "2.4.0" - "@parcel/utils" "2.4.0" - chalk "^4.1.0" - commander "^7.0.0" - get-port "^4.2.0" - v8-compile-cache "^2.0.0" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -path-browserify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pbkdf2@^3.0.3, pbkdf2@^3.0.9: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -posthtml-parser@^0.10.0, posthtml-parser@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.10.1.tgz#63c41931a9339cc2c32aba14f06286d98f107abf" - integrity sha512-i7w2QEHqiGtsvNNPty0Mt/+ERch7wkgnFh3+JnBI2VgDbGlBqKW9eDVd3ENUhE1ujGFe3e3E/odf7eKhvLUyDg== - dependencies: - htmlparser2 "^7.1.1" - -posthtml-render@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-3.0.0.tgz#97be44931496f495b4f07b99e903cc70ad6a3205" - integrity sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA== - dependencies: - is-json "^2.0.1" - -posthtml@^0.16.4, posthtml@^0.16.5: - version "0.16.5" - resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.16.5.tgz#d32f5cf32436516d49e0884b2367d0a1424136f6" - integrity sha512-1qOuPsywVlvymhTFIBniDXwUDwvlDri5KUQuBqjmCc8Jj4b/HDSVWU//P6rTWke5rzrk+vj7mms2w8e1vD0nnw== - dependencies: - posthtml-parser "^0.10.0" - posthtml-render "^3.0.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -prop-types@^15.6.2, prop-types@^15.7.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - -property-expr@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910" - integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg== - -protobufjs@^6.8.8: - version "6.11.2" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" - integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" ">=13.7.0" - long "^4.0.0" - -protobufjs@~6.10.2: - version "6.10.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.3.tgz#11ed1dd02acbfcb330becf1611461d4b407f9eef" - integrity sha512-yvAslS0hNdBhlSKckI4R1l7wunVilX66uvrjzE4MimiAt7/qw1nLpMhZrn/ObuUTM/c3Xnfl01LYMdcSJe6dwg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" "^13.7.0" - long "^4.0.0" - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -react-dom@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" - -react-hook-form@^7.20.1: - version "7.20.1" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.20.1.tgz#37e7393789087e9cbe20362dc78c9743d88696af" - integrity sha512-5uzLrIb4cM1hRldwcazhNojweGXGaxhsozyFdAeUHzxLn40VNyfNN/Em3vqHPS2CounBZkwQk3P7XnGcb0dC6g== - -react-is@^16.7.0, react-is@^16.8.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -react-refresh@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" - integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== - -react-transition-group@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" - integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -react@^18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" - -readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readonly-date@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/readonly-date/-/readonly-date-1.0.0.tgz#5af785464d8c7d7c40b9d738cbde8c646f97dcd9" - integrity sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ== - -regenerator-runtime@^0.13.7: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regexp.prototype.flags@^1.3.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" - integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve@^1.12.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -resolve@^2.0.0-next.3: - version "2.0.0-next.3" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" - integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== - dependencies: - is-core-module "^2.2.0" - path-parse "^1.0.6" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -scheduler@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" - integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== - dependencies: - loose-envify "^1.1.0" - -semver@^5.7.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -string.prototype.matchall@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" - integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.3.1" - side-channel "^1.0.4" - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -stylis@^4.0.10, stylis@^4.0.3: - version "4.0.10" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240" - integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -svgo@^2.4.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - -symbol-observable@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-2.0.3.tgz#5b521d3d07a43c351055fa43b8355b62d33fd16a" - integrity sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA== - -term-size@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" - integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== - -terser@^5.2.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425" - integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toposort@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" - integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -typescript@^4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -utility-types@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" - integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== - -v8-compile-cache@^2.0.0, v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -weak-lru-cache@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19" - integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw== - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -ws@^7: - version "7.5.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== - -xstream@^11.14.0: - version "11.14.0" - resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.14.0.tgz#2c071d26b18310523b6877e86b4e54df068a9ae5" - integrity sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw== - dependencies: - globalthis "^1.0.1" - symbol-observable "^2.0.3" - -xxhash-wasm@^0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79" - integrity sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0, yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yup@^0.32.11: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== - dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" - property-expr "^2.0.4" - toposort "^2.0.2" diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index b0477e60d6..0000000000 --- a/tests/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Binary init checker - -A simple tool to ensure that all binaries init with the correct format, using the `assert.sh` library - -Simply run `./build_and_run.sh $WORKING_BRANCH` -For example: - -`./build_and_run.sh release/v1.1.11` - -Currently, this is run on linux based machines as the nym-core binaries are published via a linux build agent - - -This will run through all the binaries and check the fields that we expect to be initialised when passing the parameters into nyms core binaries diff --git a/tests/assert.sh b/tests/assert.sh deleted file mode 100755 index ffd2b9551b..0000000000 --- a/tests/assert.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/bin/bash -# assert.sh 1.1 - bash unit testing framework -# Copyright (C) 2009-2015 Robert Lehmann -# -# http://github.com/lehmannro/assert.sh -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . - -export DISCOVERONLY=${DISCOVERONLY:-} -export DEBUG=${DEBUG:-} -export STOP=${STOP:-} -export INVARIANT=${INVARIANT:-} -export CONTINUE=${CONTINUE:-} - -args="$(getopt -n "$0" -l \ - verbose,help,stop,discover,invariant,continue vhxdic $*)" \ -|| exit -1 -for arg in $args; do - case "$arg" in - -h) - echo "$0 [-vxidc]" \ - "[--verbose] [--stop] [--invariant] [--discover] [--continue]" - echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]" - exit 0;; - --help) - cat < [stdin] - (( tests_ran++ )) || : - [[ -z "$DISCOVERONLY" ]] || return - expected=$(echo -ne "${2:-}") - result="$(eval 2>/dev/null $1 <<< ${3:-})" || true - if [[ "$result" == "$expected" ]]; then - [[ -z "$DEBUG" ]] || echo -n . - return - fi - result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")" - [[ -z "$result" ]] && result="nothing" || result="\"$result\"" - [[ -z "$2" ]] && expected="nothing" || expected="\"$2\"" - _assert_fail "expected $expected${_indent}got $result" "$1" "$3" -} - -assert_raises() { - # assert_raises [stdin] - (( tests_ran++ )) || : - [[ -z "$DISCOVERONLY" ]] || return - status=0 - (eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$? - expected=${2:-0} - if [[ "$status" -eq "$expected" ]]; then - [[ -z "$DEBUG" ]] || echo -n . - return - fi - _assert_fail "program terminated with code $status instead of $expected" "$1" "$3" -} - -_assert_fail() { - # _assert_fail - [[ -n "$DEBUG" ]] && echo -n X - report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1" - if [[ -n "$STOP" ]]; then - [[ -n "$DEBUG" ]] && echo - echo "$report" - exit 1 - fi - tests_errors[$tests_failed]="$report" - (( tests_failed++ )) || : -} - -skip_if() { - # skip_if - (eval $@) > /dev/null 2>&1 && status=0 || status=$? - [[ "$status" -eq 0 ]] || return - skip -} - -skip() { - # skip (no arguments) - shopt -q extdebug && tests_extdebug=0 || tests_extdebug=1 - shopt -q -o errexit && tests_errexit=0 || tests_errexit=1 - # enable extdebug so returning 1 in a DEBUG trap handler skips next command - shopt -s extdebug - # disable errexit (set -e) so we can safely return 1 without causing exit - set +o errexit - tests_trapped=0 - trap _skip DEBUG -} -_skip() { - if [[ $tests_trapped -eq 0 ]]; then - # DEBUG trap for command we want to skip. Do not remove the handler - # yet because *after* the command we need to reset extdebug/errexit (in - # another DEBUG trap.) - tests_trapped=1 - [[ -z "$DEBUG" ]] || echo -n s - return 1 - else - trap - DEBUG - [[ $tests_extdebug -eq 0 ]] || shopt -u extdebug - [[ $tests_errexit -eq 1 ]] || set -o errexit - return 0 - fi -} - - -_assert_reset -: ${tests_suite_status:=0} # remember if any of the tests failed so far -_assert_cleanup() { - local status=$? - # modify exit code if it's not already non-zero - [[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status -} -trap _assert_cleanup EXIT diff --git a/tests/build_and_run.sh b/tests/build_and_run.sh deleted file mode 100755 index 6c492b4ea9..0000000000 --- a/tests/build_and_run.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -PWD="../" -GIT_BRANCH=$1 - -# run the script from the correct location - -if [[ $(pwd) != */tests ]]; then - echo "Please run the script from the 'tests' directory." - exit 1 -fi - -# lets make sure the branch is up to date -# --------------------------------------- -git checkout develop -git fetch origin -git checkout $GIT_BRANCH -git pull origin $GIT_BRANCH -# --------------------------------------- - -echo "working directory ${PWD}" - -#build all binaries... -#expect the cargo tool chain to be installed on the machine -cargo build --release --all - -#here there should be the applicable binaries to test inits -echo "running mixnode binary check" -./nym-mixnode-binary-check.sh - -sleep 2 - -echo "running gateway binary check" -./nym-gateway-binary-check.sh - -sleep 2 - -echo "running socks-5 binary check" -./nym-socks-5-binary-check.sh - -sleep 2 - -echo "running network-requester binary check" -./nym-network-requester-binary-check.sh - -sleep 2 - -echo "running client binary check" -./nym-client-binary-check.sh - - diff --git a/tests/nym-client-binary-check.sh b/tests/nym-client-binary-check.sh deleted file mode 100755 index ee23bccb64..0000000000 --- a/tests/nym-client-binary-check.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash - -set -e - -. assert.sh -v -x - -PWD="../" -RELEASE_DIRECTORY="target/release" -OUTPUT=$(for i in {1..8}; do echo -n $(($RANDOM % 10)); done) -ID="test-${OUTPUT}" -BINARY_NAME="nym-client" - -# install the current release binary -# so this is dependant on running on a linux machine for the time being - -curl -L "https://builds.ci.nymte.ch/master/${BINARY_NAME}" -o $BINARY_NAME -chmod u+x $BINARY_NAME - -#---------------------------------------------------------------------------------------------------------- -# functions -#---------------------------------------------------------------------------------------------------------- - -check_nym_client_binary_build() if [ -f $BINARY_NAME ]; then - echo "running init tests" - ./${BINARY_NAME} init --id ${ID} --output-json >/dev/null 2>&1 - - # currently this outputs to a file name name - # we currently store the output in a file in the same directory - - if [ -f client_init_results.json ]; then - OUTPUT=$(cat client_init_results.json) - - # get jq values for things we can assert against - # until the service provider is provided in the output we can validate the id is correct on init - VALUE=$(echo ${OUTPUT} | jq .id) - VALUE=${VALUE#\"} - VALUE=${VALUE%\"} - - # do asserts here based upon the output on init - - assert "echo ${VALUE}" $(echo ${ID}) - assert_end nym-client-tests - else - echo "exting test no binary found" - fi -else - echo "exting test no binary found" -fi - -#---------------------------------------------------------------------------------------------------------- -# tests -#---------------------------------------------------------------------------------------------------------- -# we run the release version first - -check_nym_client_binary_build - -first_init=$(cat ${HOME}/.nym/clients/${ID}/config/config.toml | grep -v "^version =") - -#---------------------------------------------------------------------------------------------------------- -# lets remove the binary then navigate to the target/release directory for checking the latest version -# expect to have successful output and configuration -#---------------------------------------------------------------------------------------------------------- - -if [ -f $BINARY_NAME ]; then - echo "removing client binary" - rm -rf $BINARY_NAME -else - echo "no binary found exiting" - exit 1 -fi - -#---------------------------------------------------------------------------------------------------------- -# we should expect it to pass because no errors should be presented when performing the upgrade of an init -# this should be caught at testing stage - navigate to latest binary build -#---------------------------------------------------------------------------------------------------------- - -cd ${PWD}${RELEASE_DIRECTORY} - -# re-run against the current binary built locally - -echo "diff the config files after each init" -echo "-------------------------------------" - -check_nym_client_binary_build - -second_init=$(cat ${HOME}/.nym/clients/${ID}/config/config.toml | grep -v "^version =") - -diff -w <(echo "$first_init") <(echo "$second_init") - -# check the status of the diff -if [ $? -eq 0 ]; then - echo "no differences in config files, exiting script" - exit 0 -else - echo "there are differences in the config files, it may require a fresh init on the binary" - exit 1 -fi - -# we should expect it to pass because no errors should be presented when performing the upgrade of an init diff --git a/tests/nym-gateway-binary-check.sh b/tests/nym-gateway-binary-check.sh deleted file mode 100755 index 9df6fd80f6..0000000000 --- a/tests/nym-gateway-binary-check.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/bash - -set -e - -. assert.sh -v -x - -PWD="../" -RELEASE_DIRECTORY="target/release" -WALLET_ADDRESS_CONST=n1435n84se65tn7yv536am0sfvng4yyrwj7thhxr -MOCK_HOST="1.2.3.4" -RANDOM_ID=$(for i in {1..8}; do echo -n $(($RANDOM % 10)); done) -ID="test-${RANDOM_ID}" -BINARY_NAME="nym-gateway" - -# install the current release binary -# so this is dependant on running on a linux machine for the time being - -curl -L "https://builds.ci.nymte.ch/master/${BINARY_NAME}" -o $BINARY_NAME -chmod u+x $BINARY_NAME - -#-------------------------------------- -# functions -#-------------------------------------- - -check_gateway_binary_build() if [ -f "$BINARY_NAME" ]; then - echo "running init tests" - # we wont use config env files for now - # unless we want to use a specific environment - OUTPUT=$(./${BINARY_NAME} --output json init --id ${ID} --host ${MOCK_HOST} --wallet-address ${WALLET_ADDRESS_CONST}) >/dev/null 2>&1 - - # get jq values for things we can assert against - VALUE=$(echo ${OUTPUT} | jq .data_store) - VALUE=${VALUE#\"} - VALUE=${VALUE%\"} - - #------------------------------------------------------ - DATA_STORE="${HOME}/.nym/gateways/${ID}/data/db.sqlite" - #------------------------------------------------------ - - # do asserts here based upon the output - # check the data store path - - assert "echo ${VALUE}" $(echo ${DATA_STORE}) - assert_end nym-gateway-tests -else - echo "exting test no binary found" -fi - -#---------------------------------------------------------------------------------------------------------- -# tests -#---------------------------------------------------------------------------------------------------------- - -# we run the release version first -check_gateway_binary_build -# whoami -# this is dependant on where it runs on ci potentially, will need to tweak this in the future -first_init=$(cat ${HOME}/.nym/gateways/${ID}/config/config.toml | grep -v "^\[gateway\]$" | grep -v "^version =" | grep -v "^cosmos_mnemonic =") - -#lets remove the binary then navigate to the target/release directory for checking the latest version -if [ -f "$BINARY_NAME" ]; then - echo "removing nym-gateway" - rm -rf "$BINARY_NAME" - echo "successfully removed nym-gateway" -else - echo "no binary found exiting" - exit 1 -fi - -#---------------------------------------------------------------------------------------------------------- -# we should expect it to pass because no errors should be presented when performing the upgrade of an init -# this should be caught at testing stage - navigate to latest binary build -#---------------------------------------------------------------------------------------------------------- - -cd ${PWD}${RELEASE_DIRECTORY} - -#re run against the current binary built locally - -check_gateway_binary_build - -echo "diff the config files after each init" -echo "-------------------------------------" - -second_init=$(cat ${HOME}/.nym/gateways/${ID}/config/config.toml | grep -v "^\[gateway\]$" | grep -v "^version =" | grep -v "^cosmos_mnemonic =") - -diff -w <(echo "$first_init") <(echo "$second_init") - -# check the status of the diff -if [ $? -eq 0 ]; then - echo "no differences in config files, exiting script" - exit 0 -else - echo "there are differences in the config files, it may require a fresh init on the binary" - exit 1 -fi diff --git a/tests/nym-mixnode-binary-check.sh b/tests/nym-mixnode-binary-check.sh deleted file mode 100755 index cb47ed9b4b..0000000000 --- a/tests/nym-mixnode-binary-check.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash - -. assert.sh -v -x - -PWD="../" -RELEASE_DIRECTORY="target/release" -WALLET_ADDRESS_CONST=n1435n84se65tn7yv536am0sfvng4yyrwj7thhxr -MOCK_HOST="1.2.3.4" -RANDOM_ID=$(for i in {1..8}; do echo -n $(($RANDOM % 10)); done) -ID="test-${RANDOM_ID}" -BINARY_NAME="nym-mixnode" - -# install the current release binary -# so this is dependant on running on a linux machine for the time being - -curl -L "https://builds.ci.nymte.ch/master/${BINARY_NAME}" -o $BINARY_NAME -chmod u+x $BINARY_NAME - -#---------------------------------------------------------------------------------------------------------- -# functions -#---------------------------------------------------------------------------------------------------------- - -check_mixnode_binary_build() { - if [ -f "$BINARY_NAME" ]; then - echo "running init tests" - # we wont use config env files for now - OUTPUT=$(./${BINARY_NAME} --output json init --id ${ID} --host ${MOCK_HOST} --wallet-address ${WALLET_ADDRESS_CONST}) >/dev/null - # get jq values for things we can assert against - # tidy this bit up - okay for first push - - VALUE="$(echo ${OUTPUT} | jq .wallet_address | tr -d '"')" - - # do asserts here based upon the output on init - assert "echo ${VALUE}" $(echo ${WALLET_ADDRESS_CONST}) - assert_end nym-mixnode-tests - else - echo "exiting test no binary found" - fi -} - -#---------------------------------------------------------------------------------------------------------- -# tests -#---------------------------------------------------------------------------------------------------------- - -# we run the release version first -check_mixnode_binary_build -# whoami -# this is dependant on where it runs on ci potentially, will need to tweak this in the future -first_init=$(cat ${HOME}/.nym/mixnodes/${ID}/config/config.toml | grep -v "^\[mixnode\]$" | grep -v "^version =") - -#lets remove the binary then navigate to the target/release directory for checking the latest version -if [ -f "$BINARY_NAME" ]; then - echo "removing nym-mixnode" - rm -rf "$BINARY_NAME" - echo "successfully removed nym-mixnode" -else - echo "no binary found exiting" - exit 1 -fi - -#---------------------------------------------------------------------------------------------------------- -# we should expect it to pass because no errors should be presented when performing the upgrade of an init -# this should be caught at testing stage - navigate to latest binary build -#---------------------------------------------------------------------------------------------------------- - -cd ${PWD}${RELEASE_DIRECTORY} - -#re run against the current binary built locally - -check_mixnode_binary_build - -echo "diff the config files after each init" -echo "-------------------------------------" - -second_init=$(cat ${HOME}/.nym/mixnodes/${ID}/config/config.toml | grep -v "^\[mixnode\]$" | grep -v "^version =") - -diff -w <(echo "$first_init") <(echo "$second_init") - -# check the status of the diff -if [ $? -eq 0 ]; then - echo "no differences in config files, exiting script" - exit 0 -else - echo "there are differences in the config files, it may require a fresh init on the binary" - exit 1 -fi diff --git a/tests/nym-network-requester-binary-check.sh b/tests/nym-network-requester-binary-check.sh deleted file mode 100755 index 8f299cc042..0000000000 --- a/tests/nym-network-requester-binary-check.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -set -e - -. assert.sh -v -x - -PWD="../" -RELEASE_DIRECTORY="target/release" -RANDOM_ID=$(for i in {1..8}; do echo -n $(($RANDOM % 10)); done) -ID="test-${RANDOM_ID}" -BINARY_NAME="nym-network-requester" - -echo "the version number is ${RELEASE_VERSION_NUMBER} to be installed from github" - -# we have now the bundled the client into the network requester, more a less the same output as the client - -curl -L "https://builds.ci.nymte.ch/master/${BINARY_NAME}" -o $BINARY_NAME -chmod u+x $BINARY_NAME - -#---------------------------------------------------------------------------------------------------------- -# functions -#---------------------------------------------------------------------------------------------------------- - -check_nym_network_requester_binary_build() if [ -f $BINARY_NAME ]; then - echo "running init tests" - ./${BINARY_NAME} init --id ${ID} --output-json >/dev/null 2>&1 - - # currently this outputs to a file name name - # we currently store the output in a file in the same directory - - if [ -f "client_init_results.json" ]; then - OUTPUT=$(cat client_init_results.json) - - # get jq values for things we can assert against - # until the service provider is provided in the output we can validate the id is correct on init - VALUE=$(echo ${OUTPUT} | jq .id) - VALUE=${VALUE#\"} - VALUE=${VALUE%\"} - - # do asserts here based upon the output on init - - assert "echo ${VALUE}" $(echo ${ID}) - assert_end nym-network-requester-tests - else - echo "exting test no binary found" - fi -else - echo "exting test no binary found" -fi - -#---------------------------------------------------------------------------------------------------------- -# tests -#---------------------------------------------------------------------------------------------------------- -# we run the release version first - -check_nym_network_requester_binary_build - -first_init=$(cat ${HOME}/.nym/service-providers/network-requester/${ID}/config/config.toml | grep -v "^version =") - -#---------------------------------------------------------------------------------------------------------- -# lets remove the binary then navigate to the target/release directory for checking the latest version -# expect to have successful output and configuration -#---------------------------------------------------------------------------------------------------------- - -if [ -f $BINARY_NAME ]; then - echo "removing nym-network-requester binary" - rm -rf $BINARY_NAME -else - echo "no binary found exiting" - exit 1 -fi - -#---------------------------------------------------------------------------------------------------------- -# we should expect it to pass because no errors should be presented when performing the upgrade of an init -# this should be caught at testing stage - navigate to latest binary build -#---------------------------------------------------------------------------------------------------------- - -cd ${PWD}${RELEASE_DIRECTORY} - -# re-run against the current binary built locally - -echo "diff the config files after each init" -echo "-------------------------------------" - -check_nym_network_requester_binary_build - -second_init=$(cat ${HOME}/.nym/service-providers/network-requester/${ID}/config/config.toml | grep -v "^version =") - -diff -w <(echo "$first_init") <(echo "$second_init") - -# check the status of the diff -if [ $? -eq 0 ]; then - echo "no differences in config files, exiting script" - exit 0 -else - echo "there are differences in the config files, it may require a fresh init on the binary" - exit 1 -fi - -# we should expect it to pass because no errors should be presented when performing the upgrade of an init diff --git a/tests/nym-socks-5-binary-check.sh b/tests/nym-socks-5-binary-check.sh deleted file mode 100755 index ebcfe85e14..0000000000 --- a/tests/nym-socks-5-binary-check.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -set -e - -. assert.sh -v -x - -PWD="../" -RELEASE_DIRECTORY="target/release" -MOCK_SERVICE_PROVIDER="36cUqdggtdXixZhmXfyZm3Dep3Q5QsKVPotMrVSmS4oY.ZCCAdFPwPNSTtUMYveA62ttEFe8FDiB3cdheWHtCytX@6Lnxj9vD2YMtSmfe8zp5RBtj1uZLYQAFRxY9q7ANwrZz" -RANDOM_ID=$(for i in {1..8}; do echo -n $(($RANDOM % 10)); done) -ID="test-${RANDOM_ID}" -BINARY_NAME="nym-socks5-client" - -# install the current release binary -# so this is dependant on running on a linux machine for the time being - -curl -L "https://builds.ci.nymte.ch/master/${BINARY_NAME}" -o $BINARY_NAME -chmod u+x $BINARY_NAME - -#---------------------------------------------------------------------------------------------------------- -# functions -#---------------------------------------------------------------------------------------------------------- - -check_nym_socks5_client_binary_build() if [ -f $BINARY_NAME ]; then - echo "running init tests" - ./${BINARY_NAME} init --id ${ID} --provider ${MOCK_SERVICE_PROVIDER} --output-json >/dev/null 2>&1 - - # currently this outputs to a file name name - # we currently store the output in a file in the same directory - - if [ -f "socks5_client_init_results.json" ]; then - OUTPUT=$(cat socks5_client_init_results.json) - - # get jq values for things we can assert against - # until the service provider is provided in the output we can validate the id is correct on init - VALUE=$(echo ${OUTPUT} | jq .id) - VALUE=${VALUE#\"} - VALUE=${VALUE%\"} - - # do asserts here based upon the output on init - - assert "echo ${VALUE}" $(echo ${ID}) - assert_end nym-socks-5-client-tests - else - echo "exting test no binary found" - fi -else - echo "exting test no binary found" -fi - -#---------------------------------------------------------------------------------------------------------- -# tests -#---------------------------------------------------------------------------------------------------------- -# we run the release version first - -check_nym_socks5_client_binary_build - -first_init=$(cat ${HOME}/.nym/socks5-clients/${ID}/config/config.toml | grep -v "^version =") - -#---------------------------------------------------------------------------------------------------------- -# lets remove the binary then navigate to the target/release directory for checking the latest version -# expect to have successful output and configuration -#---------------------------------------------------------------------------------------------------------- - -if [ -f $BINARY_NAME ]; then - echo "removing socks-5-client binary" - rm -rf $BINARY_NAME -else - echo "no binary found exiting" - exit 1 -fi - -#---------------------------------------------------------------------------------------------------------- -# we should expect it to pass because no errors should be presented when performing the upgrade of an init -# this should be caught at testing stage - navigate to latest binary build -#---------------------------------------------------------------------------------------------------------- - -cd ${PWD}${RELEASE_DIRECTORY} - -# re-run against the current binary built locally - -echo "diff the config files after each init" -echo "-------------------------------------" - -check_nym_socks5_client_binary_build - -second_init=$(cat ${HOME}/.nym/socks5-clients/${ID}/config/config.toml | grep -v "^version =") - -diff -w <(echo "$first_init") <(echo "$second_init") - -# check the status of the diff -if [ $? -eq 0 ]; then - echo "no differences in config files, exiting script" - exit 0 -else - echo "there are differences in the config files, it may require a fresh init on the binary" - exit 1 -fi - -# we should expect it to pass because no errors should be presented when performing the upgrade of an init diff --git a/tools/echo-server/Cargo.toml b/tools/echo-server/Cargo.toml index c6e98489ce..390f746bcc 100644 --- a/tools/echo-server/Cargo.toml +++ b/tools/echo-server/Cargo.toml @@ -9,6 +9,11 @@ edition.workspace = true license.workspace = true rust-version.workspace = true + +[[bin]] +name = "echo-server" +path = "src/echo-server.rs" + [dependencies] anyhow.workspace = true dashmap.workspace = true @@ -24,3 +29,11 @@ bytecodec = { workspace = true } nym-sdk = { path = "../../sdk/rust/nym-sdk/" } bytes.workspace = true dirs.workspace = true +clap.workspace = true +nym-bin-common = { path = "../../common/bin-common", features = [ + "basic_tracing", + "output_format", +] } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } +futures = { workspace = true } +tempfile.workspace = true diff --git a/tools/echo-server/README.md b/tools/echo-server/README.md index 71d9f263c9..70a6968f9d 100644 --- a/tools/echo-server/README.md +++ b/tools/echo-server/README.md @@ -1,9 +1,7 @@ # Nym Echo Server -This is an initial minimal implementation of an echo server built using the `NymProxyServer` Rust SDK abstraction. +This is an initial, minimal implementation of an echo server built using the `NymProxyServer` Rust SDK abstraction. -## Usage -``` -cargo build --release -../../target/release/echo-server e.g. ../../target/release/echo-server 9000 ../../envs/canary.env -``` +It currently relies on parsing out a `ProxiedMessage` from incoming messages, used by the `NymProxyClient`. In the future it will try and parse a `ReconstructedMessage` type, in order to allow standard `MixnetClient`s to receive echo messages. + +You can find the docs [here](https://nym.com/docs/developers/tools/echo-server). diff --git a/tools/echo-server/src/echo-server.rs b/tools/echo-server/src/echo-server.rs new file mode 100644 index 0000000000..09f0b3725d --- /dev/null +++ b/tools/echo-server/src/echo-server.rs @@ -0,0 +1,47 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::Result; +use clap::Parser; +use echo_server::NymEchoServer; +use nym_crypto::asymmetric::ed25519; +use tracing::info; + +#[derive(Parser, Debug)] +struct Args { + /// Optional gateway to use + #[clap(short, long)] + gateway: Option, + + /// Optional config path to specify + #[clap(short, long)] + config_path: Option, + + /// Optional env file - defaults to Mainnet if None + #[clap(short, long)] + env: Option, + + /// Listen port + #[clap(short, long, default_value = "8080")] + listen_port: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + nym_bin_common::logging::setup_tracing_logger(); + let args = Args::parse(); + let mut echo_server = NymEchoServer::new( + args.gateway, + args.config_path.as_deref(), + args.env, + args.listen_port.as_str(), + ) + .await?; + + let echo_addr = echo_server.nym_address().await; + info!("listening on {echo_addr}"); + + echo_server.run().await?; + + Ok(()) +} diff --git a/tools/echo-server/src/lib.rs b/tools/echo-server/src/lib.rs new file mode 100644 index 0000000000..4630e979dd --- /dev/null +++ b/tools/echo-server/src/lib.rs @@ -0,0 +1,383 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::Result; +use nym_crypto::asymmetric::ed25519; +use nym_sdk::{mixnet::Recipient, tcp_proxy, tcp_proxy::NymProxyServer}; +use std::{ + fmt::Debug, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, +}; +use tokio::{ + io::AsyncWriteExt, + net::{TcpListener, TcpStream}, + sync::{broadcast, Mutex}, + task, +}; +use tokio_stream::StreamExt; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info}; + +const METRICS_TICK: u8 = 6; // Tempo of metrics logging in seconds + +#[derive(Debug)] +pub struct Metrics { + total_conn: AtomicU64, + bytes_recv: AtomicU64, + bytes_sent: AtomicU64, +} + +impl Metrics { + fn new() -> Self { + Self { + total_conn: AtomicU64::new(0), + bytes_recv: AtomicU64::new(0), + bytes_sent: AtomicU64::new(0), + } + } +} + +pub struct NymEchoServer { + client: Arc>, + listen_addr: String, + metrics: Arc, + cancel_token: CancellationToken, + client_shutdown_tx: tokio::sync::mpsc::Sender<()>, // Shutdown signal for the TcpProxyServer instance + shutdown_tx: tokio::sync::mpsc::Sender<()>, // Shutdown signals for the EchoServer + shutdown_rx: tokio::sync::mpsc::Receiver<()>, + ready_tx: broadcast::Sender<()>, // Signal for upstream code if consuming the crate showing readiness +} + +impl NymEchoServer { + pub async fn new( + gateway: Option, + config_path: Option<&str>, + env: Option, + listen_port: &str, + ) -> Result { + let home_dir = dirs::home_dir().expect("Unable to get home directory"); + let default_path = format!("{}/tmp/nym-proxy-server-config", home_dir.display()); + let config_path = config_path.unwrap_or(&default_path); + let listen_addr = format!("127.0.0.1:{listen_port}"); + + let client = Arc::new(Mutex::new( + tcp_proxy::NymProxyServer::new(&listen_addr, config_path, env, gateway).await?, + )); + + let client_shutdown_tx = client.lock().await.disconnect_signal(); + + let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel(1); + + let (ready_tx, _) = broadcast::channel(1); + + Ok(NymEchoServer { + client, + listen_addr, + metrics: Arc::new(Metrics::new()), + cancel_token: CancellationToken::new(), + client_shutdown_tx, + shutdown_tx, + shutdown_rx, + ready_tx, + }) + } + + pub async fn run(&mut self) -> Result<()> { + let cancel_token = self.cancel_token.clone(); + + let mut interval = + tokio::time::interval(tokio::time::Duration::from_secs(METRICS_TICK as u64)); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let client = Arc::clone(&self.client); + task::spawn(async move { + client.lock().await.run_with_shutdown().await?; + Ok::<(), anyhow::Error>(()) + }); + + let all_metrics = Arc::clone(&self.metrics); + + let listener = TcpListener::bind(self.listen_addr.clone()).await?; + debug!("{listener:?}"); + + let mut shutdown_rx = + std::mem::replace(&mut self.shutdown_rx, tokio::sync::mpsc::channel(1).1); + + info!("Ready to accept incoming traffic"); + let _ = self.ready_tx.send(()); + + loop { + tokio::select! { + Some(()) = shutdown_rx.recv() => { + info!("Disconnect signal received"); + self.cancel_token.cancel(); + info!("Cancel token cancelled: killing handle_incoming loops"); + self.client_shutdown_tx.send(()).await?; + info!("Sent shutdown signal to ProxyServer instance"); + break; + } + stream = listener.accept() => { + let (stream, _) = stream?; + info!("Handling new stream"); + let connection_metrics = Arc::clone(&self.metrics); + connection_metrics.total_conn.fetch_add(1, Ordering::Relaxed); + + tokio::spawn(NymEchoServer::handle_incoming( + stream, connection_metrics, cancel_token.clone() + )); + } + _ = interval.tick() => { + info!("Metrics: total_connections_since_start={}, bytes_received={}, bytes_sent={}", + all_metrics.total_conn.load(Ordering::Relaxed), + all_metrics.bytes_recv.load(Ordering::Relaxed), + all_metrics.bytes_sent.load(Ordering::Relaxed), + ); + } + } + } + self.shutdown_rx = shutdown_rx; + Ok(()) + } + + async fn handle_incoming( + socket: TcpStream, + metrics: Arc, + cancel_token: CancellationToken, + ) { + let (read, mut write) = socket.into_split(); + let codec = tokio_util::codec::BytesCodec::new(); + let mut framed_read = tokio_util::codec::FramedRead::new(read, codec); + + loop { + tokio::select! { + Some(result) = framed_read.next() => { + match result { + Ok(bytes) => { + let len = bytes.len(); + metrics.bytes_recv.fetch_add(len as u64, Ordering::Relaxed); + if let Err(e) = write.write_all(&bytes).await { + error!("Failed to write to stream with err: {}", e); + break; + } + metrics.bytes_sent.fetch_add(len as u64, Ordering::Relaxed); + } + Err(e) => { + error!("Failed to read from stream with err: {}", e); + break; + } + } + } + _ = cancel_token.cancelled() => { + info!("Shutdown signal received, closing connection"); + break; + } + } + } + + info!("Connection closed"); + } + + pub fn disconnect_signal(&self) -> tokio::sync::mpsc::Sender<()> { + self.shutdown_tx.clone() + } + + pub async fn nym_address(&self) -> Recipient { + *self.client.lock().await.nym_address() + } + + pub fn listen_addr(&self) -> String { + self.listen_addr.clone() + } + + pub fn metrics(&self) -> Arc { + self.metrics.clone() + } + + pub fn ready_signal(&self) -> broadcast::Receiver<()> { + self.ready_tx.subscribe() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use nym_sdk::mixnet::{IncludedSurbs, MixnetClient, MixnetMessageSender}; + use nym_sdk::tcp_proxy::{Payload, ProxiedMessage}; + use tempfile::TempDir; + + #[tokio::test] + #[ignore] + async fn shutdown_works() -> Result<()> { + let config_dir = TempDir::new()?; + let mut echo_server = match NymEchoServer::new( + None, + Some(config_dir.path().to_str().unwrap()), + None, // Mainnet by default + "9000", + ) + .await + { + Ok(server) => server, + Err(err) => { + error!("{err}"); + // this is not an ideal way of checking it, but if test fails due to networking failures + // it should be fine to progress + if err.to_string().contains("nym api request failed") { + return Ok(()); + } + return Err(err); + } + }; + + // Getter for shutdown signal + let shutdown_tx = echo_server.disconnect_signal(); + + // Getter for ready signal + let mut ready_rx = echo_server.ready_signal(); + + // Start the echo serv + let server_handle = tokio::spawn(async move { echo_server.run().await.unwrap() }); + + // Wait until you can match on ready signal - you will see "Ready to accept incoming traffic" in echo server logs when running it as CLI + loop { + match ready_rx.try_recv() { + Ok(()) => { + println!("Server is ready!"); + break; + } + Err(broadcast::error::TryRecvError::Empty) => { + // Channel is still empty, wait a bit and try again + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + Err(broadcast::error::TryRecvError::Closed) => { + return Err(anyhow::anyhow!( + "Ready channel closed before server was ready" + )); + } + Err(broadcast::error::TryRecvError::Lagged(_)) => { + // Broadcast channel was set before we checked but handle it anyway; server is ready + break; + } + } + } + + // Kill server + shutdown_tx.send(()).await?; + + // Wait for shutdown in handle + server_handle.await?; + + Ok(()) + } + + #[tokio::test] + #[ignore] + async fn echoes_bytes() -> Result<()> { + let config_dir = TempDir::new()?; + let mut echo_server = match NymEchoServer::new( + None, + Some(config_dir.path().to_str().unwrap()), + None, + "9001", + ) + .await + { + Ok(server) => server, + Err(err) => { + error!("{err}"); + // this is not an ideal way of checking it, but if test fails due to networking failures + // it should be fine to progress + if err.to_string().contains("nym api request failed") { + return Ok(()); + } + return Err(err); + } + }; + + let echo_addr = echo_server.nym_address().await; + + let shutdown_tx = echo_server.disconnect_signal(); + let mut ready_rx = echo_server.ready_signal(); + + let server_handle = tokio::task::spawn(async move { + echo_server.run().await.unwrap(); + }); + + loop { + match ready_rx.try_recv() { + Ok(()) => { + println!("Server is ready!"); + break; + } + Err(broadcast::error::TryRecvError::Empty) => { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + Err(broadcast::error::TryRecvError::Closed) => { + return Err(anyhow::anyhow!( + "Ready channel closed before server was ready" + )); + } + Err(broadcast::error::TryRecvError::Lagged(_)) => { + break; + } + } + } + println!("Sending message"); + + let session_id = uuid::Uuid::new_v4(); + let message_id = 0; + let outgoing = ProxiedMessage::new( + Payload::Data("test".as_bytes().to_vec()), + session_id, + message_id, + ); + let coded_message = bincode::serialize(&outgoing)?; + + println!("sending {coded_message:?}"); + + let mut client = MixnetClient::connect_new().await?; + + println!("sending client addr {}", client.nym_address()); + let sender = client.split_sender(); + + let receiving_task_handle = tokio::spawn(async move { + println!("in handle"); + if let Some(received) = client.next().await { + println!("{received:?}"); + let incoming: ProxiedMessage = bincode::deserialize(&received.message).unwrap(); + assert_eq!(outgoing.message, incoming.message); + } + println!("disconnecting client"); + client.disconnect().await; + println!("client disconnected"); + }); + + println!("after recv task handle"); + + let sending_task_handle = tokio::spawn(async move { + sender + .send_message(echo_addr, &coded_message, IncludedSurbs::Amount(10)) + .await + .unwrap(); + }); + + println!("after sending task handle"); + + receiving_task_handle.await?; + sending_task_handle.await?; + + println!("after handles resolve"); + + shutdown_tx.send(()).await?; + + println!("sent shutdown"); + + server_handle.await?; + + Ok(()) + } +} diff --git a/tools/echo-server/src/main.rs b/tools/echo-server/src/main.rs deleted file mode 100644 index 28b0a7b96d..0000000000 --- a/tools/echo-server/src/main.rs +++ /dev/null @@ -1,161 +0,0 @@ -use anyhow::Result; -use bytes::Bytes; -use nym_sdk::tcp_proxy; -use std::env; -use std::fs; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use tokio::io::AsyncWriteExt; -use tokio::net::{TcpListener, TcpStream}; -use tokio::signal; -use tokio::sync::broadcast; -use tokio::task; -use tokio_stream::StreamExt; -use tracing::{error, info, warn}; - -struct Metrics { - total_conn: AtomicU64, - active_conn: AtomicU64, - bytes_recv: AtomicU64, - bytes_sent: AtomicU64, -} - -impl Metrics { - fn new() -> Self { - Self { - total_conn: AtomicU64::new(0), - active_conn: AtomicU64::new(0), - bytes_recv: AtomicU64::new(0), - bytes_sent: AtomicU64::new(0), - } - } -} - -#[tokio::main] -async fn main() -> Result<()> { - // if you run this with DEBUG you see the msg buffer on the ProxyServer, but its quite chatty - tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) - .init(); - - let server_port = env::args() - .nth(1) - .expect("Server listen port not specified"); - let tcp_addr = format!("127.0.0.1:{}", server_port); - - // This dir gets cleaned up at the end: NOTE if you switch env between tests without letting the file do the automatic cleanup, make sure to manually remove this directory up before running again, otherwise your client will attempt to use these keys for the new env - let home_dir = dirs::home_dir().expect("Unable to get home directory"); - let conf_path = format!("{}/tmp/nym-proxy-server-config", home_dir.display()); - - let env_path = env::args().nth(2).expect("Env file not specified"); - let env = env_path.to_string(); - - let mut proxy_server = tcp_proxy::NymProxyServer::new(&tcp_addr, &conf_path, Some(env.clone())) - .await - .unwrap(); - let proxy_nym_addr = *proxy_server.nym_address(); - info!("ProxyServer listening out on {}", proxy_nym_addr); - - task::spawn(async move { - proxy_server.run_with_shutdown().await?; - Ok::<(), anyhow::Error>(()) - }); - - let (shutdown_sender, _) = broadcast::channel(1); - let metrics = Arc::new(Metrics::new()); - let all_metrics = Arc::clone(&metrics); - - tokio::spawn(async move { - loop { - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - info!( - "Metrics: total_connections={}, active_connections={}, bytes_received={}, bytes_sent={}", - all_metrics.total_conn.load(Ordering::Relaxed), - all_metrics.active_conn.load(Ordering::Relaxed), - all_metrics.bytes_recv.load(Ordering::Relaxed), - all_metrics.bytes_sent.load(Ordering::Relaxed), - ); - } - }); - - let listener = TcpListener::bind(tcp_addr).await?; - - loop { - tokio::select! { - _ = signal::ctrl_c() => { - info!("Shutdown signal received, closing server..."); - let _ = shutdown_sender.send(()); - // TODO we need something like this for the ProxyServer client - break; - } - Ok((socket, _)) = listener.accept() => { - let connection_metrics = Arc::clone(&metrics); - let shutdown_rx = shutdown_sender.subscribe(); - connection_metrics.total_conn.fetch_add(1, Ordering::Relaxed); - connection_metrics.active_conn.fetch_add(1, Ordering::Relaxed); - tokio::spawn(async move { - handle_incoming(socket, connection_metrics, shutdown_rx).await; - }); - } - } - } - - signal::ctrl_c().await?; - info!("Received CTRL+C"); - fs::remove_dir_all(conf_path)?; - while metrics.active_conn.load(Ordering::Relaxed) > 0 { - info!("Waiting on active connections to close: sleeping 100ms"); - // TODO some kind of hard kill here for the ProxyServer - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - } - Ok(()) -} - -async fn handle_incoming( - socket: TcpStream, - metrics: Arc, - mut shutdown_rx: broadcast::Receiver<()>, -) { - let (read, mut write) = socket.into_split(); - let codec = tokio_util::codec::BytesCodec::new(); - let mut framed_read = tokio_util::codec::FramedRead::new(read, codec); - - loop { - tokio::select! { - Some(result) = framed_read.next() => { - match result { - Ok(bytes) => { - let len = bytes.len(); - metrics.bytes_recv.fetch_add(len as u64, Ordering::Relaxed); - if let Err(e) = write.write_all(&bytes).await { - error!("Failed to write to stream with err: {}", e); - break; - } - metrics.bytes_sent.fetch_add(len as u64, Ordering::Relaxed); - } - Err(e) => { - error!("Failed to read from stream with err: {}", e); - break; - } - } - } - _ = shutdown_rx.recv() => { - warn!("Shutdown signal received, closing connection"); - break; - } - // TODO need to work out a way that if this timesout and breaks but you dont hang up the conn on the client end you can reconnect..maybe. If we just use this as a ping echo server I dont think this is a problem - // EDIT I'm not actually sure we want this functionality? Measuring active connections might be useful though - _ = tokio::time::sleep(tokio::time::Duration::from_secs(120)) => { - info!("Timeout reached, assuming we wont get more messages on this conn, closing"); - let close_message = "Closing conn, reconnect if you want to ping again"; - let bytes: Bytes = close_message.into(); - write.write_all(&bytes).await.expect("Couldn't write to socket"); - break; - } - } - } - metrics - .active_conn - .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); - info!("Connection closed"); -} diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml index d5384a9953..bea71e6d01 100644 --- a/tools/internal/testnet-manager/Cargo.toml +++ b/tools/internal/testnet-manager/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true [dependencies] anyhow.workspace = true @@ -16,6 +17,7 @@ console = { workspace = true } cw-utils.workspace = true clap = { workspace = true, features = ["cargo", "derive"] } indicatif = { workspace = true } +humantime = { workspace = true } rand.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true @@ -45,9 +47,11 @@ nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/g nym-ecash-contract-common = { path = "../../../common/cosmwasm-smart-contracts/ecash-contract" } nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" } nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" } +nym-performance-contract-common = { path = "../../../common/cosmwasm-smart-contracts/nym-performance-contract" } nym-pemstore = { path = "../../../common/pemstore" } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/testnet-manager/build.rs index cdd97f9505..be394b3c41 100644 --- a/tools/internal/testnet-manager/build.rs +++ b/tools/internal/testnet-manager/build.rs @@ -1,19 +1,26 @@ +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let database_path = format!("{}/nym-api-example.sqlite", out_dir); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; + let database_path = format!("{out_dir}/nym-api-example.sqlite"); - let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -22,4 +29,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/tools/internal/testnet-manager/migrations/02_performance_contract.sql b/tools/internal/testnet-manager/migrations/02_performance_contract.sql new file mode 100644 index 0000000000..ad8d53b2c5 --- /dev/null +++ b/tools/internal/testnet-manager/migrations/02_performance_contract.sql @@ -0,0 +1,53 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +CREATE TABLE network_old +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +INSERT INTO network_old +SELECT * +from network; + +DROP TABLE network; + +CREATE TABLE network +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id), + vesting_contract_id INTEGER NOT NULL REFERENCES contract (id), + ecash_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id), + cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id), + dkg_contract_id INTEGER NOT NULL REFERENCES contract (id), + performance_contract_id INTEGER NOT NULL REFERENCES contract (id), + + rewarder_address TEXT NOT NULL REFERENCES account (address), + ecash_holding_account_address TEXT NOT NULL REFERENCES account (address) +); + +CREATE TABLE authorised_network_monitor +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + network_id INTEGER NOT NULL REFERENCES network (id), + address TEXT NOT NULL REFERENCES account (address) +); diff --git a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs index 574559bf82..99174e6d51 100644 --- a/tools/internal/testnet-manager/src/cli/initialise_new_network.rs +++ b/tools/internal/testnet-manager/src/cli/initialise_new_network.rs @@ -25,6 +25,10 @@ pub(crate) struct Args { #[clap(long)] custom_epoch_duration_secs: Option, + /// Specifies custom number of epochs sphinx keys are going to be valid for + #[clap(long)] + key_validity_in_epochs: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -38,6 +42,7 @@ pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { args.built_contracts, args.network_name, args.custom_epoch_duration_secs.map(Duration::from_secs), + args.key_validity_in_epochs, ) .await? .into_loaded(); diff --git a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs index d302177fad..1e5d24b1bf 100644 --- a/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs +++ b/tools/internal/testnet-manager/src/cli/initialise_post_dkg_network.rs @@ -39,6 +39,10 @@ pub(crate) struct Args { #[clap(long)] custom_epoch_duration_secs: Option, + /// Specifies custom number of epochs sphinx keys are going to be valid for + #[clap(long)] + key_validity_in_epochs: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -51,6 +55,7 @@ pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { args.built_contracts, args.network_name, args.custom_epoch_duration_secs.map(Duration::from_secs), + args.key_validity_in_epochs, ) .await? .into(); diff --git a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs index f0f9a4475a..587e5c0375 100644 --- a/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs +++ b/tools/internal/testnet-manager/src/cli/local_ecash_apis.rs @@ -37,6 +37,10 @@ pub(crate) struct Args { #[clap(long)] custom_epoch_duration_secs: Option, + /// Specifies custom number of epochs sphinx keys are going to be valid for + #[clap(long)] + key_validity_in_epochs: Option, + #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -53,6 +57,7 @@ pub(crate) async fn execute(args: Args) -> Result<(), NetworkManagerError> { args.built_contracts, args.network_name, args.custom_epoch_duration_secs.map(Duration::from_secs), + args.key_validity_in_epochs, ) .await? .into(); diff --git a/tools/internal/testnet-manager/src/error.rs b/tools/internal/testnet-manager/src/error.rs index d85d8c7dcb..42dd29b2d6 100644 --- a/tools/internal/testnet-manager/src/error.rs +++ b/tools/internal/testnet-manager/src/error.rs @@ -103,4 +103,7 @@ pub enum NetworkManagerError { #[error("timed out while waiting for the gateway to start receiving traffic (you need to actually run it!)")] GatewayWaitTimeout, + + #[error("attempted to bond nodes on a non-empty network")] + NetworkNotEmpty, } diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs index 8295550dfc..6328dc0b10 100644 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -18,6 +18,7 @@ pub(crate) struct LoadedNymContracts { pub(crate) cw3_multisig: LoadedContract, pub(crate) cw4_group: LoadedContract, pub(crate) dkg: LoadedContract, + pub(crate) performance: LoadedContract, } impl From for LoadedNymContracts { @@ -29,6 +30,7 @@ impl From for LoadedNymContracts { cw3_multisig: value.cw3_multisig.into(), cw4_group: value.cw4_group.into(), dkg: value.dkg.into(), + performance: value.performance.into(), } } } @@ -41,6 +43,7 @@ pub(crate) struct NymContracts { pub(crate) cw3_multisig: Contract, pub(crate) cw4_group: Contract, pub(crate) dkg: Contract, + pub(crate) performance: Contract, } impl NymContracts { @@ -52,6 +55,7 @@ impl NymContracts { &self.cw3_multisig, &self.cw4_group, &self.dkg, + &self.performance, ] } @@ -63,11 +67,12 @@ impl NymContracts { &mut self.cw3_multisig, &mut self.cw4_group, &mut self.dkg, + &mut self.performance, ] } pub(crate) fn count(&self) -> usize { - 6 + 7 } pub(crate) fn discover_paths>( @@ -100,6 +105,9 @@ impl NymContracts { if name.contains("dkg") { self.dkg.wasm_path = Some(entry.path()) } + if name.contains("performance") { + self.performance.wasm_path = Some(entry.path()) + } } } @@ -122,6 +130,7 @@ impl Default for NymContracts { cw4_group: Contract::new("cw4_group"), cw3_multisig: Contract::new("cw3_multisig"), dkg: Contract::new("dkg"), + performance: Contract::new("performance"), } } } diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs index ec74a01af3..9f87a468fa 100644 --- a/tools/internal/testnet-manager/src/manager/dkg_skip.rs +++ b/tools/internal/testnet-manager/src/manager/dkg_skip.rs @@ -130,6 +130,7 @@ impl NetworkManager { &self, ctx: &mut DkgSkipCtx, api_endpoints: Vec, + mut prime_api: Option, ) -> Result<(), NetworkManagerError> { ctx.println(format!( "📝 {}Generating ecash keys for all signers...", @@ -144,12 +145,23 @@ impl NetworkManager { let mut ecash_signers = Vec::new(); let mut rng = OsRng; - for (endpoint, ecash_keypair) in api_endpoints.into_iter().zip(ecash_keys.into_iter()) { + for (i, (endpoint, ecash_keypair)) in api_endpoints + .into_iter() + .zip(ecash_keys.into_iter()) + .enumerate() + { + // if available, use provided account for the first api (so that it would be permitted to do rewarding, etc.) + let cosmos_account = if i == 0 { + prime_api.take().unwrap_or(Account::new()) + } else { + Account::new() + }; + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); let data = EcashSigner { ed25519_keypair, ecash_keypair, - cosmos_account: Account::new(), + cosmos_account, endpoint, }; ctx.println(format!( @@ -400,10 +412,10 @@ impl NetworkManager { let mut receivers = Vec::new(); for signer in &ctx.ecash_signers { - // send 101nym to the admin + // send 250nym to the admin receivers.push(( signer.data.cosmos_account.address.clone(), - admin.mix_coins(101_000000), + admin.mix_coins(250_000000), )) } @@ -451,7 +463,11 @@ impl NetworkManager { let mut ctx = DkgSkipCtx::new(network)?; - self.generate_ecash_signer_data(&mut ctx, api_endpoints)?; + self.generate_ecash_signer_data( + &mut ctx, + api_endpoints, + Some(network.auxiliary_addresses.mixnet_rewarder.clone()), + )?; let current_code_id = self.validate_existing_contracts(&ctx).await?; self.persist_dkg_keys(&mut ctx, data_output_dir).await?; let new_code_id = self diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index c95c3062df..9e1d2025e2 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -96,8 +96,8 @@ impl NetworkManager { let wait_fut = async { let inner_fut = async { loop { - let nodes = match api_client.get_all_basic_nodes().await { - Ok(nodes) => nodes, + let nodes = match api_client.get_all_basic_nodes_with_metadata().await { + Ok(nodes) => nodes.nodes, Err(err) => { ctx.println(format!( "❌ {} {err}", diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index e140ccb20a..e029e9f0f4 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -10,14 +10,19 @@ use console::style; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::RoleAssignment; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, MixnetSigningClient, PagedMixnetQueryClient, +}; use nym_validator_client::DirectSigningHttpRpcNyxdClient; use serde::{Deserialize, Serialize}; use std::fs; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::process::Stdio; +use time::OffsetDateTime; use tokio::process::Command; +use tokio::time::sleep; +use tracing::error; use zeroize::Zeroizing; struct LocalNodesCtx<'a> { @@ -88,6 +93,16 @@ impl<'a> LocalNodesCtx<'a> { .clone(), )?) } + + fn signing_mixnet_contract_admin( + &self, + ) -> Result { + Ok(DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + self.network.client_config()?, + self.network.rpc_endpoint.as_str(), + self.network.contracts.mixnet.admin_mnemonic.clone(), + )?) + } } #[derive(Debug, Deserialize, Serialize)] @@ -158,8 +173,8 @@ impl NetworkManager { &output_file_path.display().to_string(), ]) .stdout(Stdio::null()) + .stderr(Stdio::piped()) .stdin(Stdio::null()) - .stderr(Stdio::null()) .kill_on_drop(true); if is_gateway { @@ -169,10 +184,12 @@ impl NetworkManager { cmd.args(["--mode", "mixnode"]); } - let mut child = cmd.spawn()?; - let child_fut = child.wait(); + let child = cmd.spawn()?; + let child_fut = child.wait_with_output(); let out = ctx.async_with_progress(child_fut).await?; - if !out.success() { + if !out.status.success() { + error!("nym node failure"); + println!("{}", String::from_utf8_lossy(&out.stderr)); return Err(NetworkManagerError::NymNodeExecutionFailure); } @@ -196,14 +213,16 @@ impl NetworkManager { "--output", "json", ]) - .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) .kill_on_drop(true) .output(); let out = ctx.async_with_progress(child).await?; if !out.status.success() { + error!("nym node failure"); + println!("{}", String::from_utf8_lossy(&out.stderr)); return Err(NetworkManagerError::NymNodeExecutionFailure); } let signature: ReducedSignatureOut = serde_json::from_slice(&out.stdout)?; @@ -222,6 +241,40 @@ impl NetworkManager { Ok(()) } + async fn check_if_network_is_empty( + &self, + ctx: &LocalNodesCtx<'_>, + ) -> Result<(), NetworkManagerError> { + ctx.println(format!( + "🐽 {}Making sure the network is fresh...", + style("[0/5]").bold().dim() + )); + + ctx.set_pb_message("checking network state..."); + + let client = ctx.signing_mixnet_contract_admin()?; + let fut = client.get_all_nymnode_bonds(); + let nym_nodes = ctx.async_with_progress(fut).await?; + + if !nym_nodes.is_empty() { + return Err(NetworkManagerError::NetworkNotEmpty); + } + + let fut = client.get_all_mixnode_bonds(); + let mixnodes = ctx.async_with_progress(fut).await?; + if !mixnodes.is_empty() { + return Err(NetworkManagerError::NetworkNotEmpty); + } + + let fut = client.get_all_gateways(); + let gateways = ctx.async_with_progress(fut).await?; + if !gateways.is_empty() { + return Err(NetworkManagerError::NetworkNotEmpty); + } + + Ok(()) + } + async fn initialise_nym_nodes( &self, ctx: &mut LocalNodesCtx<'_>, @@ -348,6 +401,75 @@ impl NetworkManager { // this could be batched in a single tx, but that's too much effort for now let rewarder = ctx.signing_rewarder()?; + ctx.set_pb_message("checking and temporarily adjusting epoch lengths..."); + let fut = rewarder.get_current_interval_details(); + let original_epoch = ctx.async_with_progress(fut).await?; + + let expected_end = original_epoch.interval.current_epoch_end(); + let now = OffsetDateTime::now_utc(); + if expected_end > now { + loop { + let now = OffsetDateTime::now_utc(); + let diff = expected_end - now; + if diff.is_negative() { + break; + } + + let std_diff = diff.unsigned_abs(); + let fut = sleep(std::time::Duration::from_millis(500)); + ctx.set_pb_message(format!( + "waiting for {} for the epoch end...", + humantime::format_duration(std_diff) + )); + ctx.async_with_progress(fut).await; + } + // wait extra 10s due to possible block time desync + ctx.set_pb_message("waiting extra 10s to make sure blocks have advanced".to_string()); + let fut = sleep(std::time::Duration::from_secs(10)); + ctx.async_with_progress(fut).await; + } + + // TODO: for some reason contract rejects correct admin. won't be debugging it now. + // let changed_length = if expected_end > now { + // + // // if it's < 10s, just wait + // let diff = expected_end - now; + // + // if diff < Duration::seconds(10) { + // let std_diff = diff.unsigned_abs(); + // let fut = sleep(std_diff); + // ctx.set_pb_message(format!( + // "waiting for {} for the epoch end...", + // humantime::format_duration(std_diff) + // )); + // ctx.async_with_progress(fut).await; + // false + // } else { + // ctx.println(format!( + // "🙈 {}Reducing epoch length...", + // style("[4.pre/5]").bold().dim() + // )); + // + // // just lower the epoch length and later restore it + // let admin = ctx.signing_mixnet_contract_admin()?; + // let fut = admin.update_interval_config( + // original_epoch.interval.epochs_in_interval(), + // 10, + // true, + // None, + // ); + // ctx.async_with_progress(fut).await?; + // let fut = sleep(std::time::Duration::from_secs(10)); + // ctx.set_pb_message("waiting for 10s for the epoch end..."); + // ctx.async_with_progress(fut).await; + // true + // } + // } else { + // false + // }; + + // reduce epoch length if it would prevent us from the advancing the state + ctx.set_pb_message("starting epoch transition..."); let fut = rewarder.begin_epoch_transition(None); ctx.async_with_progress(fut).await?; @@ -416,6 +538,23 @@ impl NetworkManager { ); ctx.async_with_progress(fut).await?; + // TODO: for some reason contract rejects correct admin. won't be debugging it now. + // if changed_length { + // ctx.println(format!( + // "🙈 {}Restoring epoch length...", + // style("[4.post/5]").bold().dim() + // )); + // ctx.set_pb_message("restoring original epoch length..."); + // let admin = ctx.signing_mixnet_contract_admin()?; + // let fut = admin.update_interval_config( + // original_epoch.interval.epochs_in_interval(), + // original_epoch.interval.epoch_length_secs(), + // true, + // None, + // ); + // ctx.async_with_progress(fut).await?; + // } + Ok(()) } @@ -438,7 +577,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(mixnode); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" )); } @@ -449,7 +588,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(gateway); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" )); } @@ -502,6 +641,7 @@ impl NetworkManager { return Err(NetworkManagerError::EnvFileNotGenerated); } + self.check_if_network_is_empty(&ctx).await?; self.initialise_nym_nodes(&mut ctx, mixnodes, gateways) .await?; self.transfer_bonding_tokens(&ctx).await?; diff --git a/tools/internal/testnet-manager/src/manager/network.rs b/tools/internal/testnet-manager/src/manager/network.rs index 6c65eda06c..39e04f10cf 100644 --- a/tools/internal/testnet-manager/src/manager/network.rs +++ b/tools/internal/testnet-manager/src/manager/network.rs @@ -65,6 +65,7 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { let contracts = nym_config::defaults::NymContracts { mixnet_contract_address: Some(value.contracts.mixnet.address.to_string()), vesting_contract_address: Some(value.contracts.vesting.address.to_string()), + performance_contract_address: Some(value.contracts.performance.address.to_string()), ecash_contract_address: Some(value.contracts.ecash.address.to_string()), group_contract_address: Some(value.contracts.cw4_group.address.to_string()), multisig_contract_address: Some(value.contracts.cw3_multisig.address.to_string()), @@ -81,8 +82,9 @@ impl<'a> From<&'a LoadedNetwork> for nym_config::defaults::NymNetworkDetails { api_url: None, }], contracts, - explorer_api: None, nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, } } } @@ -133,6 +135,7 @@ impl LoadedNetwork { pub struct SpecialAddresses { pub ecash_holding_account: Account, pub mixnet_rewarder: Account, + pub network_monitors: Vec, } impl Default for SpecialAddresses { @@ -140,6 +143,8 @@ impl Default for SpecialAddresses { SpecialAddresses { ecash_holding_account: Account::new(), mixnet_rewarder: Account::new(), + // by default use one address; to be adjusted in the future + network_monitors: vec![Account::new()], } } } diff --git a/tools/internal/testnet-manager/src/manager/network_init.rs b/tools/internal/testnet-manager/src/manager/network_init.rs index 1a0fc0e41d..44c5a97335 100644 --- a/tools/internal/testnet-manager/src/manager/network_init.rs +++ b/tools/internal/testnet-manager/src/manager/network_init.rs @@ -38,8 +38,9 @@ impl InitCtx { network_name: "foomp".to_string(), // does this matter? endpoints: vec![], contracts: Default::default(), - explorer_api: None, nym_vpn_api_url: None, + nym_vpn_api_urls: None, + nym_api_urls: None, }; Ok(Config::try_from_nym_network_details(&network_details)?) } @@ -127,6 +128,7 @@ impl NetworkManager { &self, ctx: &InitCtx, custom_epoch_duration: Option, + key_validity_in_epochs: Option, ) -> Result { Ok(nym_mixnet_contract_common::InstantiateMsg { rewarding_validator_address: ctx @@ -165,6 +167,7 @@ impl NetworkManager { version_score_params: Default::default(), profit_margin: Default::default(), interval_operating_cost: Default::default(), + key_validity_in_epochs, }) } @@ -258,6 +261,22 @@ impl NetworkManager { }) } + fn performance_init_message( + &self, + ctx: &InitCtx, + ) -> Result { + Ok(nym_performance_contract_common::msg::InstantiateMsg { + mixnet_contract_address: ctx.network.contracts.mixnet.address()?.to_string(), + authorised_network_monitors: ctx + .network + .auxiliary_addresses + .network_monitors + .iter() + .map(|acc| acc.address.to_string()) + .collect(), + }) + } + fn find_contracts>( &self, ctx: &mut InitCtx, @@ -293,6 +312,10 @@ impl NetworkManager { "\tdiscovered dkg contract at '{}'", ctx.network.contracts.dkg.wasm_path()?.display() )); + ctx.println(format!( + "\tdiscovered performance contract at '{}'", + ctx.network.contracts.performance.wasm_path()?.display() + )); ctx.println("\t✅ found all the contracts!"); @@ -389,6 +412,11 @@ impl NetworkManager { ctx.admin.mix_coins(10_000000), )); + // and to any network monitors + for network_monitor in &ctx.network.auxiliary_addresses.network_monitors { + receivers.push((network_monitor.address(), ctx.admin.mix_coins(10_000000))) + } + ctx.set_pb_message("attempting to send admin tokens..."); let send_future = @@ -408,6 +436,7 @@ impl NetworkManager { &self, ctx: &mut InitCtx, custom_epoch_duration: Option, + key_validity_in_epochs: Option, ) -> Result<(), NetworkManagerError> { ctx.println(format!( "💽 {}Instantiating all the contracts...", @@ -422,7 +451,8 @@ impl NetworkManager { let code_id = ctx.network.contracts.mixnet.upload_info()?.code_id; let admin = ctx.network.contracts.mixnet.admin()?.address.clone(); ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); - let init_msg = self.mixnet_init_message(ctx, custom_epoch_duration)?; + let init_msg = + self.mixnet_init_message(ctx, custom_epoch_duration, key_validity_in_epochs)?; let init_fut = ctx.admin.instantiate( code_id, &init_msg, @@ -548,6 +578,28 @@ impl NetworkManager { )); ctx.network.contracts.ecash.init_info = Some(res.into()); + // performance (semi-temp) + ctx.set_pb_prefix(format!("[7/{total}]")); + let name = &ctx.network.contracts.performance.name; + let code_id = ctx.network.contracts.performance.upload_info()?.code_id; + let admin = ctx.network.contracts.performance.admin()?.address.clone(); + ctx.set_pb_message(format!("attempting to instantiate {name} contract...")); + let init_msg = self.performance_init_message(ctx)?; + let init_fut = ctx.admin.instantiate( + code_id, + &init_msg, + format!("{name} contract"), + "contract instantiation from testnet-manager", + Some(InstantiateOptions::default().with_admin(admin)), + None, + ); + let res = ctx.async_with_progress(init_fut).await?; + let address = &res.contract_address; + ctx.println(format!( + "\t{name} contract instantiated with address: {address}", + )); + ctx.network.contracts.performance.init_info = Some(res.into()); + ctx.println("\t✅ instantiated all the contracts!"); Ok(()) @@ -694,6 +746,7 @@ impl NetworkManager { contracts: P, network_name: Option, custom_epoch_duration: Option, + key_validity_in_epochs: Option, ) -> Result { let network_name = self.get_network_name(network_name); let mut ctx = InitCtx::new(network_name, self.admin.deref().clone(), &self.rpc_endpoint)?; @@ -702,7 +755,7 @@ impl NetworkManager { self.upload_contracts(&mut ctx).await?; self.create_contract_admins_mnemonics(&mut ctx)?; self.transfer_admin_tokens(&ctx).await?; - self.instantiate_contracts(&mut ctx, custom_epoch_duration) + self.instantiate_contracts(&mut ctx, custom_epoch_duration, key_validity_in_epochs) .await?; self.perform_final_migrations(&mut ctx).await?; self.get_build_info(&mut ctx).await?; diff --git a/tools/internal/testnet-manager/src/manager/storage/manager.rs b/tools/internal/testnet-manager/src/manager/storage/manager.rs index 24132db687..1bd3713d3b 100644 --- a/tools/internal/testnet-manager/src/manager/storage/manager.rs +++ b/tools/internal/testnet-manager/src/manager/storage/manager.rs @@ -1,6 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::manager::storage::models::{RawAccount, RawContract, RawNetwork}; +use crate::manager::storage::models::{ + RawAccount, RawAuthorisedNetworkMonitor, RawContract, RawNetwork, +}; use time::OffsetDateTime; #[derive(Clone)] @@ -85,6 +87,7 @@ impl StorageManager { cw3_id: i64, cw4_id: i64, dkg_id: i64, + performance_id: i64, rewarder_address: &str, ecash_holding_address: &str, ) -> Result { @@ -99,10 +102,11 @@ impl StorageManager { cw3_multisig_contract_id, cw4_group_contract_id, dkg_contract_id, + performance_contract_id, rewarder_address, ecash_holding_account_address ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, name, created_at, @@ -112,6 +116,7 @@ impl StorageManager { cw3_id, cw4_id, dkg_id, + performance_id, rewarder_address, ecash_holding_address, ) @@ -159,19 +164,46 @@ impl StorageManager { .await } + pub(crate) async fn save_authorised_network_monitor( + &self, + network_id: i64, + address: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO authorised_network_monitor (network_id, address) VALUES (?, ?)", + network_id, + address, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn load_authorised_network_monitors( + &self, + network_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM authorised_network_monitor WHERE network_id = ?") + .bind(network_id) + .fetch_all(&self.connection_pool) + .await + } + pub(crate) async fn save_account( &self, address: &str, mnemonic: &str, - ) -> Result<(), sqlx::Error> { - sqlx::query!( + ) -> Result { + let account_id = sqlx::query!( "INSERT INTO account (address, mnemonic) VALUES (?, ?)", address, mnemonic ) .execute(&self.connection_pool) - .await?; - Ok(()) + .await? + .last_insert_rowid(); + Ok(account_id) } pub(crate) async fn load_account(&self, address: &str) -> Result { diff --git a/tools/internal/testnet-manager/src/manager/storage/mod.rs b/tools/internal/testnet-manager/src/manager/storage/mod.rs index 7eb3df7cb1..21c85df32e 100644 --- a/tools/internal/testnet-manager/src/manager/storage/mod.rs +++ b/tools/internal/testnet-manager/src/manager/storage/mod.rs @@ -1,6 +1,6 @@ -use std::fs; // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only + use crate::{ error::NetworkManagerError, manager::{ @@ -14,6 +14,7 @@ use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, ConnectOptions, }; +use std::fs; use std::path::Path; use tracing::{error, info}; use url::Url; @@ -159,7 +160,7 @@ impl NetworkManagerStorage { .await?) } - async fn persist_account(&self, account: &Account) -> Result<(), NetworkManagerError> { + async fn persist_account(&self, account: &Account) -> Result { let as_str = Zeroizing::new(account.mnemonic.to_string()); Ok(self .manager @@ -191,6 +192,18 @@ impl NetworkManagerStorage { Ok(()) } + async fn persist_authorised_network_monitor( + &self, + network_id: i64, + account: &Account, + ) -> Result<(), NetworkManagerError> { + self.persist_account(account).await?; + self.manager + .save_authorised_network_monitor(network_id, account.address.as_ref()) + .await?; + Ok(()) + } + pub(crate) async fn persist_network( &self, network: &Network, @@ -206,6 +219,8 @@ impl NetworkManagerStorage { self.persist_account(network.contracts.cw4_group.admin()?) .await?; self.persist_account(network.contracts.dkg.admin()?).await?; + self.persist_account(network.contracts.performance.admin()?) + .await?; self.persist_account(&network.auxiliary_addresses.mixnet_rewarder) .await?; @@ -220,6 +235,9 @@ impl NetworkManagerStorage { .await?; let cw4_group_id = self.persist_contract(&network.contracts.cw4_group).await?; let dkg_id = self.persist_contract(&network.contracts.dkg).await?; + let performance_id = self + .persist_contract(&network.contracts.performance) + .await?; let network_id = self .manager @@ -232,6 +250,7 @@ impl NetworkManagerStorage { cw3_multisig_id, cw4_group_id, dkg_id, + performance_id, network.auxiliary_addresses.mixnet_rewarder.address.as_ref(), network .auxiliary_addresses @@ -242,6 +261,10 @@ impl NetworkManagerStorage { .await?; self.manager.save_latest_network_id(network_id).await?; + for nm in &network.auxiliary_addresses.network_monitors { + self.persist_authorised_network_monitor(network_id, nm) + .await? + } Ok(()) } @@ -256,6 +279,20 @@ impl NetworkManagerStorage { .await? .ok_or_else(|| NetworkManagerError::RpcEndpointNotSet)?; + let authorised = self + .manager + .load_authorised_network_monitors(base_network.id) + .await?; + let mut network_monitors = Vec::with_capacity(authorised.len()); + for authorised in authorised { + network_monitors.push( + self.manager + .load_account(&authorised.address) + .await? + .try_into()?, + ) + } + Ok(LoadedNetwork { id: base_network.id, name: base_network.name, @@ -292,6 +329,11 @@ impl NetworkManagerStorage { .load_contract(base_network.dkg_contract_id) .await? .try_into()?, + performance: self + .manager + .load_contract(base_network.performance_contract_id) + .await? + .try_into()?, }, auxiliary_addresses: SpecialAddresses { ecash_holding_account: self @@ -304,6 +346,7 @@ impl NetworkManagerStorage { .load_account(&base_network.rewarder_address) .await? .try_into()?, + network_monitors, }, }) } diff --git a/tools/internal/testnet-manager/src/manager/storage/models.rs b/tools/internal/testnet-manager/src/manager/storage/models.rs index c224fc36d5..9df7ebb481 100644 --- a/tools/internal/testnet-manager/src/manager/storage/models.rs +++ b/tools/internal/testnet-manager/src/manager/storage/models.rs @@ -6,6 +6,14 @@ use crate::manager::contract::{Account, LoadedContract}; use sqlx::FromRow; use time::OffsetDateTime; +#[allow(dead_code)] +#[derive(FromRow)] +pub(crate) struct RawAuthorisedNetworkMonitor { + pub(crate) id: i64, + pub(crate) network_id: i64, + pub(crate) address: String, +} + #[derive(FromRow)] pub(crate) struct RawAccount { pub(crate) address: String, @@ -70,6 +78,7 @@ pub(crate) struct RawNetwork { pub(crate) cw3_multisig_contract_id: i64, pub(crate) cw4_group_contract_id: i64, pub(crate) dkg_contract_id: i64, + pub(crate) performance_contract_id: i64, pub(crate) rewarder_address: String, pub(crate) ecash_holding_account_address: String, diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index c255f53fcf..a30c16a255 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.53" +version = "1.1.61" authors.workspace = true edition = "2021" license.workspace = true @@ -14,7 +14,6 @@ clap_complete_fig = { workspace = true } dotenvy = { workspace = true } inquire = { workspace = true } log = { workspace = true } -pretty_env_logger = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros", "signal"] } @@ -23,7 +22,7 @@ anyhow = { workspace = true } tap = { workspace = true } nym-cli-commands = { path = "../../common/commands" } -nym-bin-common = { path = "../../common/bin-common"} +nym-bin-common = { path = "../../common/bin-common", features = ["basic_tracing"] } nym-validator-client = { path = "../../common/client-libs/validator-client", features = ["http-client"] } nym-network-defaults = { path = "../../common/network-defaults" } diff --git a/tools/nym-cli/src/internal/ecash/mod.rs b/tools/nym-cli/src/internal/ecash/mod.rs index ee9f8ef47c..8973925500 100644 --- a/tools/nym-cli/src/internal/ecash/mod.rs +++ b/tools/nym-cli/src/internal/ecash/mod.rs @@ -7,8 +7,8 @@ use nym_network_defaults::NymNetworkDetails; pub(super) async fn execute( global_args: ClientArgs, - ecash: nym_cli_commands::internal::ecash::InternalEcash, nym_network_details: &NymNetworkDetails, + ecash: nym_cli_commands::internal::ecash::InternalEcash, ) -> anyhow::Result<()> { // I reckon those will be needed later let _ = global_args; diff --git a/tools/nym-cli/src/internal/mod.rs b/tools/nym-cli/src/internal/mod.rs index 253c45b227..5bdb26f78d 100644 --- a/tools/nym-cli/src/internal/mod.rs +++ b/tools/nym-cli/src/internal/mod.rs @@ -6,6 +6,7 @@ use nym_cli_commands::internal::InternalCommands; use nym_network_defaults::NymNetworkDetails; mod ecash; +mod nyx; pub(super) async fn execute( global_args: ClientArgs, @@ -14,7 +15,10 @@ pub(super) async fn execute( ) -> anyhow::Result<()> { match internal.command { InternalCommands::Ecash(ecash_commands) => { - ecash::execute(global_args, ecash_commands, nym_network_details).await + ecash::execute(global_args, nym_network_details, ecash_commands).await + } + InternalCommands::Nyx(nyx_commands) => { + nyx::execute(global_args, nym_network_details, nyx_commands).await } } } diff --git a/tools/nym-cli/src/internal/nyx/mod.rs b/tools/nym-cli/src/internal/nyx/mod.rs new file mode 100644 index 0000000000..ac7bb500fc --- /dev/null +++ b/tools/nym-cli/src/internal/nyx/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::internal::nyx::InternalNyxCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(super) async fn execute( + global_args: ClientArgs, + nym_network_details: &NymNetworkDetails, + nyx: nym_cli_commands::internal::nyx::InternalNyx, +) -> anyhow::Result<()> { + match nyx.command { + InternalNyxCommands::ForceAdvanceEpoch(args) => { + nym_cli_commands::internal::nyx::force_advance_epoch::force_advance_epoch( + args, + create_signing_client(global_args, nym_network_details)?, + ) + .await + } + } +} diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index 7854dbb0d2..b8315de641 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -3,7 +3,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use log::{error, warn}; -use nym_bin_common::logging::setup_logging; +use nym_bin_common::logging::setup_tracing_logger; use nym_cli_commands::context::{get_network_details, ClientArgs}; use nym_validator_client::nyxd::AccountId; @@ -138,10 +138,7 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { async fn wait_for_interrupt() { if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); + error!("There was an error while capturing SIGINT - {e:?}. We will terminate regardless",); } println!( "Received SIGINT - the process will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." @@ -150,7 +147,7 @@ async fn wait_for_interrupt() { #[tokio::main] async fn main() -> anyhow::Result<()> { - setup_logging(); + setup_tracing_logger(); let cli = Cli::parse(); diff --git a/tools/nym-nr-query/Cargo.toml b/tools/nym-nr-query/Cargo.toml index 8309e7e9c8..f5ec6f6475 100644 --- a/tools/nym-nr-query/Cargo.toml +++ b/tools/nym-nr-query/Cargo.toml @@ -10,7 +10,7 @@ license.workspace = true anyhow = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"]} log = { workspace = true } -nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } +nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "basic_tracing"] } nym-network-defaults = { path = "../../common/network-defaults" } nym-sdk = { path = "../../sdk/rust/nym-sdk" } nym-service-providers-common = { path = "../../service-providers/common" } diff --git a/tools/nym-nr-query/src/main.rs b/tools/nym-nr-query/src/main.rs index 477d3937c9..aecf7110d0 100644 --- a/tools/nym-nr-query/src/main.rs +++ b/tools/nym-nr-query/src/main.rs @@ -75,7 +75,7 @@ fn parse_socks5_response(received: Vec) -> Socks5R assert_eq!(received.len(), 1); let response: Response = Response::try_from_bytes(&received[0].message).unwrap(); match response.content { - ResponseContent::Control(control) => panic!("unexpected control response: {:?}", control), + ResponseContent::Control(control) => panic!("unexpected control response: {control:?}"), ResponseContent::ProviderData(data) => data, } } @@ -276,9 +276,9 @@ enum ClientResponse { impl fmt::Display for ClientResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ClientResponse::Control(control) => write!(f, "{:#?}", control), - ClientResponse::Query(query) => write!(f, "{:#?}", query), - ClientResponse::Ping(ping) => write!(f, "{}", ping), + ClientResponse::Control(control) => write!(f, "{control:#?}"), + ClientResponse::Query(query) => write!(f, "{query:#?}"), + ClientResponse::Ping(ping) => write!(f, "{ping}"), } } } @@ -312,7 +312,7 @@ async fn main() -> anyhow::Result<()> { let args = Cli::parse(); if args.debug { - nym_bin_common::logging::setup_logging(); + nym_bin_common::logging::setup_tracing_logger(); } nym_network_defaults::setup_env(args.config_env_file.as_ref()); diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 54bf2ff786..f7331cda2d 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.18" +version = "0.1.26" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/tools/nymvisor/src/upgrades/types.rs b/tools/nymvisor/src/upgrades/types.rs index abeea0ed70..50b997be80 100644 --- a/tools/nymvisor/src/upgrades/types.rs +++ b/tools/nymvisor/src/upgrades/types.rs @@ -150,10 +150,7 @@ impl UpgradePlan { pub(crate) fn try_load>(path: P) -> Result { let path = path.as_ref(); let mut upgrade_plan: UpgradePlan = fs::File::open(path) - .and_then(|file| { - serde_json::from_reader(file) - .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) - }) + .and_then(|file| serde_json::from_reader(file).map_err(io::Error::other)) .map_err(|source| NymvisorError::UpgradePlanLoadFailure { path: path.to_path_buf(), source, @@ -274,10 +271,7 @@ impl UpgradeInfo { pub(crate) fn try_load>(path: P) -> Result { let path = path.as_ref(); fs::File::open(path) - .and_then(|file| { - serde_json::from_reader(file) - .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) - }) + .and_then(|file| serde_json::from_reader(file).map_err(io::Error::other)) .map_err(|source| NymvisorError::UpgradeInfoLoadFailure { path: path.to_path_buf(), source, @@ -426,10 +420,7 @@ impl UpgradeHistory { pub(crate) fn try_load>(path: P) -> Result { let path = path.as_ref(); let mut history: UpgradeHistory = fs::File::open(path) - .and_then(|file| { - serde_json::from_reader(file) - .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) - }) + .and_then(|file| serde_json::from_reader(file).map_err(io::Error::other)) .map_err(|source| NymvisorError::UpgradeHistoryLoadFailure { path: path.to_path_buf(), source, @@ -481,10 +472,7 @@ impl CurrentVersionInfo { pub(crate) fn try_load>(path: P) -> Result { let path = path.as_ref(); fs::File::open(path) - .and_then(|file| { - serde_json::from_reader(file) - .map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err)) - }) + .and_then(|file| serde_json::from_reader(file).map_err(io::Error::other)) .map_err(|source| NymvisorError::CurrentVersionInfoLoadFailure { path: path.to_path_buf(), source, diff --git a/ts-packages/mock-nym-api/.env b/ts-packages/mock-nym-api/.env deleted file mode 100644 index 2b528d6467..0000000000 --- a/ts-packages/mock-nym-api/.env +++ /dev/null @@ -1 +0,0 @@ -NYM_API_URL=https://validator.nymtech.net diff --git a/ts-packages/mock-nym-api/Caddyfile b/ts-packages/mock-nym-api/Caddyfile deleted file mode 100644 index 097a0b9fbb..0000000000 --- a/ts-packages/mock-nym-api/Caddyfile +++ /dev/null @@ -1,4 +0,0 @@ -localhost:8001 { - reverse_proxy 127.0.0.1:8000 - tls internal -} diff --git a/ts-packages/mock-nym-api/README.md b/ts-packages/mock-nym-api/README.md deleted file mode 100644 index 8936d8ff7b..0000000000 --- a/ts-packages/mock-nym-api/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# NYM API mock - -This package provides a mock server that allows you to modify parts of the Nym API by: - -- modifying live responses from a running Nym API -- providing a static response - -## How to run it? - -Adjust the [.env](./.env) to use the Nym API you want to proxy. The defaults are for mainnet. - -From this directory run: - -``` -yarn -yarn start -``` - -When you modify files in `src` and `mocks` the process will be restarted automatically. - -## How can I find out what methods I can override? - -Look in the swagger docs for the Nym API, e.g. https://validator.nymtech.net/api/swagger/index.html. - -Then write a handler to override it, for example, to return custom gateways: - -```ts -app.get('/api/v1/gateways', (req, res) => { - const customGateways = JSON.parse(fs.readFileSync('./mocks/custom-gateway.json').toString()); - - // modify custom gateway - customGateways[0].gateway.sphinx_key += '-ccc'; - customGateways[0].gateway.identity_key += '-ddd'; - - res.json(customGateways); -}); -``` - -## How to get seed data? - -You can get seed data from a running Nym API with: - -``` -curl https://validator.nymtech.net/api/v1/mixnodes | jq . > mocks/mixnodes.json -``` - -If you don't have `jq` installed, you can just do `curl https://validator.nymtech.net/api/v1/mixnodes > mocks/mixnodes.json` to store the unformatted response. - -## HTTPS - -If you need HTTPS then install `caddy` (see https://caddyserver.com/ or `brew install caddy` on MacOS) and run: - -``` -yarn -yarn start:https -``` - -The API will available on `https://localhost:8001` e.g. https://localhost:8001/api/v1/mixnodes with a self-signed certificate. - -Modify the [Caddyfile](Caddyfile) to set the domain for the certificate (NB: you will need to set up the DNS and control the domain records). - -If you need the local certificate to be valid, look at https://github.com/FiloSottile/mkcert and https://dev.to/josuebustos/https-localhost-for-node-js-1p1k. - diff --git a/ts-packages/mock-nym-api/mocks/custom-gateway.json b/ts-packages/mock-nym-api/mocks/custom-gateway.json deleted file mode 100644 index f1eaf99514..0000000000 --- a/ts-packages/mock-nym-api/mocks/custom-gateway.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "pledge_amount": { - "denom": "unym", - "amount": "100000000" - }, - "owner": "n1...", - "block_height": 5900000, - "gateway": { - "host": "127.0.0.1", - "mix_port": 1789, - "clients_port": 9000, - "location": "My local computer", - "sphinx_key": "aaa", - "identity_key": "bbb", - "version": "1.1.11" - }, - "proxy": null - } -] diff --git a/ts-packages/mock-nym-api/mocks/gateways.json b/ts-packages/mock-nym-api/mocks/gateways.json deleted file mode 100644 index ed7945bbe4..0000000000 --- a/ts-packages/mock-nym-api/mocks/gateways.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "pledge_amount": { - "denom": "unym", - "amount": "100000000" - }, - "owner": "n1ecaqmk4ajcc80mtgpvvqf6zun7eq8k64uz6f40", - "block_height": 5908500, - "gateway": { - "host": "194.182.191.207", - "mix_port": 1789, - "clients_port": 9000, - "location": "Switzerland", - "sphinx_key": "2az9QVu6o4ZEVe72HQaxcDEdSFSxXiFvH3gWiU33Vrmn", - "identity_key": "2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh", - "version": "1.1.11" - }, - "proxy": null - } -] diff --git a/ts-packages/mock-nym-api/mocks/mixnodes.json b/ts-packages/mock-nym-api/mocks/mixnodes.json deleted file mode 100644 index 9afcf3b4ac..0000000000 --- a/ts-packages/mock-nym-api/mocks/mixnodes.json +++ /dev/null @@ -1,116 +0,0 @@ -[ - { - "bond_information": { - "mix_id": 1, - "owner": "n1cg6lgel27zmq8yutxgnu2tdzx3hf0m8uemfpwp", - "original_pledge": { - "denom": "unym", - "amount": "164186215" - }, - "layer": 2, - "mix_node": { - "host": "116.203.115.86", - "mix_port": 1789, - "verloc_port": 1790, - "http_api_port": 8000, - "sphinx_key": "82MfLZ4NtBmuZXL45XC1x9HNwsK3HqW7iNNpd4YdgTdY", - "identity_key": "12Vbu59dESdcQEfPKvbqt3r6XAhvn5xAiLB3Ke4L42Nn", - "version": "1.1.4" - }, - "proxy": null, - "bonding_height": 2622578, - "is_unbonding": false - }, - "rewarding_details": { - "cost_params": { - "profit_margin_percent": "0.05", - "interval_operating_cost": { - "denom": "unym", - "amount": "40000000" - } - }, - "operator": "177630956.71827075154157932", - "delegates": "2002077254.831772380804187632", - "total_unit_reward": "923810.444666272288950945", - "unit_delegation": "1000000000", - "last_rewarded_epoch": 2580, - "unique_delegations": 2 - } - }, - { - "bond_information": { - "mix_id": 2, - "owner": "n1vfdtzh9vzaaxxv9n9w4ywdzle9m2pejhx0d5r7", - "original_pledge": { - "denom": "unym", - "amount": "1691956412" - }, - "layer": 1, - "mix_node": { - "host": "95.179.226.75", - "mix_port": 1789, - "verloc_port": 1790, - "http_api_port": 8000, - "sphinx_key": "HomUqYe7hyC1cBNbMUKWguLfq7aFdoxoQDyQsa2fGQa1", - "identity_key": "14Qzy15U7uA8pVwvs66MuRvABSEK5C62JaDYD18WbkN7", - "version": "1.1.10" - }, - "proxy": null, - "bonding_height": 2944467, - "is_unbonding": false - }, - "rewarding_details": { - "cost_params": { - "profit_margin_percent": "0.05", - "interval_operating_cost": { - "denom": "unym", - "amount": "500000000" - } - }, - "operator": "1774161955.223791723839539477", - "delegates": "637943739106.672292940395867727", - "total_unit_reward": "67206510.581661315274414946", - "unit_delegation": "1000000000", - "last_rewarded_epoch": 2583, - "unique_delegations": 31 - } - }, - { - "bond_information": { - "mix_id": 5, - "owner": "n1v7nrjw6pnkrdxmmuk7pjtxrzrxrtztgh33pszu", - "original_pledge": { - "denom": "unym", - "amount": "28752403445" - }, - "layer": 3, - "mix_node": { - "host": "194.163.191.29", - "mix_port": 1789, - "verloc_port": 1790, - "http_api_port": 8000, - "sphinx_key": "2BjwQw2txBofh7QvRAt3d8mUDLZHZgwTm3cuxpkngiWX", - "identity_key": "26WHDRFM5QU7whg1hnxKk4iVY7DnXyZknPcNMitX6GXp", - "version": "1.1.10" - }, - "proxy": null, - "bonding_height": 2935258, - "is_unbonding": false - }, - "rewarding_details": { - "cost_params": { - "profit_margin_percent": "0.05", - "interval_operating_cost": { - "denom": "unym", - "amount": "40000000" - } - }, - "operator": "31540882828.329440044410277309", - "delegates": "700258477245.036486032804830664", - "total_unit_reward": "73511253.913602285045142911", - "unit_delegation": "1000000000", - "last_rewarded_epoch": 2583, - "unique_delegations": 126 - } - } -] diff --git a/ts-packages/mock-nym-api/package.json b/ts-packages/mock-nym-api/package.json deleted file mode 100644 index f21e2b884e..0000000000 --- a/ts-packages/mock-nym-api/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "mock-nym-api", - "version": "1.0.0", - "license": "Apache-2.0", - "main": "index.js", - "type": "module", - "dependencies": { - "express": "^4.18.2", - "http-proxy-middleware": "^2.0.6", - "dotenv": "^16.0.3" - }, - "devDependencies": { - "@babel/core": "^7.17.5", - "@typescript-eslint/eslint-plugin": "^5.13.0", - "@typescript-eslint/parser": "^5.13.0", - "babel-loader": "^8.2.3", - "babel-plugin-root-import": "^5.1.0", - "eslint": "^8.10.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-airbnb-typescript": "^16.1.0", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-root-import": "^1.0.4", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jest": "^26.1.1", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.2", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-storybook": "^0.5.12", - "jest": "^27.1.0", - "prettier": "^2.8.7", - "rimraf": "^3.0.2", - "ts-jest": "^27.0.5", - "typescript": "^4.6.2", - "ts-node": "10", - "nodemon": "^2.0.21", - "npm-run-all": "^4.1.5" - }, - "scripts": { - "clean": "rimraf dist", - "start": "nodemon --watch src --watch mocks -e ts,json --exec 'ts-node --esm' src/index.ts", - "start:https": "run-p start run:caddy", - "run:caddy": "caddy run", - "build": "tsc --noEmit false", - "watch": "tsc --noEmit false -w", - "lint": "eslint src", - "lint:fix": "eslint src --fix" - } -} diff --git a/ts-packages/mock-nym-api/src/index.ts b/ts-packages/mock-nym-api/src/index.ts deleted file mode 100644 index dc5848fd1a..0000000000 --- a/ts-packages/mock-nym-api/src/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable no-console */ -import express from 'express'; -import dotenv from 'dotenv'; -import { createProxyMiddleware } from 'http-proxy-middleware'; -import fs from 'fs'; - -dotenv.config(); - -const app = express(); -const port = process.env.PORT || 8000; - -const { NYM_API_URL } = process.env; - -if (!NYM_API_URL || NYM_API_URL.trim().length < 1) { - throw new Error('Please specify a valid NYM_API_URL in `.env` or as an environment variable'); -} - -// proxy the Nym API and only override some routes -const proxy = createProxyMiddleware(['/api/**', '/swagger/**'], { - target: NYM_API_URL, - changeOrigin: true, -}); - -/** - * Return a single custom gateway, from a static file and modify some the fields - */ -app.get('/api/v1/gateways', (req, res) => { - const customGateways = JSON.parse(fs.readFileSync('./mocks/custom-gateway.json').toString()); - - // modify custom gateway - customGateways[0].gateway.sphinx_key += '-ccc'; - customGateways[0].gateway.identity_key += '-ddd'; - - res.json(customGateways); -}); - -/** - * Returns only 3 mixnodes from a static file - */ -app.get('/api/v1/mixnodes', (req, res) => { - const customMixnodes = JSON.parse(fs.readFileSync('./mocks/mixnodes.json').toString()); - res.json(customMixnodes); -}); - -// start the Express server -app.use(proxy); -app.listen(port, () => { - console.log(`[API] Nym API mock is running at http://localhost:${port} and proxying ${NYM_API_URL}`); -}); diff --git a/ts-packages/mock-nym-api/tsconfig.json b/ts-packages/mock-nym-api/tsconfig.json deleted file mode 100644 index 47d23cadec..0000000000 --- a/ts-packages/mock-nym-api/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist", - "noEmit": true - }, - "include": [ - "src/**/*.ts", - ], - "exclude": [ - "node_modules", - "dist" - ] -} diff --git a/wasm/client/internal-dev/package-lock.json b/wasm/client/internal-dev/package-lock.json index 1256d3de35..0b7eb5945c 100644 --- a/wasm/client/internal-dev/package-lock.json +++ b/wasm/client/internal-dev/package-lock.json @@ -1943,9 +1943,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/wasm/client/internal-dev/yarn.lock b/wasm/client/internal-dev/yarn.lock index b8a914dd2b..b7ead3db0e 100644 --- a/wasm/client/internal-dev/yarn.lock +++ b/wasm/client/internal-dev/yarn.lock @@ -1162,9 +1162,9 @@ http-parser-js@>=0.5.1: integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" diff --git a/wasm/client/src/encoded_payload_helper.rs b/wasm/client/src/encoded_payload_helper.rs index 71600ed5ce..8f2099425f 100644 --- a/wasm/client/src/encoded_payload_helper.rs +++ b/wasm/client/src/encoded_payload_helper.rs @@ -61,7 +61,7 @@ pub fn encode_payload_with_headers( Ok([size, metadata, payload].concat()) } Err(e) => Err(JsValue::from(JsError::new( - format!("Could not encode message: {}", e).as_str(), + format!("Could not encode message: {e}").as_str(), ))), } } @@ -84,7 +84,7 @@ pub fn decode_payload(message: Vec) -> Result { .unwrap() .unchecked_into::()), Err(e) => Err(JsValue::from(JsError::new( - format!("Could not parse message: {}", e).as_str(), + format!("Could not parse message: {e}").as_str(), ))), } } diff --git a/wasm/mix-fetch/go-mix-conn/go.mod b/wasm/mix-fetch/go-mix-conn/go.mod index bd67a14c06..6adec2ce08 100644 --- a/wasm/mix-fetch/go-mix-conn/go.mod +++ b/wasm/mix-fetch/go-mix-conn/go.mod @@ -4,6 +4,6 @@ go 1.23.0 toolchain go1.23.3 -require golang.org/x/net v0.36.0 +require golang.org/x/net v0.38.0 -require golang.org/x/text v0.22.0 // indirect +require golang.org/x/text v0.23.0 // indirect diff --git a/wasm/mix-fetch/go-mix-conn/go.sum b/wasm/mix-fetch/go-mix-conn/go.sum index 469ae58fbe..d5b7c23864 100644 --- a/wasm/mix-fetch/go-mix-conn/go.sum +++ b/wasm/mix-fetch/go-mix-conn/go.sum @@ -1,4 +1,4 @@ -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= diff --git a/wasm/mix-fetch/internal-dev/package-lock.json b/wasm/mix-fetch/internal-dev/package-lock.json index 596dca6c6f..8b67650496 100644 --- a/wasm/mix-fetch/internal-dev/package-lock.json +++ b/wasm/mix-fetch/internal-dev/package-lock.json @@ -20,7 +20,7 @@ "hello-wasm-pack": "^0.1.0", "webpack": "^5.98.0", "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4" + "webpack-dev-server": "^5.2.1" } }, "../go-mix-conn/build": { @@ -105,10 +105,67 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", + "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true, "license": "MIT" }, @@ -155,9 +212,9 @@ "link": true }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -166,9 +223,9 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -176,9 +233,9 @@ } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { @@ -186,9 +243,9 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "license": "MIT", "dependencies": { @@ -226,9 +283,9 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -239,21 +296,29 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-proxy": { - "version": "1.17.10", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", - "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", "dev": true, "license": "MIT", "dependencies": { @@ -268,9 +333,9 @@ "license": "MIT" }, "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, @@ -281,31 +346,52 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true, "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "license": "MIT", "dependencies": { @@ -313,20 +399,21 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "*", - "@types/node": "*" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -334,9 +421,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -659,20 +746,6 @@ "node": ">= 8" } }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -681,13 +754,16 @@ "license": "MIT" }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/body-parser": { @@ -726,29 +802,16 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, "license": "MIT", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -802,6 +865,22 @@ "dev": true, "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -865,16 +944,10 @@ "license": "CC-BY-4.0" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -888,6 +961,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -983,13 +1059,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -1097,27 +1166,47 @@ "ms": "2.0.0" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "execa": "^5.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/depd": { @@ -1161,17 +1250,10 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true, - "license": "MIT" - }, "node_modules/dns-packet": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz", - "integrity": "sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "license": "MIT", "dependencies": { @@ -1378,30 +1460,6 @@ "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -1573,9 +1631,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -1613,19 +1671,20 @@ "node": ">= 0.6" } }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "Unlicense" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, "node_modules/function-bind": { "version": "1.1.2", @@ -1676,40 +1735,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1882,13 +1907,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true, - "license": "MIT" - }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -1936,9 +1954,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1960,14 +1978,14 @@ } } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.17.0" + "node": ">=10.18" } }, "node_modules/iconv-lite": { @@ -2013,17 +2031,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2042,9 +2049,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, "license": "MIT", "engines": { @@ -2078,16 +2085,16 @@ } }, "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2116,6 +2123,38 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2152,30 +2191,20 @@ "node": ">=0.10.0" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isarray": { @@ -2242,14 +2271,14 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/loader-runner": { @@ -2296,16 +2325,23 @@ } }, "node_modules/memfs": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", - "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, - "license": "Unlicense", + "license": "Apache-2.0", "dependencies": { - "fs-monkey": "^1.0.3" + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" }, "engines": { "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" } }, "node_modules/merge-descriptors": { @@ -2395,16 +2431,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -2412,19 +2438,6 @@ "dev": true, "license": "ISC" }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2490,19 +2503,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2546,45 +2546,20 @@ "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "dev": true, "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2620,17 +2595,21 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -2663,16 +2642,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2984,20 +2953,17 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/run-parallel": { @@ -3080,12 +3046,13 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, "license": "MIT", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -3273,11 +3240,14 @@ } }, "node_modules/shell-quote": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", - "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3358,13 +3328,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -3513,16 +3476,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3616,6 +3569,19 @@ } } }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -3646,6 +3612,30 @@ "node": ">=0.6" } }, + "node_modules/tree-dump": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", + "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3878,79 +3868,83 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-dev-server": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz", - "integrity": "sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz", + "integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "http-proxy-middleware": "^2.0.7", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -4033,13 +4027,6 @@ "dev": true, "license": "MIT" }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", diff --git a/wasm/mix-fetch/internal-dev/package.json b/wasm/mix-fetch/internal-dev/package.json index 1e813770d9..cf3cef682a 100644 --- a/wasm/mix-fetch/internal-dev/package.json +++ b/wasm/mix-fetch/internal-dev/package.json @@ -32,7 +32,7 @@ "hello-wasm-pack": "^0.1.0", "webpack": "^5.98.0", "webpack-cli": "^4.9.2", - "webpack-dev-server": "^4.7.4" + "webpack-dev-server": "^5.2.1" }, "dependencies": { "@nymproject/mix-fetch-wasm": "file:../pkg", diff --git a/wasm/mix-fetch/internal-dev/yarn.lock b/wasm/mix-fetch/internal-dev/yarn.lock index c9e1578198..336d46a647 100644 --- a/wasm/mix-fetch/internal-dev/yarn.lock +++ b/wasm/mix-fetch/internal-dev/yarn.lock @@ -52,6 +52,26 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jsonjoy.com/base64@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/json-pack@^1.0.3": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz#e658900e81d194903171c42546e1aa27f446846a" + integrity sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA== + dependencies: + "@jsonjoy.com/base64" "^1.1.1" + "@jsonjoy.com/util" "^1.1.2" + hyperdyperid "^1.2.0" + thingies "^1.20.0" + +"@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.6.0.tgz#23991b2fe12cb3a006573d9dc97c768d3ed2c9f1" + integrity sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" @@ -89,17 +109,17 @@ "@types/connect" "*" "@types/node" "*" -"@types/bonjour@^3.5.9": - version "3.5.10" - resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz" - integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" -"@types/connect-history-api-fallback@^1.3.5": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== dependencies: "@types/express-serve-static-core" "*" "@types/node" "*" @@ -132,25 +152,31 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.33" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz" - integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" -"@types/express@*", "@types/express@^4.17.13": - version "4.17.17" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== +"@types/express@*", "@types/express@^4.17.21": + version "4.17.23" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" + integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" "@types/serve-static" "*" +"@types/http-errors@*": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== + "@types/http-proxy@^1.17.8": version "1.17.10" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz" @@ -163,10 +189,17 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/mime@*": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" "@types/node@*": version "18.15.11" @@ -183,37 +216,46 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== -"@types/serve-index@^1.9.1": - version "1.9.1" - resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" - integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== +"@types/send@*": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== dependencies: "@types/express" "*" -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.1" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz" - integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== +"@types/serve-static@*", "@types/serve-static@^1.15.5": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== dependencies: - "@types/mime" "*" + "@types/http-errors" "*" "@types/node" "*" + "@types/send" "*" -"@types/sockjs@^0.3.33": - version "0.3.33" - resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz" - integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== +"@types/sockjs@^0.3.36": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== dependencies: "@types/node" "*" -"@types/ws@^8.5.1": - version "8.5.4" - resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz" - integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== +"@types/ws@^8.5.10": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== dependencies: "@types/node" "*" @@ -430,16 +472,6 @@ array-flatten@1.1.1: resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - batch@0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" @@ -468,24 +500,14 @@ body-parser@1.20.3: type-is "~1.6.18" unpipe "1.0.0" -bonjour-service@^1.0.11: - version "1.1.1" - resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz" - integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== +bonjour-service@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722" + integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA== dependencies: - array-flatten "^2.1.2" - dns-equal "^1.0.0" fast-deep-equal "^3.1.3" multicast-dns "^7.2.5" -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - braces@^3.0.2, braces@~3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" @@ -508,6 +530,13 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" @@ -539,10 +568,10 @@ caniuse-lite@^1.0.30001688: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718" integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ== -chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -603,11 +632,6 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - connect-history-api-fallback@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" @@ -675,17 +699,23 @@ debug@^4.1.0: dependencies: ms "2.1.2" -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== depd@2.0.0: version "2.0.0" @@ -714,11 +744,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" - integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== - dns-packet@^5.2.2: version "5.5.0" resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz" @@ -840,22 +865,7 @@ events@^3.2.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -express@^4.17.3: +express@^4.21.2: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== @@ -975,16 +985,6 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== -fs-monkey@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" @@ -1024,11 +1024,6 @@ get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" @@ -1048,18 +1043,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - globby@^13.1.1: version "13.2.2" resolved "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz" @@ -1128,11 +1111,6 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -html-entities@^2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz" - integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== - http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" @@ -1164,10 +1142,10 @@ http-parser-js@>=0.5.1: resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== -http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== +http-proxy-middleware@^2.0.7: + version "2.0.9" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" + integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -1184,10 +1162,10 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== iconv-lite@0.4.24: version "0.4.24" @@ -1209,24 +1187,16 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - inherits@2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== +inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" @@ -1237,10 +1207,10 @@ ipaddr.js@1.9.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== is-binary-path@~2.1.0: version "2.1.0" @@ -1256,10 +1226,10 @@ is-core-module@^2.9.0: dependencies: has "^1.0.3" -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== is-extglob@^2.1.1: version "2.1.1" @@ -1273,6 +1243,18 @@ is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" + integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" @@ -1290,17 +1272,12 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== dependencies: - is-docker "^2.0.0" + is-inside-container "^1.0.0" isarray@~1.0.0: version "1.0.0" @@ -1341,13 +1318,13 @@ kind-of@^6.0.2: resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -launch-editor@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz" - integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== +launch-editor@^2.6.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.10.0.tgz#5ca3edfcb9667df1e8721310f3a40f1127d4bc42" + integrity sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA== dependencies: picocolors "^1.0.0" - shell-quote "^1.7.3" + shell-quote "^1.8.1" loader-runner@^4.2.0: version "4.3.0" @@ -1371,12 +1348,15 @@ media-typer@0.3.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memfs@^3.4.3: - version "3.4.13" - resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz" - integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg== +memfs@^4.6.0: + version "4.17.2" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.17.2.tgz#1f71a6d85c8c53b4f1b388234ed981a690c7e227" + integrity sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg== dependencies: - fs-monkey "^1.0.3" + "@jsonjoy.com/json-pack" "^1.0.3" + "@jsonjoy.com/util" "^1.3.0" + tree-dump "^1.0.1" + tslib "^2.0.0" merge-descriptors@1.0.3: version "1.0.3" @@ -1423,23 +1403,11 @@ mime@1.6.0: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -1488,13 +1456,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - object-inspect@^1.13.3: version "1.13.4" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" @@ -1505,7 +1466,7 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -1517,28 +1478,15 @@ on-headers@~1.0.2: resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== +open@^10.0.3: + version "10.1.2" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.2.tgz#d5df40984755c9a9c3c93df8156a12467e882925" + integrity sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw== dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^8.0.9: - version "8.4.2" - resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" p-limit@^2.2.0: version "2.3.0" @@ -1554,12 +1502,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== +p-retry@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.1.tgz#81828f8dc61c6ef5a800585491572cc9892703af" + integrity sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ== dependencies: - "@types/retry" "0.12.0" + "@types/retry" "0.12.2" + is-network-error "^1.0.0" retry "^0.13.1" p-try@^2.0.0: @@ -1577,12 +1526,7 @@ path-exists@^4.0.0: resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -1753,12 +1697,10 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== run-parallel@^1.1.9: version "1.2.0" @@ -1782,10 +1724,10 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^4.0.0, schema-utils@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" - integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== +schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -1797,11 +1739,12 @@ select-hose@^2.0.0: resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== -selfsigned@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz" - integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== dependencies: + "@types/node-forge" "^1.3.0" node-forge "^1" send@0.19.0: @@ -1889,10 +1832,10 @@ shebang-regex@^3.0.0: resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3: - version "1.8.0" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz" - integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== +shell-quote@^1.8.1: + version "1.8.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== side-channel-list@^1.0.0: version "1.0.0" @@ -1934,11 +1877,6 @@ side-channel@^1.0.6: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - slash@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" @@ -2013,11 +1951,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - supports-color@^8.0.0: version "8.1.1" resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" @@ -2056,6 +1989,11 @@ terser@^5.31.1: commander "^2.20.0" source-map-support "~0.5.20" +thingies@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" + integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== + thunky@^1.0.2: version "1.1.0" resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" @@ -2073,6 +2011,16 @@ toidentifier@1.0.1: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tree-dump@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.3.tgz#2f0e42e77354714418ed7ab44291e435ccdb0f80" + integrity sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg== + +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -2154,52 +2102,51 @@ webpack-cli@^4.9.2: rechoir "^0.7.0" webpack-merge "^5.7.3" -webpack-dev-middleware@^5.3.1: - version "5.3.4" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz" - integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== +webpack-dev-middleware@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz#40e265a3d3d26795585cff8207630d3a8ff05877" + integrity sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA== dependencies: colorette "^2.0.10" - memfs "^3.4.3" + memfs "^4.6.0" mime-types "^2.1.31" + on-finished "^2.4.1" range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.7.4: - version "4.13.2" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.2.tgz" - integrity sha512-5i6TrGBRxG4vnfDpB6qSQGfnB6skGBXNL5/542w2uRGLimX6qeE5BQMLrzIC3JYV/xlGOv+s+hTleI9AZKUQNw== +webpack-dev-server@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz#049072d6e19cbda8cf600b9e364e6662d61218ba" + integrity sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ== dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.1" + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/express-serve-static-core" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" + bonjour-service "^1.2.1" + chokidar "^3.6.0" colorette "^2.0.10" compression "^1.7.4" connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" + express "^4.21.2" graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" + http-proxy-middleware "^2.0.7" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + schema-utils "^4.2.0" + selfsigned "^2.4.1" serve-index "^1.9.1" sockjs "^0.3.24" spdy "^4.0.2" - webpack-dev-middleware "^5.3.1" - ws "^8.13.0" + webpack-dev-middleware "^7.4.2" + ws "^8.18.0" webpack-merge@^5.7.3: version "5.8.0" @@ -2269,12 +2216,7 @@ wildcard@^2.0.0: resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@^8.13.0: - version "8.18.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" - integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== +ws@^8.18.0: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== diff --git a/wasm/zknym-lib/src/vpn_api_client/client.rs b/wasm/zknym-lib/src/vpn_api_client/client.rs index 846ec11b64..e3a7f983b0 100644 --- a/wasm/zknym-lib/src/vpn_api_client/client.rs +++ b/wasm/zknym-lib/src/vpn_api_client/client.rs @@ -8,8 +8,7 @@ use crate::vpn_api_client::types::{ }; use async_trait::async_trait; pub use nym_http_api_client::Client; -use nym_http_api_client::{parse_response, ApiClient, PathSegments, NO_PARAMS}; -use reqwest::IntoUrl; +use nym_http_api_client::{parse_response, ApiClient, IntoUrl, PathSegments, NO_PARAMS}; use serde::de::DeserializeOwned; #[allow(dead_code)] diff --git a/yarn.lock b/yarn.lock index cb6068ed14..581675dc1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,25 +3,17 @@ "@adobe/css-tools@^4.0.1": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" - integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== + version "4.4.4" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.4.tgz#2856c55443d3d461693f32d2b96fb6ea92e1ffa9" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== +"@asamuzakjp/css-color@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794" + integrity sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw== dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@asamuzakjp/css-color@^3.1.1": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.1.2.tgz#4efb1abb3bfbb5982df66bd6e71fea21e3a29fbe" - integrity sha512-nwgc7jPn3LpZ4JWsoHtuwBsad1qSSLDDX634DdG0PBJofIuIEtSWk4KkRmuXyu178tjuHAbwiMNNzwqIyLYxZw== - dependencies: - "@csstools/css-calc" "^2.1.2" - "@csstools/css-color-parser" "^3.0.8" + "@csstools/css-calc" "^2.1.3" + "@csstools/css-color-parser" "^3.0.9" "@csstools/css-parser-algorithms" "^3.0.4" "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" @@ -33,19 +25,19 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.26.2", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-validator-identifier" "^7.27.1" js-tokens "^4.0.0" - picocolors "^1.0.0" + picocolors "^1.1.1" -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.8": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367" - integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== +"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04" + integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw== "@babel/core@7.12.9": version "7.12.9" @@ -70,74 +62,74 @@ source-map "^0.5.0" "@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.15.0", "@babel/core@^7.17.5", "@babel/core@^7.19.6", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.8.0": - version "7.26.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9" - integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496" + integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA== dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.26.10" - "@babel/helper-compilation-targets" "^7.26.5" - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.10" - "@babel/parser" "^7.26.10" - "@babel/template" "^7.26.9" - "@babel/traverse" "^7.26.10" - "@babel/types" "^7.26.10" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.4" + "@babel/types" "^7.28.4" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.26.10", "@babel/generator@^7.27.0", "@babel/generator@^7.7.2": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.0.tgz#764382b5392e5b9aff93cadb190d0745866cbc2c" - integrity sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw== +"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.28.3", "@babel/generator@^7.7.2": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== dependencies: - "@babel/parser" "^7.27.0" - "@babel/types" "^7.27.0" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" - integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== +"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== dependencies: - "@babel/types" "^7.25.9" + "@babel/types" "^7.27.3" -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9", "@babel/helper-compilation-targets@^7.26.5": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz#de0c753b1cd1d9ab55d473c5a5cf7170f0a81880" - integrity sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== dependencies: - "@babel/compat-data" "^7.26.8" - "@babel/helper-validator-option" "^7.25.9" + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.25.9", "@babel/helper-create-class-features-plugin@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz#518fad6a307c6a96f44af14912b2c20abe9bfc30" - integrity sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg== +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz#3e747434ea007910c320c4d39a6b46f20f371d46" + integrity sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/helper-replace-supers" "^7.26.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/traverse" "^7.27.0" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.3" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz#0e41f7d38c2ebe06ebd9cf0e02fb26019c77cd95" - integrity sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" + integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" regexpu-core "^6.2.0" semver "^6.3.1" @@ -155,124 +147,129 @@ resolve "^1.14.2" semver "^6.1.2" -"@babel/helper-define-polyfill-provider@^0.6.3", "@babel/helper-define-polyfill-provider@^0.6.4": - version "0.6.4" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz#15e8746368bfa671785f5926ff74b3064c291fab" - integrity sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw== +"@babel/helper-define-polyfill-provider@^0.6.5": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz#742ccf1cb003c07b48859fc9fa2c1bbe40e5f753" + integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg== dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + debug "^4.4.1" lodash.debounce "^4.0.8" - resolve "^1.14.2" + resolve "^1.22.10" -"@babel/helper-member-expression-to-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" - integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/helper-optimise-call-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" - integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== +"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== dependencies: - "@babel/types" "^7.25.9" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" "@babel/helper-plugin-utils@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.26.5", "@babel/helper-plugin-utils@^7.8.0": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" - integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== -"@babel/helper-remap-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" - integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-wrap-function" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" -"@babel/helper-replace-supers@^7.25.9", "@babel/helper-replace-supers@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d" - integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg== +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== dependencies: - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/traverse" "^7.26.5" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" "@babel/helper-simple-access@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz#6d51783299884a2c74618d6ef0f86820ec2e7739" - integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.27.1.tgz#7f2171e29d95d6283ede286a4994f985cbe07973" + integrity sha512-OU4zVQrJgFBNXMjrHs1yFSdlTgufO4tefcUZoqNhukVfw0p8x1Asht/gcGZ3bpHbi8gu/76m4JhrlKPqkrs/WQ== dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" - integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0", "@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.25.9", "@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== -"@babel/helper-wrap-function@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" - integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== +"@babel/helper-wrap-function@^7.27.1": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz#fe4872092bc1438ffd0ce579e6f699609f9d0a7a" + integrity sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g== dependencies: - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.3" + "@babel/types" "^7.28.2" -"@babel/helpers@^7.12.5", "@babel/helpers@^7.26.10": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" - integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== +"@babel/helpers@^7.12.5", "@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== dependencies: - "@babel/template" "^7.27.0" - "@babel/types" "^7.27.0" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" "@babel/highlight@^7.10.4": version "7.25.9" @@ -284,51 +281,51 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" - integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" + integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== dependencies: - "@babel/types" "^7.27.0" + "@babel/types" "^7.28.4" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" - integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" + integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" - integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" - integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" - integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" - integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz#373f6e2de0016f73caf8f27004f61d167743742a" + integrity sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.3" "@babel/plugin-proposal-class-properties@^7.12.1": version "7.18.6" @@ -339,20 +336,20 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-proposal-decorators@^7.12.12": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz#8680707f943d1a3da2cd66b948179920f097e254" - integrity sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g== + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz#419c8acc31088e05a774344c021800f7ddc39bf0" + integrity sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-syntax-decorators" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-decorators" "^7.27.1" "@babel/plugin-proposal-export-default-from@^7.12.1": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.25.9.tgz#52702be6ef8367fc8f18b8438278332beeb8f87c" - integrity sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz#59b050b0e5fdc366162ab01af4fcbac06ea40919" + integrity sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": version "7.18.6" @@ -442,12 +439,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-decorators@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz#986b4ca8b7b5df3f67cee889cedeffc2e2bf14b3" - integrity sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg== +"@babel/plugin-syntax-decorators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz#ee7dd9590aeebc05f9d4c8c0560007b05979a63d" + integrity sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" @@ -456,26 +453,26 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-flow@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.26.0.tgz#96507595c21b45fccfc2bc758d5c45452e6164fa" - integrity sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg== +"@babel/plugin-syntax-flow@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz#6c83cf0d7d635b716827284b7ecd5aead9237662" + integrity sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-import-assertions@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" - integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== +"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" @@ -498,12 +495,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-jsx@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" - integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== +"@babel/plugin-syntax-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" @@ -561,12 +558,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.25.9", "@babel/plugin-syntax-typescript@^7.7.2": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" - integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== +"@babel/plugin-syntax-typescript@^7.27.1", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -576,535 +573,546 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.12.1", "@babel/plugin-transform-arrow-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" - integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== +"@babel/plugin-transform-arrow-functions@^7.12.1", "@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-async-generator-functions@^7.26.8": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz#5e3991135e3b9c6eaaf5eff56d1ae5a11df45ff8" - integrity sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg== +"@babel/plugin-transform-async-generator-functions@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz#1276e6c7285ab2cd1eccb0bc7356b7a69ff842c2" + integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-remap-async-to-generator" "^7.25.9" - "@babel/traverse" "^7.26.8" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.28.0" -"@babel/plugin-transform-async-to-generator@^7.14.5", "@babel/plugin-transform-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" - integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== +"@babel/plugin-transform-async-to-generator@^7.14.5", "@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" -"@babel/plugin-transform-block-scoped-functions@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz#3dc4405d31ad1cbe45293aa57205a6e3b009d53e" - integrity sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ== +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoping@^7.12.12", "@babel/plugin-transform-block-scoping@^7.25.9": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz#acc2c0d98a7439bbde4244588ddbd4904701d47f" - integrity sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ== +"@babel/plugin-transform-block-scoping@^7.12.12", "@babel/plugin-transform-block-scoping@^7.28.0": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz#e19ac4ddb8b7858bac1fd5c1be98a994d9726410" + integrity sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-class-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" - integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-class-static-block@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" - integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== +"@babel/plugin-transform-class-static-block@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz#d1b8e69b54c9993bc558203e1f49bfc979bfd852" + integrity sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.28.3" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" - integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== +"@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.28.3": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/traverse" "^7.25.9" - globals "^11.1.0" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.28.4" -"@babel/plugin-transform-computed-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" - integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/template" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" -"@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" - integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== +"@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz#0f156588f69c596089b7d5b06f5af83d9aa7f97a" + integrity sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.0" -"@babel/plugin-transform-dotall-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" - integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-keys@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" - integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" - integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-dynamic-import@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" - integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-exponentiation-operator@^7.26.3": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" - integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== +"@babel/plugin-transform-explicit-resource-management@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz#45be6211b778dbf4b9d54c4e8a2b42fa72e09a1a" + integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" -"@babel/plugin-transform-export-namespace-from@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" - integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== +"@babel/plugin-transform-exponentiation-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" + integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-flow-strip-types@^7.25.9": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz#2904c85a814e7abb1f4850b8baf4f07d0a2389d4" - integrity sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ== +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/plugin-syntax-flow" "^7.26.0" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-for-of@^7.12.1", "@babel/plugin-transform-for-of@^7.26.9": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz#27231f79d5170ef33b5111f07fe5cafeb2c96a56" - integrity sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg== +"@babel/plugin-transform-flow-strip-types@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz#5def3e1e7730f008d683144fb79b724f92c5cdf9" + integrity sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-flow" "^7.27.1" -"@babel/plugin-transform-function-name@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" - integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== +"@babel/plugin-transform-for-of@^7.12.1", "@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-json-strings@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" - integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" -"@babel/plugin-transform-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" - integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-logical-assignment-operators@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" - integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-member-expression-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" - integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== +"@babel/plugin-transform-logical-assignment-operators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" + integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-amd@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" - integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-commonjs@^7.26.3": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" - integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== dependencies: - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-systemjs@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" - integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-umd@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" - integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== +"@babel/plugin-transform-modules-systemjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" + integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" -"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" - integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-new-target@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" - integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-nullish-coalescing-operator@^7.26.6": - version "7.26.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz#fbf6b3c92cb509e7b319ee46e3da89c5bedd31fe" - integrity sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw== +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-numeric-separator@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" - integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-object-rest-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" - integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-object-super@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" - integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== +"@babel/plugin-transform-object-rest-spread@^7.28.0": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz#9ee1ceca80b3e6c4bac9247b2149e36958f7f98d" + integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.4" -"@babel/plugin-transform-optional-catch-binding@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" - integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" -"@babel/plugin-transform-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" - integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" - integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== +"@babel/plugin-transform-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-private-methods@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" - integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== +"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-private-property-in-object@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" - integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-property-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" - integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-constant-elements@^7.18.12": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz#08a1de35a301929b60fdf2788a54b46cd8ecd0ef" - integrity sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz#6c6b50424e749a6e48afd14cf7b92f98cb9383f9" + integrity sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-display-name@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz#4b79746b59efa1f38c8695065a92a9f5afb24f7d" - integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== +"@babel/plugin-transform-react-display-name@^7.27.1": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" + integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-jsx-development@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz#8fd220a77dd139c07e25225a903b8be8c829e0d7" - integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== +"@babel/plugin-transform-react-jsx-development@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz#47ff95940e20a3a70e68ad3d4fcb657b647f6c98" + integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== dependencies: - "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.27.1" -"@babel/plugin-transform-react-jsx@^7.12.12", "@babel/plugin-transform-react-jsx@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz#06367940d8325b36edff5e2b9cbe782947ca4166" - integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== +"@babel/plugin-transform-react-jsx@^7.12.12", "@babel/plugin-transform-react-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" + integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-syntax-jsx" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/plugin-transform-react-pure-annotations@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz#ea1c11b2f9dbb8e2d97025f43a3b5bc47e18ae62" - integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== +"@babel/plugin-transform-react-pure-annotations@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz#339f1ce355eae242e0649f232b1c68907c02e879" + integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.25.9": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz#822feebef43d6a59a81f696b2512df5b1682db31" - integrity sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA== +"@babel/plugin-transform-regenerator@^7.28.3": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz#9d3fa3bebb48ddd0091ce5729139cd99c67cea51" + integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - regenerator-transform "^0.15.2" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regexp-modifiers@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" - integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-reserved-words@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" - integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-shorthand-properties@^7.12.1", "@babel/plugin-transform-shorthand-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" - integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== +"@babel/plugin-transform-shorthand-properties@^7.12.1", "@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" - integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== +"@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-sticky-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" - integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-template-literals@^7.12.1", "@babel/plugin-transform-template-literals@^7.26.8": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz#966b15d153a991172a540a69ad5e1845ced990b5" - integrity sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q== +"@babel/plugin-transform-template-literals@^7.12.1", "@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typeof-symbol@^7.26.7": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz#044a0890f3ca694207c7826d0c7a65e5ac008aae" - integrity sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w== +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typescript@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.0.tgz#a29fd3481da85601c7e34091296e9746d2cccba8" - integrity sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg== +"@babel/plugin-transform-typescript@^7.27.1": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz#796cbd249ab56c18168b49e3e1d341b72af04a6b" + integrity sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.27.0" - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-syntax-typescript" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" -"@babel/plugin-transform-unicode-escapes@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" - integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-property-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" - integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" - integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-sets-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" - integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/preset-env@^7.12.11", "@babel/preset-env@^7.15.0", "@babel/preset-env@^7.19.4": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.9.tgz#2ec64e903d0efe743699f77a10bdf7955c2123c3" - integrity sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ== + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.28.3.tgz#2b18d9aff9e69643789057ae4b942b1654f88187" + integrity sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg== dependencies: - "@babel/compat-data" "^7.26.8" - "@babel/helper-compilation-targets" "^7.26.5" - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" + "@babel/compat-data" "^7.28.0" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.3" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.26.0" - "@babel/plugin-syntax-import-attributes" "^7.26.0" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.25.9" - "@babel/plugin-transform-async-generator-functions" "^7.26.8" - "@babel/plugin-transform-async-to-generator" "^7.25.9" - "@babel/plugin-transform-block-scoped-functions" "^7.26.5" - "@babel/plugin-transform-block-scoping" "^7.25.9" - "@babel/plugin-transform-class-properties" "^7.25.9" - "@babel/plugin-transform-class-static-block" "^7.26.0" - "@babel/plugin-transform-classes" "^7.25.9" - "@babel/plugin-transform-computed-properties" "^7.25.9" - "@babel/plugin-transform-destructuring" "^7.25.9" - "@babel/plugin-transform-dotall-regex" "^7.25.9" - "@babel/plugin-transform-duplicate-keys" "^7.25.9" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-dynamic-import" "^7.25.9" - "@babel/plugin-transform-exponentiation-operator" "^7.26.3" - "@babel/plugin-transform-export-namespace-from" "^7.25.9" - "@babel/plugin-transform-for-of" "^7.26.9" - "@babel/plugin-transform-function-name" "^7.25.9" - "@babel/plugin-transform-json-strings" "^7.25.9" - "@babel/plugin-transform-literals" "^7.25.9" - "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" - "@babel/plugin-transform-member-expression-literals" "^7.25.9" - "@babel/plugin-transform-modules-amd" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.26.3" - "@babel/plugin-transform-modules-systemjs" "^7.25.9" - "@babel/plugin-transform-modules-umd" "^7.25.9" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-new-target" "^7.25.9" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.26.6" - "@babel/plugin-transform-numeric-separator" "^7.25.9" - "@babel/plugin-transform-object-rest-spread" "^7.25.9" - "@babel/plugin-transform-object-super" "^7.25.9" - "@babel/plugin-transform-optional-catch-binding" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" - "@babel/plugin-transform-private-methods" "^7.25.9" - "@babel/plugin-transform-private-property-in-object" "^7.25.9" - "@babel/plugin-transform-property-literals" "^7.25.9" - "@babel/plugin-transform-regenerator" "^7.25.9" - "@babel/plugin-transform-regexp-modifiers" "^7.26.0" - "@babel/plugin-transform-reserved-words" "^7.25.9" - "@babel/plugin-transform-shorthand-properties" "^7.25.9" - "@babel/plugin-transform-spread" "^7.25.9" - "@babel/plugin-transform-sticky-regex" "^7.25.9" - "@babel/plugin-transform-template-literals" "^7.26.8" - "@babel/plugin-transform-typeof-symbol" "^7.26.7" - "@babel/plugin-transform-unicode-escapes" "^7.25.9" - "@babel/plugin-transform-unicode-property-regex" "^7.25.9" - "@babel/plugin-transform-unicode-regex" "^7.25.9" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.28.0" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.0" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.28.3" + "@babel/plugin-transform-classes" "^7.28.3" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-explicit-resource-management" "^7.28.0" + "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.28.0" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.28.3" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.11.0" - babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.40.0" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + core-js-compat "^3.43.0" semver "^6.3.1" "@babel/preset-flow@^7.12.1": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.25.9.tgz#ef8b5e7e3f24a42b3711e77fb14919b87dffed0a" - integrity sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.27.1.tgz#3050ed7c619e8c4bfd0e0eeee87a2fa86a4bb1c6" + integrity sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-transform-flow-strip-types" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-flow-strip-types" "^7.27.1" "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" @@ -1116,32 +1124,32 @@ esutils "^2.0.2" "@babel/preset-react@^7.12.10", "@babel/preset-react@^7.14.5", "@babel/preset-react@^7.18.6": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" - integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.27.1.tgz#86ea0a5ca3984663f744be2fd26cb6747c3fd0ec" + integrity sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-transform-react-display-name" "^7.25.9" - "@babel/plugin-transform-react-jsx" "^7.25.9" - "@babel/plugin-transform-react-jsx-development" "^7.25.9" - "@babel/plugin-transform-react-pure-annotations" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.27.1" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" "@babel/preset-typescript@^7.12.7", "@babel/preset-typescript@^7.15.0", "@babel/preset-typescript@^7.18.6": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.0.tgz#4dcb8827225975f4290961b0b089f9c694ca50c7" - integrity sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ== + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" + integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== dependencies: - "@babel/helper-plugin-utils" "^7.26.5" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-syntax-jsx" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.26.3" - "@babel/plugin-transform-typescript" "^7.27.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.27.1" "@babel/register@^7.12.1": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.25.9.tgz#1c465acf7dc983d70ccc318eb5b887ecb04f021b" - integrity sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA== + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.28.3.tgz#abd8a3753480c799bdaf9c9092d6745d16e052c2" + integrity sha512-CieDOtd8u208eI49bYl4z1J22ySFw87IGwE+IswFEExH7e3rLgKb0WNQeumnacQ1+VoDJLYI5QFA3AJZuyZQfA== dependencies: clone-deep "^4.0.1" find-cache-dir "^2.0.0" @@ -1156,12 +1164,10 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.0", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.27.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" - integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== - dependencies: - regenerator-runtime "^0.14.0" +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.27.6", "@babel/runtime@^7.28.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== "@babel/runtime@~7.5.4": version "7.5.5" @@ -1170,35 +1176,35 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.12.7", "@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0", "@babel/template@^7.3.3": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" - integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== +"@babel/template@^7.12.7", "@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.3.3": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/parser" "^7.27.0" - "@babel/types" "^7.27.0" + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" -"@babel/traverse@^7.1.6", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.27.0", "@babel/traverse@^7.7.2": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70" - integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA== +"@babel/traverse@^7.1.6", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.7.2": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b" + integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.27.0" - "@babel/parser" "^7.27.0" - "@babel/template" "^7.27.0" - "@babel/types" "^7.27.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" debug "^4.3.1" - globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" - integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== +"@babel/types@^7.0.0", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.2.0", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" "@base2/pretty-print-object@1.0.1": version "1.0.1" @@ -1265,21 +1271,21 @@ integrity sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA== "@chain-registry/client@^1.49.11": - version "1.53.116" - resolved "https://registry.yarnpkg.com/@chain-registry/client/-/client-1.53.116.tgz#50db8379b622cb34a36721eecdf2a48bf5ce414c" - integrity sha512-caWptWLpxRcykfqHWIBR1RBnOzjycb9sVy8lMbm4GPNrEmTr3HLsfYetVUSpNtaObni+SJHr8CKhWX8Lb5ffgQ== + version "1.53.207" + resolved "https://registry.yarnpkg.com/@chain-registry/client/-/client-1.53.207.tgz#ff13747aeaa9f05d9ece5e6e79b88a7e8168b3e8" + integrity sha512-zKpbnQPEr4MHRc4srXemQE/AxDNsYDK3XLAFD0zDIznMKZuTKYV7V+OSGmyCwA3CsP4CIuTRDiPnk5Ky0YY6yw== dependencies: - "@chain-registry/types" "^0.50.116" - "@chain-registry/utils" "^1.51.116" + "@chain-registry/types" "^0.50.207" + "@chain-registry/utils" "^1.51.207" bfs-path "^1.0.2" cross-fetch "^3.1.5" "@chain-registry/keplr@^1.69.13": - version "1.74.185" - resolved "https://registry.yarnpkg.com/@chain-registry/keplr/-/keplr-1.74.185.tgz#705019f50a18a4ba841afc4157d648a589504138" - integrity sha512-mds9rNbgZn5TsOQNmQ66YvP3Gk5+pmaZwLnwcktvNtzHYFD9yWD8zU+iz5vqR2FpGbhqflnOXMmsLKq93UXeqA== + version "1.74.334" + resolved "https://registry.yarnpkg.com/@chain-registry/keplr/-/keplr-1.74.334.tgz#10812ba94de1e8720100df23540b830d6ce6d054" + integrity sha512-oQwQVpybax3wknFy9A+JhhPK/KJCwQtWrrK53hqkP6HxGJqMOCKdxVCUOgc+SMEwiZhQQPBt1+ocPgfSYd0VYw== dependencies: - "@chain-registry/types" "^0.50.116" + "@chain-registry/types" "^0.50.207" "@keplr-wallet/cosmos" "0.12.28" "@keplr-wallet/crypto" "0.12.28" semver "^7.5.0" @@ -1289,17 +1295,17 @@ resolved "https://registry.yarnpkg.com/@chain-registry/types/-/types-0.46.15.tgz#f4c0219fb7060d97cb224b55f349adb1d436aac9" integrity sha512-gzf+pkAbEZ7fKuTuwmrEAEcx1K/BNrKuCnB9s+WwSo9Ad/3s+GV5LOXcOOxjjHh9Mrs9kvnxKvzKjOwWu8gDJw== -"@chain-registry/types@^0.50.116", "@chain-registry/types@^0.50.36": - version "0.50.116" - resolved "https://registry.yarnpkg.com/@chain-registry/types/-/types-0.50.116.tgz#c0170772ca573d0b855006a15922d59e7c6c49ef" - integrity sha512-tlhFIEiRxS+lpm1Q+zX8O9lPQ8c2/MmpDp9CCoJk+vuxjxQml/OfNyKrvg117kBaTA5i1ADCvY3N0Lo2R/6vYQ== +"@chain-registry/types@^0.50.207", "@chain-registry/types@^0.50.36": + version "0.50.207" + resolved "https://registry.yarnpkg.com/@chain-registry/types/-/types-0.50.207.tgz#e8c2f1fa128479155ec242aaee3c04c174cde6bc" + integrity sha512-f4/XMrW/SaWCGE50skmq10qm3+LaCbrSgGmpeeirSCa1G0DENetGz89cl/CBJ7DoPZUEP2VElhSBm9usopKGOg== -"@chain-registry/utils@^1.51.116": - version "1.51.116" - resolved "https://registry.yarnpkg.com/@chain-registry/utils/-/utils-1.51.116.tgz#de7a54272c64c1677620204a822b9754b169d8ed" - integrity sha512-E1O4oYcL2AMawLtBpQim1BkMA8B0xGUGBH3uvLO4Nap60gkeSMI+lFTW6aiFkfwbvTiim3ZdUri/EayNunJY6Q== +"@chain-registry/utils@^1.51.207": + version "1.51.207" + resolved "https://registry.yarnpkg.com/@chain-registry/utils/-/utils-1.51.207.tgz#d508fdd6853e58e7e18effa239cd9b95ed522163" + integrity sha512-fsutE0CwVy8MrPFiXw1DKwm8JaX2zGflJhP+kjFl9OZeq2pavx+DBJXABCjiugQl/aGcaYle9kPkv3qtBBBbnw== dependencies: - "@chain-registry/types" "^0.50.116" + "@chain-registry/types" "^0.50.207" bignumber.js "9.1.2" sha.js "^2.4.11" @@ -1334,7 +1340,7 @@ "@cosmjs/math" "^0.32.4" "@cosmjs/utils" "^0.32.4" -"@cosmjs/cosmwasm-stargate@^0.25.5", "@cosmjs/cosmwasm-stargate@^0.29.5", "@cosmjs/cosmwasm-stargate@^0.32.3", "@cosmjs/cosmwasm-stargate@^0.32.4": +"@cosmjs/cosmwasm-stargate@^0.29.5", "@cosmjs/cosmwasm-stargate@^0.32.3", "@cosmjs/cosmwasm-stargate@^0.32.4": version "0.32.4" resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.32.4.tgz#2ee93f2cc0b1c146ac369b2bf8ef9ee2e159fd50" integrity sha512-Fuo9BGEiB+POJ5WeRyBGuhyKR1ordvxZGLPuPosFJOH9U0gKMgcjwKMCgAlWFkMlHaTB+tNdA8AifWiHrI7VgA== @@ -1410,13 +1416,6 @@ "@cosmjs/stream" "^0.32.4" xstream "^11.14.0" -"@cosmjs/math@^0.25.5": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.25.6.tgz#25c7b106aaded889a5b80784693caa9e654b0c28" - integrity sha512-Fmyc9FJ8KMU34n7rdapMJrT/8rx5WhMw2F7WLBu7AVLcBh0yWsXIcMSJCoPHTOnMIiABjXsnrrwEaLrOOBfu6A== - dependencies: - bn.js "^4.11.8" - "@cosmjs/math@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.29.5.tgz#722c96e080d6c2b62215ce9f4c70da7625b241b6" @@ -1431,7 +1430,7 @@ dependencies: bn.js "^5.2.0" -"@cosmjs/proto-signing@^0.25.5", "@cosmjs/proto-signing@^0.29.5", "@cosmjs/proto-signing@^0.32.3", "@cosmjs/proto-signing@^0.32.4": +"@cosmjs/proto-signing@^0.29.5", "@cosmjs/proto-signing@^0.32.3", "@cosmjs/proto-signing@^0.32.4": version "0.32.4" resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.32.4.tgz#5a06e087c6d677439c8c9b25b5223d5e72c4cd93" integrity sha512-QdyQDbezvdRI4xxSlyM1rSVBO2st5sqtbEIl3IX03uJ7YiZIQHyv6vaHVf1V4mapusCqguiHJzm4N4gsFdLBbQ== @@ -1463,7 +1462,7 @@ ws "^7" xstream "^11.14.0" -"@cosmjs/stargate@^0.25.5", "@cosmjs/stargate@^0.29.5", "@cosmjs/stargate@^0.32.3", "@cosmjs/stargate@^0.32.4": +"@cosmjs/stargate@^0.29.5", "@cosmjs/stargate@^0.32.3", "@cosmjs/stargate@^0.32.4": version "0.32.4" resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.32.4.tgz#bd0e4d3bf613b629addbf5f875d3d3b50f640af1" integrity sha512-usj08LxBSsPRq9sbpCeVdyLx2guEcOHfJS9mHGCLCXpdAPEIEQEtWLDpEUc0LEhWOx6+k/ChXTc5NpFkdrtGUQ== @@ -1535,10 +1534,10 @@ resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.32.4.tgz#a9a717c9fd7b1984d9cefdd0ef6c6f254060c671" integrity sha512-D1Yc+Zy8oL/hkUkFUL/bwxvuDBzRGpc4cF7/SkdhxX4iHpSLgdOuTt1mhCh9+kl6NQREy9t7SYZ6xeW5gFe60w== -"@cosmos-kit/core@^2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@cosmos-kit/core/-/core-2.16.0.tgz#19a393cad477183014e919690ce409ce32c7decf" - integrity sha512-bhN3Fs4TcAoasdYMVaYHbpJWo8ZUhmCRc49t1Nc5UJHHu9VGs9NSnSk2NoprLA+kciIt+Y+RUXZLnt588nkG3Q== +"@cosmos-kit/core@^2.16.3": + version "2.16.3" + resolved "https://registry.yarnpkg.com/@cosmos-kit/core/-/core-2.16.3.tgz#587e6b3be8214402a57ea13292af6e43c0ac7fdd" + integrity sha512-tmLJYlE/94iv69Uz6HqPm9GUZSNW+9Ftiq3rXXMcQTVBS64dASJlS4PEWN7QZNr7nsYxQcyzlYxzsrc9g13Dqg== dependencies: "@chain-registry/client" "^1.49.11" "@chain-registry/keplr" "^1.69.13" @@ -1547,7 +1546,7 @@ "@cosmjs/cosmwasm-stargate" "^0.32.3" "@cosmjs/proto-signing" "^0.32.3" "@cosmjs/stargate" "^0.32.3" - "@dao-dao/cosmiframe" "^1.0.0-rc.1" + "@dao-dao/cosmiframe" "^1.0.0" "@walletconnect/types" "2.11.0" bowser "2.11.0" cosmjs-types "^0.9.0" @@ -1556,73 +1555,66 @@ uuid "^9.0.1" "@cosmos-kit/keplr-extension@^2.14.0": - version "2.15.0" - resolved "https://registry.yarnpkg.com/@cosmos-kit/keplr-extension/-/keplr-extension-2.15.0.tgz#a674d8a657a182ef1227498db89c93d52a9c13dc" - integrity sha512-jaB2mEfy4bamnZL+RYonfrsAecoVNrk1e+PsTxPNNJh8EFAohRTY6OG4Q6ZZGy8zIQuUNTGdFTIt2T03o7o0ig== + version "2.15.3" + resolved "https://registry.yarnpkg.com/@cosmos-kit/keplr-extension/-/keplr-extension-2.15.3.tgz#8325d77f99904008430f7e7a0712c6019b6efa1a" + integrity sha512-nKCJQIGVMHI+ZTEmeWsrQ89O0uMAfqEVJxmkaMWPJSgUmUOSso8miylo7oStHBqq3JAv+i7sZShei2lGRllNAA== dependencies: "@chain-registry/keplr" "^1.69.13" - "@cosmos-kit/core" "^2.16.0" + "@cosmos-kit/core" "^2.16.3" "@keplr-wallet/provider-extension" "^0.12.95" "@keplr-wallet/types" "^0.12.95" -"@cosmos-kit/react-lite@^2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@cosmos-kit/react-lite/-/react-lite-2.16.0.tgz#f7eb23930965a242209e6480c89689f3bd11eed5" - integrity sha512-vXPHtGZwGZn5DwBllPNnnSlwuFRliLuqqAHgGzAkMR9FpyjTQzUrkJRsl5Ur8KTvJiO6CqR2//fU2b1uZirw5A== +"@cosmos-kit/react-lite@^2.16.3": + version "2.16.3" + resolved "https://registry.yarnpkg.com/@cosmos-kit/react-lite/-/react-lite-2.16.3.tgz#f1e214a582825424cefcecbf77a8d324f463ba29" + integrity sha512-v7rQvLZjjHtEL7eHwvoRpnXPS+dlmFXFaKJkX2HX1Xqo1gD3tQ+ULBxWLRX4fZiFAdbC1dTnJmP5e7UpEs6hhQ== dependencies: "@chain-registry/types" "^0.46.11" - "@cosmos-kit/core" "^2.16.0" - "@dao-dao/cosmiframe" "^1.0.0-rc.1" + "@cosmos-kit/core" "^2.16.3" + "@dao-dao/cosmiframe" "^1.0.0" "@cosmos-kit/react@^2.20.1": - version "2.22.0" - resolved "https://registry.yarnpkg.com/@cosmos-kit/react/-/react-2.22.0.tgz#831980d3322e121c64f88c80b9e4593c0c5a9d93" - integrity sha512-boTQT4hMUYlcfCk9BqlbGA52T+OXU9djFlIYFyp6vryOWLpy53Jx0sTwsvViOzrMtcpSJGBnw1Zil1cneexu+A== + version "2.22.3" + resolved "https://registry.yarnpkg.com/@cosmos-kit/react/-/react-2.22.3.tgz#521ba3f7347f00d4d8c8e2642e395df4571022e2" + integrity sha512-rcSTmXzjGzDElA5ZsRnHs7CXZUu34ssRudw/Q3TgrqwWBmRTCWLPy+E5/6+y5Dz3vriVMxBUGE4u3RW+MDjiSg== dependencies: "@chain-registry/types" "^0.46.11" - "@cosmos-kit/core" "^2.16.0" - "@cosmos-kit/react-lite" "^2.16.0" + "@cosmos-kit/core" "^2.16.3" + "@cosmos-kit/react-lite" "^2.16.3" "@react-icons/all-files" "^4.1.0" -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== +"@csstools/color-helpers@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz#106c54c808cabfd1ab4c602d8505ee584c2996ef" + integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== + +"@csstools/css-calc@^2.1.3", "@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.0.9": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz#4e386af3a99dd36c46fef013cfe4c1c341eed6f0" + integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@csstools/color-helpers@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8" - integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA== - -"@csstools/css-calc@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.2.tgz#bffd55f002dab119b76d4023f95cd943e6c8c11e" - integrity sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw== - -"@csstools/css-color-parser@^3.0.8": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz#5fe9322920851450bf5e065c2b0e731b9e165394" - integrity sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ== - dependencies: - "@csstools/color-helpers" "^5.0.2" - "@csstools/css-calc" "^2.1.2" + "@csstools/color-helpers" "^5.1.0" + "@csstools/css-calc" "^2.1.4" "@csstools/css-parser-algorithms@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356" - integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== "@csstools/css-tokenizer@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" - integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== -"@dao-dao/cosmiframe@^1.0.0-rc.1": - version "1.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@dao-dao/cosmiframe/-/cosmiframe-1.0.0-rc.1.tgz#51d4d1801605b8abfd01ae0425c797b6b876991e" - integrity sha512-XftJPwbqgS1RWg9SUNBI58vTGnjINZ995OYRwDuUZ6CsuENS2DifAzqgzDgdac+0o5Hqvy/5Aq+HN1Jl/dptTw== +"@dao-dao/cosmiframe@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@dao-dao/cosmiframe/-/cosmiframe-1.0.0.tgz#c780a2c3f14560ccd0f178616567328ca0bc9fcf" + integrity sha512-xPaD9MV1tUueYl+VIJD7CEi4+MCdcgo4E1R9w8XplKlM7pvmBaSBeTz88HwITpKxWKI+DWoJngeCWu5/8oqLJQ== dependencies: uuid "^9.0.1" @@ -1677,25 +1669,25 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@emnapi/core@^1.4.0": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.3.tgz#9ac52d2d5aea958f67e52c40a065f51de59b77d6" - integrity sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g== +"@emnapi/core@^1.4.3": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0" + integrity sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg== dependencies: - "@emnapi/wasi-threads" "1.0.2" + "@emnapi/wasi-threads" "1.1.0" tslib "^2.4.0" -"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.0": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d" - integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ== +"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.3": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73" + integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ== dependencies: tslib "^2.4.0" -"@emnapi/wasi-threads@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz#977f44f844eac7d6c138a415a123818c655f874c" - integrity sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA== +"@emnapi/wasi-threads@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== dependencies: tslib "^2.4.0" @@ -1733,9 +1725,9 @@ integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== "@emotion/is-prop-valid@^1.3.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz#8d5cf1132f836d7adbe42cf0b49df7816fc88240" - integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== + version "1.4.0" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz#e9ad47adff0b5c94c72db3669ce46de33edf28c0" + integrity sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw== dependencies: "@emotion/memoize" "^0.9.0" @@ -1775,9 +1767,9 @@ integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== "@emotion/styled@^11.13.5", "@emotion/styled@^11.6.0": - version "11.14.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.14.0.tgz#f47ca7219b1a295186d7661583376fcea95f0ff3" - integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== + version "11.14.1" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.14.1.tgz#8c34bed2948e83e1980370305614c20955aacd1c" + integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw== dependencies: "@babel/runtime" "^7.18.3" "@emotion/babel-plugin" "^11.13.5" @@ -1806,10 +1798,10 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz#bfe67b3d334a8579a35e48fe240dc0638d1bcd91" - integrity sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" @@ -1914,27 +1906,27 @@ rollup-plugin-dts "^5.0.0" typescript "^4.8.4" -"@floating-ui/core@^1.6.0", "@floating-ui/core@^1.6.7": - version "1.6.9" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6" - integrity sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw== +"@floating-ui/core@^1.6.7", "@floating-ui/core@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== dependencies: - "@floating-ui/utils" "^0.2.9" + "@floating-ui/utils" "^0.2.10" -"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.6.1", "@floating-ui/dom@^1.6.10": - version "1.6.13" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34" - integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w== +"@floating-ui/dom@^1.6.10", "@floating-ui/dom@^1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77" + integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA== dependencies: - "@floating-ui/core" "^1.6.0" - "@floating-ui/utils" "^0.2.9" + "@floating-ui/core" "^1.7.3" + "@floating-ui/utils" "^0.2.10" "@floating-ui/react-dom@^2.0.8", "@floating-ui/react-dom@^2.1.1", "@floating-ui/react-dom@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" - integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== + version "2.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231" + integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw== dependencies: - "@floating-ui/dom" "^1.0.0" + "@floating-ui/dom" "^1.7.4" "@floating-ui/react@^0.26.23": version "0.26.28" @@ -1945,10 +1937,10 @@ "@floating-ui/utils" "^0.2.8" tabbable "^6.0.0" -"@floating-ui/utils@^0.2.7", "@floating-ui/utils@^0.2.8", "@floating-ui/utils@^0.2.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" - integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== +"@floating-ui/utils@^0.2.10", "@floating-ui/utils@^0.2.7", "@floating-ui/utils@^0.2.8": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== "@formatjs/ecma402-abstract@2.3.4": version "2.3.4" @@ -1992,9 +1984,9 @@ tslib "^2.8.0" "@formkit/auto-animate@^0.8.2": - version "0.8.2" - resolved "https://registry.yarnpkg.com/@formkit/auto-animate/-/auto-animate-0.8.2.tgz#24a56e816159fd5585405dcee6bb992f81c27585" - integrity sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ== + version "0.8.4" + resolved "https://registry.yarnpkg.com/@formkit/auto-animate/-/auto-animate-0.8.4.tgz#2c857102bbf612c0e7bd4a0aac0e05a7f0efd8e7" + integrity sha512-DHHC01EJ1p70Q0z/ZFRBIY8NDnmfKccQoyoM84Tgb6omLMat6jivCdf272Y8k3nf4Lzdin/Y4R9q8uFtU0GbnA== "@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" @@ -2063,13 +2055,6 @@ optionalDependencies: "@img/sharp-libvips-darwin-arm64" "1.0.4" -"@img/sharp-darwin-arm64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz#e79a4756bea9a06a7aadb4391ee53cb154a4968c" - integrity sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.1.0" - "@img/sharp-darwin-x64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" @@ -2077,98 +2062,46 @@ optionalDependencies: "@img/sharp-libvips-darwin-x64" "1.0.4" -"@img/sharp-darwin-x64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz#f1f1d386719f6933796415d84937502b7199a744" - integrity sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.1.0" - "@img/sharp-libvips-darwin-arm64@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== -"@img/sharp-libvips-darwin-arm64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz#843f7c09c7245dc0d3cfec2b3c83bb08799a704f" - integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA== - "@img/sharp-libvips-darwin-x64@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== -"@img/sharp-libvips-darwin-x64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz#1239c24426c06a8e833815562f78047a3bfbaaf8" - integrity sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ== - "@img/sharp-libvips-linux-arm64@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== -"@img/sharp-libvips-linux-arm64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz#20d276cefd903ee483f0441ba35961679c286315" - integrity sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew== - "@img/sharp-libvips-linux-arm@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== -"@img/sharp-libvips-linux-arm@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz#067c0b566eae8063738cf1b1db8f8a8573b5465c" - integrity sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA== - -"@img/sharp-libvips-linux-ppc64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz#682334595f2ca00e0a07a675ba170af165162802" - integrity sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ== - "@img/sharp-libvips-linux-s390x@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== -"@img/sharp-libvips-linux-s390x@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz#82fcd68444b3666384235279c145c2b28d8ee302" - integrity sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA== - "@img/sharp-libvips-linux-x64@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== -"@img/sharp-libvips-linux-x64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz#65b2b908bf47156b0724fde9095676c83a18cf5a" - integrity sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q== - "@img/sharp-libvips-linuxmusl-arm64@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== -"@img/sharp-libvips-linuxmusl-arm64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz#72accf924e80b081c8db83b900b444a67c203f01" - integrity sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w== - "@img/sharp-libvips-linuxmusl-x64@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== -"@img/sharp-libvips-linuxmusl-x64@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz#1fa052737e203f46bf44192acd01f9faf11522d7" - integrity sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A== - "@img/sharp-linux-arm64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" @@ -2176,13 +2109,6 @@ optionalDependencies: "@img/sharp-libvips-linux-arm64" "1.0.4" -"@img/sharp-linux-arm64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz#c36ef964499b8cfc2d2ed88fe68f27ce41522c80" - integrity sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.1.0" - "@img/sharp-linux-arm@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" @@ -2190,13 +2116,6 @@ optionalDependencies: "@img/sharp-libvips-linux-arm" "1.0.5" -"@img/sharp-linux-arm@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz#c96e38ff028d645912bb0aa132a7178b96997866" - integrity sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.1.0" - "@img/sharp-linux-s390x@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" @@ -2204,13 +2123,6 @@ optionalDependencies: "@img/sharp-libvips-linux-s390x" "1.0.4" -"@img/sharp-linux-s390x@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz#8ac58d9a49dcb08215e76c8d450717979b7815c3" - integrity sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.1.0" - "@img/sharp-linux-x64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" @@ -2218,13 +2130,6 @@ optionalDependencies: "@img/sharp-libvips-linux-x64" "1.0.4" -"@img/sharp-linux-x64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz#3d8652efac635f0dba39d5e3b8b49515a2b2dee1" - integrity sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA== - optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.1.0" - "@img/sharp-linuxmusl-arm64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" @@ -2232,13 +2137,6 @@ optionalDependencies: "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" -"@img/sharp-linuxmusl-arm64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz#b267e6a3e06f9e4d345cde471e5480c5c39e6969" - integrity sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.1.0" - "@img/sharp-linuxmusl-x64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" @@ -2246,13 +2144,6 @@ optionalDependencies: "@img/sharp-libvips-linuxmusl-x64" "1.0.4" -"@img/sharp-linuxmusl-x64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz#a8dee4b6227f348c4bbacaa6ac3dc584a1a80391" - integrity sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.1.0" - "@img/sharp-wasm32@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" @@ -2260,32 +2151,23 @@ dependencies: "@emnapi/runtime" "^1.2.0" -"@img/sharp-wasm32@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz#f7dfd66b6c231269042d3d8750c90f28b9ddcba1" - integrity sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg== - dependencies: - "@emnapi/runtime" "^1.4.0" - "@img/sharp-win32-ia32@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== -"@img/sharp-win32-ia32@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz#4bc293705df76a5f0a02df66ca3dc12e88f61332" - integrity sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw== - "@img/sharp-win32-x64@0.33.5": version "0.33.5" resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== -"@img/sharp-win32-x64@0.34.1": - version "0.34.1" - resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz#8a7922fec949f037c204c79f6b83238d2482384b" - integrity sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw== +"@inquirer/external-editor@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-1.0.1.tgz#ab0a82c5719a963fb469021cde5cd2b74fea30f8" + integrity sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q== + dependencies: + chardet "^2.1.0" + iconv-lite "^0.6.3" "@interchain-ui/react@^1.26.1": version "1.26.3" @@ -2319,35 +2201,47 @@ react-stately "^3.32.2" zustand "^4.5.5" -"@internationalized/date@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.8.0.tgz#24fb301029224351381aa87cba853ca1093af094" - integrity sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw== +"@internationalized/date@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.9.0.tgz#cf241989b5dd07a2a9f1c91aabd2ad93968a0cc3" + integrity sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg== dependencies: "@swc/helpers" "^0.5.0" -"@internationalized/message@^3.1.7": - version "3.1.7" - resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.1.7.tgz#bf5d3332a685d946949bfb7447aa212bbe44ad5d" - integrity sha512-gLQlhEW4iO7DEFPf/U7IrIdA3UyLGS0opeqouaFwlMObLUzwexRjbygONHDVbC9G9oFLXsLyGKYkJwqXw/QADg== +"@internationalized/message@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@internationalized/message/-/message-3.1.8.tgz#7181e8178f0868535f4507a573bf285e925832cb" + integrity sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA== dependencies: "@swc/helpers" "^0.5.0" intl-messageformat "^10.1.0" -"@internationalized/number@^3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.1.tgz#7c13cc55eb546aa3d42b8d5e7ac7db69a082fec7" - integrity sha512-UVsb4bCwbL944E0SX50CHFtWEeZ2uB5VozZ5yDXJdq6iPZsZO5p+bjVMZh2GxHf4Bs/7xtDCcPwEa2NU9DaG/g== +"@internationalized/number@^3.6.5": + version "3.6.5" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.5.tgz#1103f2832ca8d9dd3e4eecf95733d497791dbbbe" + integrity sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g== dependencies: "@swc/helpers" "^0.5.0" -"@internationalized/string@^3.2.6": - version "3.2.6" - resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.6.tgz#dc46f771aeb63a3f1823e060270c4cce8ad44d37" - integrity sha512-LR2lnM4urJta5/wYJVV7m8qk5DrMZmLRTuFhbQO5b9/sKLHgty6unQy1Li4+Su2DWydmB4aZdS5uxBRXIq2aAw== +"@internationalized/string@^3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.7.tgz#76ae10f1e6e1fdaec7d0028a3f807d37a71bd2dd" + integrity sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A== dependencies: "@swc/helpers" "^0.5.0" +"@isaacs/balanced-match@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" + integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== + +"@isaacs/brace-expansion@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" + integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== + dependencies: + "@isaacs/balanced-match" "^4.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -2422,6 +2316,11 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/diff-sequences@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" + integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== + "@jest/environment@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" @@ -2432,6 +2331,13 @@ "@types/node" "*" jest-mock "^27.5.1" +"@jest/expect-utils@30.1.2": + version "30.1.2" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.1.2.tgz#88ea18040f707c9fadb6fd9e77568cae5266cee8" + integrity sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A== + dependencies: + "@jest/get-type" "30.1.0" + "@jest/expect-utils@^28.1.3": version "28.1.3" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.3.tgz#58561ce5db7cd253a7edddbc051fb39dda50f525" @@ -2439,13 +2345,6 @@ dependencies: jest-get-type "^28.0.2" -"@jest/expect-utils@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" - integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== - dependencies: - jest-get-type "^29.6.3" - "@jest/fake-timers@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" @@ -2458,6 +2357,11 @@ jest-mock "^27.5.1" jest-util "^27.5.1" +"@jest/get-type@30.1.0": + version "30.1.0" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" + integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== + "@jest/globals@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" @@ -2467,6 +2371,14 @@ "@jest/types" "^27.5.1" expect "^27.5.1" +"@jest/pattern@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== + dependencies: + "@types/node" "*" + jest-regex-util "30.0.1" + "@jest/reporters@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" @@ -2498,6 +2410,13 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" +"@jest/schemas@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== + dependencies: + "@sinclair/typebox" "^0.34.0" + "@jest/schemas@^28.1.3": version "28.1.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" @@ -2583,6 +2502,19 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.0.5.tgz#29a33a4c036e3904f1cfd94f6fe77f89d2e1cc05" + integrity sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ== + dependencies: + "@jest/pattern" "30.0.1" + "@jest/schemas" "30.0.5" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -2617,62 +2549,44 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.8" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" - integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - "@jridgewell/source-map@^0.3.3": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== dependencies: "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.30" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" + integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -2728,23 +2642,24 @@ protobufjs "^6.11.2" "@keplr-wallet/provider-extension@^0.12.95": - version "0.12.220" - resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-extension/-/provider-extension-0.12.220.tgz#024c738b47e08f44171adbd6415999fe8c81e028" - integrity sha512-TeHFUQVv/mwqJwDneRYLWpbrBzO1tFTmAEAxWCOIHPk8zDW/eyLybTSCRZidPLuNAwKdKH4YlrndEtSdkPLWbQ== + version "0.12.271" + resolved "https://registry.yarnpkg.com/@keplr-wallet/provider-extension/-/provider-extension-0.12.271.tgz#909ab908dfbaaf017b28b3403ed96751507ba784" + integrity sha512-cYXSn+nmv8fCo/2jqMrNXer5sFKbw5VrMjWMaEm3zodKQz6wYPQXZDVVhqgU56XNlGVoAU7sXvDeRuOi7+9Ykg== dependencies: - "@keplr-wallet/types" "0.12.220" + "@keplr-wallet/types" "0.12.271" deepmerge "^4.2.2" long "^4.0.0" + uuid "^11.1.0" "@keplr-wallet/simple-fetch@0.12.28": version "0.12.28" resolved "https://registry.yarnpkg.com/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.28.tgz#44225df5b329c823076280df1ec9930a21b1373e" integrity sha512-T2CiKS2B5n0ZA7CWw0CA6qIAH0XYI1siE50MP+i+V0ZniCGBeL+BMcDw64vFJUcEH+1L5X4sDAzV37fQxGwllA== -"@keplr-wallet/types@0.12.220", "@keplr-wallet/types@^0.12.211", "@keplr-wallet/types@^0.12.95": - version "0.12.220" - resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.220.tgz#45fd906ab0dbc7db753d72edca8d3af16411dbe8" - integrity sha512-vQkun9eeGriMR9+eCdWmynWG6yre0DreBP6axRT8hruTYnVEVDVjRTD5A6WYyTA6Qj8Yb0yzrka3LuRMfgG15g== +"@keplr-wallet/types@0.12.271", "@keplr-wallet/types@^0.12.211", "@keplr-wallet/types@^0.12.95": + version "0.12.271" + resolved "https://registry.yarnpkg.com/@keplr-wallet/types/-/types-0.12.271.tgz#22621c769488efb54f4af8dfca86b105fc069980" + integrity sha512-Pp4OcYKilVZ2YZVYdb5/4IL+BKYfXIDwTbOmH2G6z6/fmaZaH1hB25raHD+odQA6LhauvtxTKTpjkloI5kbOYg== dependencies: long "^4.0.0" @@ -2905,33 +2820,20 @@ clsx "^2.1.0" prop-types "^15.8.1" -"@mui/base@^5.0.0-beta.40": - version "5.0.0-beta.70" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.70.tgz#cd9755b8ae1b375fb3c985f06a4741505d793ecf" - integrity sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ== - dependencies: - "@babel/runtime" "^7.26.0" - "@floating-ui/react-dom" "^2.1.1" - "@mui/types" "~7.2.24" - "@mui/utils" "^6.4.8" - "@popperjs/core" "^2.11.8" - clsx "^2.1.1" - prop-types "^15.8.1" +"@mui/core-downloads-tracker@^5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz#85019a8704b0f63305fc5600635ee663810f2b66" + integrity sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA== -"@mui/core-downloads-tracker@^5.17.1": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz#49b88ecb68b800431b5c2f2bfb71372d1f1478fa" - integrity sha512-OcZj+cs6EfUD39IoPBOgN61zf1XFVY+imsGoBDwXeSq2UHJZE3N59zzBOVjclck91Ne3e9gudONOeILvHCIhUA== - -"@mui/core-downloads-tracker@^6.4.11": - version "6.4.11" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.11.tgz#0752eea024d8ff0f120b0f26aee4342c79112847" - integrity sha512-CzAQs9CTzlwbsF9ZYB4o4lLwBv1/qNE264NjuYao+ctAXsmlPtYa8RtER4UsUXSMxNN9Qi+aQdYcKl2sUpnmAw== +"@mui/core-downloads-tracker@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz#e9f7049d7e7bb1ee05839f7a0ce813755f137432" + integrity sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q== "@mui/icons-material@^5.16.11", "@mui/icons-material@^5.2.0": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.17.1.tgz#2b14832473d4d3738d8194665af359377eb91752" - integrity sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A== + version "5.18.0" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.18.0.tgz#97d87f1b7bee5fa7b9ba844518631de3112c1e57" + integrity sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg== dependencies: "@babel/runtime" "^7.23.9" @@ -2949,20 +2851,20 @@ prop-types "^15.8.1" "@mui/material-nextjs@^6.1.9": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/material-nextjs/-/material-nextjs-6.4.3.tgz#516eec4b6ca7d79f196f5c8b1b7f91e69e5c95ab" - integrity sha512-4ZRLrcD1HeWpvY8c7MrKYKuaUSobtvqcLYeEfGh/x5ezzPgKizhl7C0jpVVEgf6g+C9OgOGbhLTVfks7Y2IBAQ== + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/material-nextjs/-/material-nextjs-6.5.0.tgz#740993e8175c27e4dd7bd8cec2c1a17e49081d5e" + integrity sha512-VV+4BhmnY32pbPuIevs+rPl0R+bkVvqGCqRqZqNhkdBdX0pn1IHQ5mG/EfYJKoKUkF7q9FKSag5wduSTUxDqnQ== dependencies: "@babel/runtime" "^7.26.0" "@mui/material@^5.2.2": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.17.1.tgz#596f542a51fc74db75da2df66565b4874ce4049d" - integrity sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw== + version "5.18.0" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.18.0.tgz#71e72d52338252edc6f8d9461e04fdf0d61905cd" + integrity sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA== dependencies: "@babel/runtime" "^7.23.9" - "@mui/core-downloads-tracker" "^5.17.1" - "@mui/system" "^5.17.1" + "@mui/core-downloads-tracker" "^5.18.0" + "@mui/system" "^5.18.0" "@mui/types" "~7.2.15" "@mui/utils" "^5.17.1" "@popperjs/core" "^2.11.8" @@ -2974,13 +2876,13 @@ react-transition-group "^4.4.5" "@mui/material@^6.1.10": - version "6.4.11" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.4.11.tgz#ac5c42c25fc807545eef4de0e88a47b52de8ed75" - integrity sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA== + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.5.0.tgz#c7eccfe260030433c51b7aec17574bae4504cacc" + integrity sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow== dependencies: "@babel/runtime" "^7.26.0" - "@mui/core-downloads-tracker" "^6.4.11" - "@mui/system" "^6.4.11" + "@mui/core-downloads-tracker" "^6.5.0" + "@mui/system" "^6.5.0" "@mui/types" "~7.2.24" "@mui/utils" "^6.4.9" "@popperjs/core" "^2.11.8" @@ -3009,20 +2911,21 @@ "@mui/utils" "^6.4.9" prop-types "^15.8.1" -"@mui/styled-engine@^5.16.14": - version "5.16.14" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.14.tgz#f90fef5b4f8ebf11d48e1b1df8854a45bb31a9f5" - integrity sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw== +"@mui/styled-engine@^5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.18.0.tgz#914cca1385bb33ce0cde31721f529c8bd7fa301c" + integrity sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg== dependencies: "@babel/runtime" "^7.23.9" "@emotion/cache" "^11.13.5" + "@emotion/serialize" "^1.3.3" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/styled-engine@^6.4.11": - version "6.4.11" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.4.11.tgz#a48f48165382943018f70519de1d31e036abb054" - integrity sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw== +"@mui/styled-engine@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.5.0.tgz#cf9b3e706517f5f2989df92d2aea0d2917a77c8a" + integrity sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw== dependencies: "@babel/runtime" "^7.26.0" "@emotion/cache" "^11.13.5" @@ -3032,9 +2935,9 @@ prop-types "^15.8.1" "@mui/styles@^5.2.2": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.17.1.tgz#1d51a6eee7fdb396979199e6b08a2d0b3ca9e6f8" - integrity sha512-GxNtcD1jXjj1i81vyuaeNxCpph/ApxSxgJ+G8A2jUY5/bMOxXSmgUdupbB0JLexsDIqmaSqTePVN0jnMZc1iZQ== + version "5.18.0" + resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.18.0.tgz#e70330282e2c64880cc5f36f3fd9fa94257177d5" + integrity sha512-GDgFUfl2MMN8iGrYTuhJ0hHRoja2kVOlXnHb5d3BBQCHFxOBaAPDKQxaNkoAwRf/vBhhwXWDsJA2XlkNHUPBgA== dependencies: "@babel/runtime" "^7.23.9" "@emotion/hash" "^0.9.1" @@ -3054,40 +2957,40 @@ jss-plugin-vendor-prefixer "^10.10.0" prop-types "^15.8.1" -"@mui/system@^5.15.14", "@mui/system@^5.15.15", "@mui/system@^5.17.1": - version "5.17.1" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.17.1.tgz#1f987cce91bf738545a8cf5f99152cd2728e6077" - integrity sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg== +"@mui/system@^5.15.15", "@mui/system@^5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.18.0.tgz#e55331203a40584b26c5a855a07949ac8973bfb6" + integrity sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw== dependencies: "@babel/runtime" "^7.23.9" "@mui/private-theming" "^5.17.1" - "@mui/styled-engine" "^5.16.14" + "@mui/styled-engine" "^5.18.0" "@mui/types" "~7.2.15" "@mui/utils" "^5.17.1" clsx "^2.1.0" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^6.4.11": - version "6.4.11" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.11.tgz#f209e0792d008be2f75af41fada2ee9e06adeef2" - integrity sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA== +"@mui/system@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.5.0.tgz#52751ac4e3a546f53bc34fd2ef2731c28a824b92" + integrity sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w== dependencies: "@babel/runtime" "^7.26.0" "@mui/private-theming" "^6.4.9" - "@mui/styled-engine" "^6.4.11" + "@mui/styled-engine" "^6.5.0" "@mui/types" "~7.2.24" "@mui/utils" "^6.4.9" clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/types@^7.2.14", "@mui/types@^7.4.1": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.4.1.tgz#5611268faa0b46ab0c622c02b54f3f30f9809c2d" - integrity sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g== +"@mui/types@^7.2.14", "@mui/types@^7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.4.6.tgz#1432e0814cf155287283f6bbd1e95976a148ef07" + integrity sha512-NVBbIw+4CDMMppNamVxyTccNv0WxtDb7motWDlMeSC8Oy95saj1TIZMGynPpFLePt3yOD8TskzumeqORCgRGWw== dependencies: - "@babel/runtime" "^7.27.0" + "@babel/runtime" "^7.28.3" "@mui/types@~7.2.15", "@mui/types@~7.2.24": version "7.2.24" @@ -3106,19 +3009,19 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-7.0.2.tgz#b6842a9f979a619b65011a84a1964b85b205a9a4" - integrity sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w== +"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0": + version "7.3.2" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-7.3.2.tgz#361775d72c557a03115150e8aec4329c7ef14563" + integrity sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g== dependencies: - "@babel/runtime" "^7.27.0" - "@mui/types" "^7.4.1" - "@types/prop-types" "^15.7.14" + "@babel/runtime" "^7.28.3" + "@mui/types" "^7.4.6" + "@types/prop-types" "^15.7.15" clsx "^2.1.1" prop-types "^15.8.1" - react-is "^19.1.0" + react-is "^19.1.1" -"@mui/utils@^6.4.8", "@mui/utils@^6.4.9": +"@mui/utils@^6.4.9": version "6.4.9" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.9.tgz#b0df01daa254c7c32a1a30b30a5179e19ef071a7" integrity sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg== @@ -3130,126 +3033,53 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@mui/x-charts-vendor@7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz#b5858b91da0bde4f9c31f5360d05ade0b6eb5e31" - integrity sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg== - dependencies: - "@babel/runtime" "^7.25.7" - "@types/d3-color" "^3.1.3" - "@types/d3-delaunay" "^6.0.4" - "@types/d3-interpolate" "^3.0.4" - "@types/d3-scale" "^4.0.8" - "@types/d3-shape" "^3.1.6" - "@types/d3-time" "^3.0.3" - d3-color "^3.1.0" - d3-delaunay "^6.0.4" - d3-interpolate "^3.0.1" - d3-scale "^4.0.2" - d3-shape "^3.2.0" - d3-time "^3.1.0" - delaunator "^5.0.1" - robust-predicates "^3.0.2" - -"@mui/x-charts@^7.22.3": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@mui/x-charts/-/x-charts-7.28.0.tgz#8c3c4b7c02474ef80669c3416d24ebc245da5519" - integrity sha512-TNfq/rQfGKnjTaEITkY6l09NpMxwMwRTgLiDw+JQsS/7gwBBJUmMhEOj67BaFeYTsroFLUYeggiAj+RTSryd4A== - dependencies: - "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" - "@mui/x-charts-vendor" "7.20.0" - "@mui/x-internals" "7.28.0" - "@react-spring/rafz" "^9.7.5" - "@react-spring/web" "^9.7.5" - clsx "^2.1.1" - prop-types "^15.8.1" - -"@mui/x-data-grid@7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-7.1.1.tgz#ed4b852bf03c86d39bb4d35eacc35d5d0312f7ed" - integrity sha512-hNvz927lkAznFdy45QPE7mIZVyQhlqveHmTK9+SD0N1us4sSTij90uUJ/roTNDod0VA9f5GqWmNz+5h8ihpz6Q== - dependencies: - "@babel/runtime" "^7.24.0" - "@mui/system" "^5.15.14" - "@mui/utils" "^5.15.14" - clsx "^2.1.0" - prop-types "^15.8.1" - reselect "^4.1.8" - -"@mui/x-date-pickers@7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.1.1.tgz#13523f3d1cc9df89def9a6f90b19ae2d8d5d13ea" - integrity sha512-doSaoNfYR4nAXSN2mz5MwktYUmPt37jZ8/t5QrPgFtEFc3KWZoBps0YEcno5qUynY1ISpOjvnVr18zqszzG+RA== - dependencies: - "@babel/runtime" "^7.24.0" - "@mui/base" "^5.0.0-beta.40" - "@mui/system" "^5.15.14" - "@mui/utils" "^5.15.14" - "@types/react-transition-group" "^4.4.10" - clsx "^2.1.0" - prop-types "^15.8.1" - react-transition-group "^4.4.5" - "@mui/x-date-pickers@^7.23.2": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.28.3.tgz#eaf28b86b50b6fb61d0c671ba4e3d89a1b199a57" - integrity sha512-5umKB/DIMfDN+FAlzcrocix9PpoJDJ+5hMdlby8spTPObP4wCSN+wkEhk0vFC7qE9FAWXr4wjemaKvsNf41cCw== + version "7.29.4" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.29.4.tgz#b8808cb8e28c1d4e528b37b336effc8074e65faf" + integrity sha512-wJ3tsqk/y6dp+mXGtT9czciAMEO5Zr3IIAHg9x6IL0Eqanqy0N3chbmQQZv3iq0m2qUpQDLvZ4utZBUTJdjNzw== dependencies: "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" - "@mui/x-internals" "7.28.0" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" + "@mui/x-internals" "7.29.0" "@types/react-transition-group" "^4.4.11" clsx "^2.1.1" prop-types "^15.8.1" react-transition-group "^4.4.5" -"@mui/x-internals@7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.28.0.tgz#b0a04f4c0f53f2f91d13a46f357f731b77c832c5" - integrity sha512-p4GEp/09bLDumktdIMiw+OF4p+pJOOjTG0VUvzNxjbHB9GxbBKoMcHrmyrURqoBnQpWIeFnN/QAoLMFSpfwQbw== +"@mui/x-internals@7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.29.0.tgz#1f353b697ed1bf5594ac549556ade2e6841f4bf5" + integrity sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA== dependencies: "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" "@mui/x-tree-view@^7.11.1": - version "7.28.1" - resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.28.1.tgz#2b61da7a4b97c5326cdc87d211d8a15f4f3026bf" - integrity sha512-nXrP5pDgoi6snxngIezL8PRvkWECqSApq8Rh8+TVsslBog+XmWs5PHgUXaKsCgi3SN0eNOvjz0+e4I0c+qkuvA== + version "7.29.1" + resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.29.1.tgz#5bc17b212a1c42c8922bbd0db6d13e622dd62db2" + integrity sha512-hjfgDVxiuRr5BYKEI2bemkqMaWbh/YIVRJ01OxEU5An2hL5DKAA/Ziv6UV9jse3nTXJwOGkZ3uj0ofoxb9iznQ== dependencies: "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" - "@mui/x-internals" "7.28.0" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0" + "@mui/x-internals" "7.29.0" "@types/react-transition-group" "^4.4.11" clsx "^2.1.1" prop-types "^15.8.1" react-transition-group "^4.4.5" -"@napi-rs/wasm-runtime@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz#642e8390ee78ed21d6b79c467aa610e249224ed6" - integrity sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg== +"@napi-rs/wasm-runtime@^0.2.11": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" + integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== dependencies: - "@emnapi/core" "^1.4.0" - "@emnapi/runtime" "^1.4.0" - "@tybys/wasm-util" "^0.9.0" + "@emnapi/core" "^1.4.3" + "@emnapi/runtime" "^1.4.3" + "@tybys/wasm-util" "^0.10.0" -"@next/env@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.21.tgz#09ff0813d29c596397e141205d4f5fd5c236bdd0" - integrity sha512-lXcwcJd5oR01tggjWJ6SrNNYFGuOOMB9c251wUNkjCpkoXOPkDeF/15c3mnVlBqrW4JJXb2kVxDFhC4GduJt2A== - -"@next/env@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.0.tgz#ea3a2a02e8023097efa23ec573540f522815e9d4" - integrity sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA== - -"@next/eslint-plugin-next@14.1.4": - version "14.1.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.4.tgz#d7372b5ffede0e466af8af2ff534386418827fc8" - integrity sha512-n4zYNLSyCo0Ln5b7qxqQeQ34OZKXwgbdcx6kmkQbywr+0k6M3Vinft0T72R6CDAcDrne2IAgSud4uWCzFgc5HA== - dependencies: - glob "10.3.10" +"@next/env@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.32.tgz#6d1107e2b7cc8649ff3730b8b46deb4e8a6d38fa" + integrity sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng== "@next/eslint-plugin-next@15.0.3": version "15.0.3" @@ -3258,90 +3088,50 @@ dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.21.tgz#32a31992aace1440981df9cf7cb3af7845d94fec" - integrity sha512-HwEjcKsXtvszXz5q5Z7wCtrHeTTDSTgAbocz45PHMUjU3fBYInfvhR+ZhavDRUYLonm53aHZbB09QtJVJj8T7g== +"@next/swc-darwin-arm64@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.32.tgz#83482a7282df899b73d916e02b02a189771e706c" + integrity sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw== -"@next/swc-darwin-arm64@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.0.tgz#804660ea060920ed9835d079a0af64552771ccf4" - integrity sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw== +"@next/swc-darwin-x64@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.32.tgz#1a9eb676a014e1fc999251f10288c25a0f81d6d1" + integrity sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg== -"@next/swc-darwin-x64@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.21.tgz#5ab4b3f6685b6b52f810d0f5cf6e471480ddffdb" - integrity sha512-TSAA2ROgNzm4FhKbTbyJOBrsREOMVdDIltZ6aZiKvCi/v0UwFmwigBGeqXDA97TFMpR3LNNpw52CbVelkoQBxA== +"@next/swc-linux-arm64-gnu@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.32.tgz#7713a49abd555d6f698e766b1631b67d881b4ee4" + integrity sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA== -"@next/swc-darwin-x64@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.0.tgz#0e352c9aee544b18d3e4c138487ecc6aa71469b4" - integrity sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg== +"@next/swc-linux-arm64-musl@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.32.tgz#327efdffe97e56f5389a7889cdedbd676fdbb519" + integrity sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ== -"@next/swc-linux-arm64-gnu@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.21.tgz#8a0e1fa887aef19ca218af2af515d0a5ee67ba3f" - integrity sha512-0Dqjn0pEUz3JG+AImpnMMW/m8hRtl1GQCNbO66V1yp6RswSTiKmnHf3pTX6xMdJYSemf3O4Q9ykiL0jymu0TuA== +"@next/swc-linux-x64-gnu@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.32.tgz#a3e7444613d0fe5c8ea4ead08d6a9c818246758c" + integrity sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw== -"@next/swc-linux-arm64-gnu@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.0.tgz#2dbdc7e7234bb56031ede78ad48275f66a09cdce" - integrity sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg== +"@next/swc-linux-x64-musl@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.32.tgz#a2ec5b0a06c740d6740c938b1d4a614f1a13f018" + integrity sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg== -"@next/swc-linux-arm64-musl@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.21.tgz#ddad844406b42fa8965fe11250abc85c1fe0fd05" - integrity sha512-Ggfw5qnMXldscVntwnjfaQs5GbBbjioV4B4loP+bjqNEb42fzZlAaK+ldL0jm2CTJga9LynBMhekNfV8W4+HBw== +"@next/swc-win32-arm64-msvc@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.32.tgz#b4d3e47c6b276fc4711deb978d04015d029d198d" + integrity sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg== -"@next/swc-linux-arm64-musl@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.0.tgz#410143ca5650e366da5483bd5078ba9dfdd96185" - integrity sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA== +"@next/swc-win32-ia32-msvc@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.32.tgz#d1f1f854a1fbbaeefa8f81271437448653f33494" + integrity sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA== -"@next/swc-linux-x64-gnu@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.21.tgz#db55fd666f9ba27718f65caa54b622a912cdd16b" - integrity sha512-uokj0lubN1WoSa5KKdThVPRffGyiWlm/vCc/cMkWOQHw69Qt0X1o3b2PyLLx8ANqlefILZh1EdfLRz9gVpG6tg== - -"@next/swc-linux-x64-gnu@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.0.tgz#1db7dc39e528a2de8ba11156585dfc74b4d6e2aa" - integrity sha512-ZMQ9yzDEts/vkpFLRAqfYO1wSpIJGlQNK9gZ09PgyjBJUmg8F/bb8fw2EXKgEaHbCc4gmqMpDfh+T07qUphp9A== - -"@next/swc-linux-x64-musl@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.21.tgz#dddb850353624efcd58c4c4e30ad8a1aab379642" - integrity sha512-iAEBPzWNbciah4+0yI4s7Pce6BIoxTQ0AGCkxn/UBuzJFkYyJt71MadYQkjPqCQCJAFQ26sYh7MOKdU+VQFgPg== - -"@next/swc-linux-x64-musl@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.0.tgz#3f7f0fb55a16d7b87801451555cb929117b8ff88" - integrity sha512-RFwq5VKYTw9TMr4T3e5HRP6T4RiAzfDJ6XsxH8j/ZeYq2aLsBqCkFzwMI0FmnSsLaUbOb46Uov0VvN3UciHX5A== - -"@next/swc-win32-arm64-msvc@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.21.tgz#290012ee57b196d3d2d04853e6bf0179cae9fbaf" - integrity sha512-plykgB3vL2hB4Z32W3ktsfqyuyGAPxqwiyrAi2Mr8LlEUhNn9VgkiAl5hODSBpzIfWweX3er1f5uNpGDygfQVQ== - -"@next/swc-win32-arm64-msvc@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.0.tgz#1eb4ba20beae55d41e02e1780bc45ceabd45550f" - integrity sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ== - -"@next/swc-win32-ia32-msvc@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.21.tgz#c959135a78cab18cca588d11d1e33bcf199590d4" - integrity sha512-w5bacz4Vxqrh06BjWgua3Yf7EMDb8iMcVhNrNx8KnJXt8t+Uu0Zg4JHLDL/T7DkTCEEfKXO/Er1fcfWxn2xfPA== - -"@next/swc-win32-x64-msvc@14.2.21": - version "14.2.21" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.21.tgz#21ff892286555b90538a7d1b505ea21a005d6ead" - integrity sha512-sT6+llIkzpsexGYZq8cjjthRyRGe5cJVhqh12FmlbxHqna6zsDDK8UNaV7g41T6atFHCJUPeLb3uyAwrBwy0NA== - -"@next/swc-win32-x64-msvc@15.3.0": - version "15.3.0" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz#68740dee9831fcd738ab2056438bd3de75712f65" - integrity sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw== +"@next/swc-win32-x64-msvc@14.2.32": + version "14.2.32" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.32.tgz#8212d681cf6858a9e3204728f8f2b161000683ed" + integrity sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA== "@nivo/annotations@0.88.0": version "0.88.0" @@ -3460,9 +3250,9 @@ d3-scale "^4.0.2" "@noble/hashes@^1", "@noble/hashes@^1.0.0", "@noble/hashes@^1.2.0": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.2.tgz#d53c65a21658fb02f3303e7ee3ba89d6754c64b4" - integrity sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ== + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -3667,17 +3457,6 @@ resolved "https://registry.yarnpkg.com/@nymproject/node-tester/-/node-tester-1.2.3.tgz#79fbde8b69e2d1180eed897557c4610a1aa1038f" integrity sha512-VDFdH2ddIcXXamwkMbeHA88Xa/S2iPWh9QxkFggppvHS1d6gmnNHAZxXm3Uuhx7pCpzKldA0OT7qohMg9GW9xg== -"@nymproject/nym-validator-client@0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@nymproject/nym-validator-client/-/nym-validator-client-0.18.0.tgz#4dd72bafdf6c72b603242f32c0bb9a1f9e475b98" - integrity sha512-FO1T15S2BJVuMoPA2yOaH40aD3hKJPKJVyX5ix7eJEbBWIdsYNoVeVc/soHhaAU1FIy2uU0G/FZQkaUYJGGb7Q== - dependencies: - "@cosmjs/cosmwasm-stargate" "^0.25.5" - "@cosmjs/math" "^0.25.5" - "@cosmjs/proto-signing" "^0.25.5" - "@cosmjs/stargate" "^0.25.5" - axios "^0.21.1" - "@octokit/auth-token@^3.0.0": version "3.0.4" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.4.tgz#70e941ba742bdd2b49bdb7393e821dea8520a3db" @@ -3808,9 +3587,9 @@ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@pmmmwh/react-refresh-webpack-plugin@^0.5.3", "@pmmmwh/react-refresh-webpack-plugin@^0.5.4": - version "0.5.16" - resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz#36795b3d5a967032a769977780f2dcc01f4e9c4a" - integrity sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q== + version "0.5.17" + resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz#8c2f34ca8651df74895422046e11ce5a120e7930" + integrity sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ== dependencies: ansi-html "^0.0.9" core-js-pure "^3.23.3" @@ -3878,644 +3657,644 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@react-aria/breadcrumbs@^3.5.23": - version "3.5.23" - resolved "https://registry.yarnpkg.com/@react-aria/breadcrumbs/-/breadcrumbs-3.5.23.tgz#d5f15a567656ccbba11680cb3849f42e74618c32" - integrity sha512-4uLxuAgPfXds8sBc/Cg0ml7LKWzK+YTwHL7xclhQUkPO32rzlHDl+BJ5cyWhvZgGUf8JJXbXhD5VlJJzbbl8Xg== +"@react-aria/breadcrumbs@^3.5.28": + version "3.5.28" + resolved "https://registry.yarnpkg.com/@react-aria/breadcrumbs/-/breadcrumbs-3.5.28.tgz#746c769f1a2266d8b8a50494e25dc581579f70d9" + integrity sha512-6S3QelpajodEzN7bm49XXW5gGoZksK++cl191W0sexq/E5hZHAEA9+CFC8pL3px13ji7qHGqKAxOP4IUVBdVpQ== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/link" "^3.8.0" - "@react-aria/utils" "^3.28.2" - "@react-types/breadcrumbs" "^3.7.12" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/link" "^3.8.5" + "@react-aria/utils" "^3.30.1" + "@react-types/breadcrumbs" "^3.7.16" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/button@^3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.13.0.tgz#9751a34bc69e02c935d4b373fb456560ec0d3ef1" - integrity sha512-BEcTQb7Q8ZrAtn0scPDv/ErZoGC1FI0sLk0UTPGskuh/RV9ZZGFbuSWTqOwV8w5CS6VMvPjH6vaE8hS7sb5DIw== +"@react-aria/button@^3.14.1": + version "3.14.1" + resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.14.1.tgz#97475f770f0ad8fe928f2fa97227ef01f729901b" + integrity sha512-Ug06unKEYVG3OF6zKmpVR7VfLzpj7eJVuFo3TCUxwFJG7DI28pZi2TaGWnhm7qjkxfl1oz0avQiHVfDC99gSuw== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/toolbar" "3.0.0-beta.15" - "@react-aria/utils" "^3.28.2" - "@react-stately/toggle" "^3.8.3" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/toolbar" "3.0.0-beta.20" + "@react-aria/utils" "^3.30.1" + "@react-stately/toggle" "^3.9.1" + "@react-types/button" "^3.14.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/calendar@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@react-aria/calendar/-/calendar-3.8.0.tgz#8f210d2ecfd89a8020f5568fdf2014a8e6c62a46" - integrity sha512-9vms/fWjJPZkJcMxciwWWOjGy/Q0nqI6FV0pYbMZbqepkzglEaVd98kl506r/4hLhWKwLdTfqCgbntRecj8jBg== +"@react-aria/calendar@^3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-aria/calendar/-/calendar-3.9.1.tgz#2b32284b33919571557ba8d220f5a889d1b118c5" + integrity sha512-dCJliRIi3x3VmAZkJDNTZddq0+QoUX9NS7GgdqPPYcJIMbVPbyLWL61//0SrcCr3MuSRCoI1eQZ8PkQe/2PJZQ== dependencies: - "@internationalized/date" "^3.8.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/calendar" "^3.8.0" - "@react-types/button" "^3.12.0" - "@react-types/calendar" "^3.7.0" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/live-announcer" "^3.4.4" + "@react-aria/utils" "^3.30.1" + "@react-stately/calendar" "^3.8.4" + "@react-types/button" "^3.14.0" + "@react-types/calendar" "^3.7.4" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/checkbox@^3.15.4": - version "3.15.4" - resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.15.4.tgz#3e85c948923960eebcc3a6d668a5d056f1bf0d7e" - integrity sha512-ZkDJFs2EfMBXVIpBSo4ouB+NXyr2LRgZNp2x8/v+7n3aTmMU8j2PzT+Ra2geTQbC0glMP7UrSg4qZblqrxEBcQ== +"@react-aria/checkbox@^3.16.1": + version "3.16.1" + resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.16.1.tgz#18e5b36e1e3c07358b0283d7696806e5fd7b57a5" + integrity sha512-YcG3QhuGIwqPHo4GVGVmwxPM5Ayq9CqYfZjla/KTfJILPquAJ12J7LSMpqS/Z5TlMNgIIqZ3ZdrYmjQlUY7eUg== dependencies: - "@react-aria/form" "^3.0.15" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/toggle" "^3.11.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/checkbox" "^3.6.13" - "@react-stately/form" "^3.1.3" - "@react-stately/toggle" "^3.8.3" - "@react-types/checkbox" "^3.9.3" - "@react-types/shared" "^3.29.0" + "@react-aria/form" "^3.1.1" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/toggle" "^3.12.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/checkbox" "^3.7.1" + "@react-stately/form" "^3.2.1" + "@react-stately/toggle" "^3.9.1" + "@react-types/checkbox" "^3.10.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/color@^3.0.6": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@react-aria/color/-/color-3.0.6.tgz#6444d992afe0149bb5cce28d75b747a01cc59fef" - integrity sha512-ik4Db9hrN1yIT0CQMB888ktBmrwA/kNhkfiDACtoUHv8Ev+YEpmagnmih9vMyW2vcnozYJpnn/aCMl59J5uMew== +"@react-aria/color@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@react-aria/color/-/color-3.1.1.tgz#5d3e169e86a247cf78eb71e6344bebaf409dfb7a" + integrity sha512-4+woybtn4kh5ytggWQ06bqqWsoucOrxwNrwW1XP6EmvcjIcsfVW+VwFwM5ZYa2LGF+fHiW3dM4bjRqVa7i9PVg== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/numberfield" "^3.11.13" - "@react-aria/slider" "^3.7.18" - "@react-aria/spinbutton" "^3.6.14" - "@react-aria/textfield" "^3.17.2" - "@react-aria/utils" "^3.28.2" - "@react-aria/visually-hidden" "^3.8.22" - "@react-stately/color" "^3.8.4" - "@react-stately/form" "^3.1.3" - "@react-types/color" "^3.0.4" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/numberfield" "^3.12.1" + "@react-aria/slider" "^3.8.1" + "@react-aria/spinbutton" "^3.6.18" + "@react-aria/textfield" "^3.18.1" + "@react-aria/utils" "^3.30.1" + "@react-aria/visually-hidden" "^3.8.27" + "@react-stately/color" "^3.9.1" + "@react-stately/form" "^3.2.1" + "@react-types/color" "^3.1.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/combobox@^3.12.2": - version "3.12.2" - resolved "https://registry.yarnpkg.com/@react-aria/combobox/-/combobox-3.12.2.tgz#6e2b0cfc6863d8ead79a5dd7736104187a6929f7" - integrity sha512-EgddiF8VnAjB4EynJERPn4IoDMUabI8GiKOQZ6Ar3MlRWxQnUfxPpZwXs8qWR3dPCzYUt2PhBinhBMjyR1yRIw== +"@react-aria/combobox@^3.13.2": + version "3.13.2" + resolved "https://registry.yarnpkg.com/@react-aria/combobox/-/combobox-3.13.2.tgz#b309dc60f2918227dd52c551d9380a028de24cad" + integrity sha512-PNyqlaM19A+lKX9hwqkKTXvWDilCKaRH2RdrB/C5AfmGi3bh/IKsu66c8ohgadXB2AIdJB36EOOm3hNh8G9DqQ== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/listbox" "^3.14.3" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/menu" "^3.18.2" - "@react-aria/overlays" "^3.27.0" - "@react-aria/selection" "^3.24.0" - "@react-aria/textfield" "^3.17.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/collections" "^3.12.3" - "@react-stately/combobox" "^3.10.4" - "@react-stately/form" "^3.1.3" - "@react-types/button" "^3.12.0" - "@react-types/combobox" "^3.13.4" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/listbox" "^3.14.8" + "@react-aria/live-announcer" "^3.4.4" + "@react-aria/menu" "^3.19.2" + "@react-aria/overlays" "^3.29.1" + "@react-aria/selection" "^3.25.1" + "@react-aria/textfield" "^3.18.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/collections" "^3.12.7" + "@react-stately/combobox" "^3.11.1" + "@react-stately/form" "^3.2.1" + "@react-types/button" "^3.14.0" + "@react-types/combobox" "^3.13.8" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/datepicker@^3.14.2": - version "3.14.2" - resolved "https://registry.yarnpkg.com/@react-aria/datepicker/-/datepicker-3.14.2.tgz#db1efa94edc86b57950cf32cbfd252b206653fab" - integrity sha512-O7fdzcqIJ7i/+8SGYvx4tloTZgK4Ws8OChdbFcd2rZoRPqxM50M6J+Ota8hTet2wIhojUXnM3x2na3EvoucBXA== +"@react-aria/datepicker@^3.15.1": + version "3.15.1" + resolved "https://registry.yarnpkg.com/@react-aria/datepicker/-/datepicker-3.15.1.tgz#df128ee61bdfaabd24a387e22dcc24b4bf2f036f" + integrity sha512-RfUOvsupON6E5ZELpBgb9qxsilkbqwzsZ78iqCDTVio+5kc5G9jVeHEIQOyHnavi/TmJoAnbmmVpEbE6M9lYJQ== dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/number" "^3.6.1" - "@internationalized/string" "^3.2.6" - "@react-aria/focus" "^3.20.2" - "@react-aria/form" "^3.0.15" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/spinbutton" "^3.6.14" - "@react-aria/utils" "^3.28.2" - "@react-stately/datepicker" "^3.14.0" - "@react-stately/form" "^3.1.3" - "@react-types/button" "^3.12.0" - "@react-types/calendar" "^3.7.0" - "@react-types/datepicker" "^3.12.0" - "@react-types/dialog" "^3.5.17" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@internationalized/number" "^3.6.5" + "@internationalized/string" "^3.2.7" + "@react-aria/focus" "^3.21.1" + "@react-aria/form" "^3.1.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/spinbutton" "^3.6.18" + "@react-aria/utils" "^3.30.1" + "@react-stately/datepicker" "^3.15.1" + "@react-stately/form" "^3.2.1" + "@react-types/button" "^3.14.0" + "@react-types/calendar" "^3.7.4" + "@react-types/datepicker" "^3.13.1" + "@react-types/dialog" "^3.5.21" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/dialog@^3.5.24": - version "3.5.24" - resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.5.24.tgz#1ad9ecd505f8c3e0c7700a84e8c213a0416c5278" - integrity sha512-tw0WH89gVpHMI5KUQhuzRE+IYCc9clRfDvCppuXNueKDrZmrQKbeoU6d0b5WYRsBur2+d7ErtvpLzHVqE1HzfA== +"@react-aria/dialog@^3.5.30": + version "3.5.30" + resolved "https://registry.yarnpkg.com/@react-aria/dialog/-/dialog-3.5.30.tgz#e7e7080dec37bf4e98cb98430b931d5bd7fa7c66" + integrity sha512-fiodaeMSTiC4qKNwnCLbNykyvfcxuz/PiU/pBNhWYd4lUrX1TauBQb0++o5/K6OHt8iB+A7/LSHRbPtyOSWE9g== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/overlays" "^3.27.0" - "@react-aria/utils" "^3.28.2" - "@react-types/dialog" "^3.5.17" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/overlays" "^3.29.1" + "@react-aria/utils" "^3.30.1" + "@react-types/dialog" "^3.5.21" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/disclosure@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@react-aria/disclosure/-/disclosure-3.0.4.tgz#cff7e457b7014038785bcdb23c4ee10a5813674a" - integrity sha512-HXGVLA06BH0b/gN8dCTzWATwMikz8D+ahRxZiI0HDZxLADWGsSPqRXKN0GNAiBKbvPtvAbrwslE3pktk/SlU/w== +"@react-aria/disclosure@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@react-aria/disclosure/-/disclosure-3.0.8.tgz#a5b2ed7eec552beab2c95241f7c91a1c9a1ff9bf" + integrity sha512-Q2v6czm3ViMTw7J+GCWdXw3rZ5Fgmy97gpSQjpEoxSyqA1UfpRRvNa+XYoXmbpaY1MGhtUX3m2GgZ4IuhhMHVQ== dependencies: - "@react-aria/ssr" "^3.9.8" - "@react-aria/utils" "^3.28.2" - "@react-stately/disclosure" "^3.0.3" - "@react-types/button" "^3.12.0" + "@react-aria/ssr" "^3.9.10" + "@react-aria/utils" "^3.30.1" + "@react-stately/disclosure" "^3.0.7" + "@react-types/button" "^3.14.0" "@swc/helpers" "^0.5.0" -"@react-aria/dnd@^3.9.2": - version "3.9.2" - resolved "https://registry.yarnpkg.com/@react-aria/dnd/-/dnd-3.9.2.tgz#1b45989d3e61e5aecc0703931d8715a158aec91b" - integrity sha512-pPYygmJTjSPV2K/r48TvF75WuddG8d8nlIxAXSW22++WKqZ0z+eun6gDUXoKeB2rgY7sVfLqpRdnPV52AnBX+Q== +"@react-aria/dnd@^3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@react-aria/dnd/-/dnd-3.11.2.tgz#619741f547487e53e5ced858269e134d76dbe790" + integrity sha512-xaIUV0zPtUTLIBoE7qlGFPfRTfyDJT78fDzawYq6FwZcjgrl8X408UDCUaKk6xSJRh9UjNn78hil1WDYTLFNWA== dependencies: - "@internationalized/string" "^3.2.6" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/overlays" "^3.27.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/dnd" "^3.5.3" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@internationalized/string" "^3.2.7" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/live-announcer" "^3.4.4" + "@react-aria/overlays" "^3.29.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/collections" "^3.12.7" + "@react-stately/dnd" "^3.7.0" + "@react-types/button" "^3.14.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/focus@^3.20.2": - version "3.20.2" - resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.20.2.tgz#f20cd830d2536b905169a547228c5d5471a874bc" - integrity sha512-Q3rouk/rzoF/3TuH6FzoAIKrl+kzZi9LHmr8S5EqLAOyP9TXIKG34x2j42dZsAhrw7TbF9gA8tBKwnCNH4ZV+Q== +"@react-aria/focus@^3.21.1": + version "3.21.1" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.21.1.tgz#fad9d0803e0e4423bb6e14ed3208fffd694e5e42" + integrity sha512-hmH1IhHlcQ2lSIxmki1biWzMbGgnhdxJUM0MFfzc71Rv6YAzhlx4kX3GYn4VNcjCeb6cdPv4RZ5vunV4kgMZYQ== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" clsx "^2.0.0" -"@react-aria/form@^3.0.15": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@react-aria/form/-/form-3.0.15.tgz#eaa46c5bc36314a60da116b11bd334e2cf3fb7ca" - integrity sha512-kk8AnLz+EOgnn3sTaXYmtw+YzVDc1of/+xAkuOupQi6zQFnNRjc99JlDbKHoUZ39urMl+8lsp/1b9VPPhNrBNw== +"@react-aria/form@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@react-aria/form/-/form-3.1.1.tgz#76b3eb4a6985fb27b1d437db89b5a69c54be292f" + integrity sha512-PjZC25UgH5orit9p56Ymbbo288F3eaDd3JUvD8SG+xgx302HhlFAOYsQLLAb4k4H03bp0gWtlUEkfX6KYcE1Tw== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/form" "^3.1.3" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-stately/form" "^3.2.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/grid@^3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@react-aria/grid/-/grid-3.13.0.tgz#d61877ca662c5082cd8d688ab674641db3bfb185" - integrity sha512-RcuJYA4fyJ83MH3SunU+P5BGkx3LJdQ6kxwqwWGIuI9eUKc7uVbqvN9WN3fI+L0QfxqBFmh7ffRxIdQn7puuzw== +"@react-aria/grid@^3.14.4": + version "3.14.4" + resolved "https://registry.yarnpkg.com/@react-aria/grid/-/grid-3.14.4.tgz#0511ae807fe7ff6df03b96edfb0e23624f137804" + integrity sha512-l1FLQNKnoHpY4UClUTPUV0AqJ5bfAULEE0ErY86KznWLd+Hqzo7mHLqqDV02CDa/8mIUcdoax/MrYYIbPDlOZA== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/collections" "^3.12.3" - "@react-stately/grid" "^3.11.1" - "@react-stately/selection" "^3.20.1" - "@react-types/checkbox" "^3.9.3" - "@react-types/grid" "^3.3.1" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/live-announcer" "^3.4.4" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/collections" "^3.12.7" + "@react-stately/grid" "^3.11.5" + "@react-stately/selection" "^3.20.5" + "@react-types/checkbox" "^3.10.1" + "@react-types/grid" "^3.3.5" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/gridlist@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@react-aria/gridlist/-/gridlist-3.12.0.tgz#9ba69d0cf0b5f24ad8a6be28740921f7cd78cbd5" - integrity sha512-KSpnSBYQ7ozGQNaRR2NGq7Fl2zIv5w9KNyO9V/IE2mxUNfX6fwqUPoANFcy9ySosksE7pPnFtuYIB+TQtUjYqQ== +"@react-aria/gridlist@^3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@react-aria/gridlist/-/gridlist-3.14.0.tgz#cf4454e94ca9ca99dd21adad421420ff2e572f7a" + integrity sha512-8NWDaUbPe6ujI+kSvDqr2onPYWlBXiaLCQ6nfYOo+GFKxeVCsv4a2I5HAAoGf9THNQ5b8b8kJa+M0xyL1Z71XA== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/grid" "^3.13.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/collections" "^3.12.3" - "@react-stately/list" "^3.12.1" - "@react-stately/tree" "^3.8.9" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/grid" "^3.14.4" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/list" "^3.13.0" + "@react-stately/tree" "^3.9.2" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/i18n@^3.12.8": - version "3.12.8" - resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.12.8.tgz#43d534f04d3bfdef674ba94527cf7532875d8fc8" - integrity sha512-V/Nau9WuwTwxfFffQL4URyKyY2HhRlu9zmzkF2Hw/j5KmEQemD+9jfaLueG2CJu85lYL06JrZXUdnhZgKnqMkA== +"@react-aria/i18n@^3.12.12": + version "3.12.12" + resolved "https://registry.yarnpkg.com/@react-aria/i18n/-/i18n-3.12.12.tgz#186eadf0c5dd3c38eb31c40b7c0191df07cef185" + integrity sha512-JN6p+Xc6Pu/qddGRoeYY6ARsrk2Oz7UiQc9nLEPOt3Ch+blJZKWwDjcpo/p6/wVZdD/2BgXS7El6q6+eMg7ibw== dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/message" "^3.1.7" - "@internationalized/number" "^3.6.1" - "@internationalized/string" "^3.2.6" - "@react-aria/ssr" "^3.9.8" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@internationalized/message" "^3.1.8" + "@internationalized/number" "^3.6.5" + "@internationalized/string" "^3.2.7" + "@react-aria/ssr" "^3.9.10" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/interactions@^3.25.0": - version "3.25.0" - resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.25.0.tgz#a57dcec4b8c429756770fbe969263588bb879110" - integrity sha512-GgIsDLlO8rDU/nFn6DfsbP9rfnzhm8QFjZkB9K9+r+MTSCn7bMntiWQgMM+5O6BiA8d7C7x4zuN4bZtc0RBdXQ== +"@react-aria/interactions@^3.25.5": + version "3.25.5" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.25.5.tgz#f7f69467c899f9673460c3401fcaac08d2dcac7d" + integrity sha512-EweYHOEvMwef/wsiEqV73KurX/OqnmbzKQa2fLxdULbec5+yDj6wVGaRHIzM4NiijIDe+bldEl5DG05CAKOAHA== dependencies: - "@react-aria/ssr" "^3.9.8" - "@react-aria/utils" "^3.28.2" - "@react-stately/flags" "^3.1.1" - "@react-types/shared" "^3.29.0" + "@react-aria/ssr" "^3.9.10" + "@react-aria/utils" "^3.30.1" + "@react-stately/flags" "^3.1.2" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/label@^3.7.17": - version "3.7.17" - resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.7.17.tgz#288ee245c4caf6bc4dad495f7f994633e0adc122" - integrity sha512-Fz7IC2LQT2Y/sAoV+gFEXoULtkznzmK2MmeTv5shTNjeTxzB1BhQbD4wyCypi7eGsnD/9Zy+8viULCsIUbvjWw== +"@react-aria/label@^3.7.21": + version "3.7.21" + resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.7.21.tgz#1deeb3886bc5110f76659137c3e21811fa1606d1" + integrity sha512-8G+059/GZahgQbrhMcCcVcrjm7W+pfzrypH/Qkjo7C1yqPGt6geeFwWeOIbiUZoI0HD9t9QvQPryd6m46UC7Tg== dependencies: - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/landmark@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@react-aria/landmark/-/landmark-3.0.2.tgz#bc79d6f31be313e7741b5fc9451aa0119fd432db" - integrity sha512-KVXa9s3fSgo/PiUjdbnPh3a1yS4t2bMZeVBPPzYAgQ4wcU2WjuLkhviw+5GWSWRfT+jpIMV7R/cmyvr0UHvRfg== +"@react-aria/landmark@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@react-aria/landmark/-/landmark-3.0.6.tgz#60805b4c4191525d843ac8d8a28f01ff6195096b" + integrity sha512-dMPBqJWTDAr3Lj5hA+XYDH2PWqtFghYy+y7iq7K5sK/96cub8hZEUjhwn+HGgHsLerPp0dWt293nKupAJnf4Vw== dependencies: - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" use-sync-external-store "^1.4.0" -"@react-aria/link@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@react-aria/link/-/link-3.8.0.tgz#7067ec4de77c2cae6c820fb84c9aee5eca7059df" - integrity sha512-gpDD6t3FqtFR9QjSIKNpmSR3tS4JG2anVKx2wixuRDHO6Ddexxv4SBzsE1+230p+FlFGjftFa2lEgQ7RNjZrmA== +"@react-aria/link@^3.8.5": + version "3.8.5" + resolved "https://registry.yarnpkg.com/@react-aria/link/-/link-3.8.5.tgz#d8bade7e3f1012c77a1e147942310520796e7b0b" + integrity sha512-klhV4roPp5MLRXJv1N+7SXOj82vx4gzVpuwQa3vouA+YI1my46oNzwgtkLGSTvE9OvDqYzPDj2YxFYhMywrkuw== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-types/link" "^3.6.0" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-types/link" "^3.6.4" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/listbox@^3.13.3", "@react-aria/listbox@^3.14.3": - version "3.14.3" - resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.14.3.tgz#d8a223b8830fd8cbc762a0fb237da0bfc0a502a2" - integrity sha512-wzelam1KENUvKjsTq8gfrOW2/iab8SyIaSXfFvGmWW82XlDTlW+oQeA39tvOZktMVGspr+xp8FySY09rtz6UXw== +"@react-aria/listbox@^3.13.3", "@react-aria/listbox@^3.14.8": + version "3.14.8" + resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.14.8.tgz#c46d79ba3f2d389a95cad4eba8bdbf0a53c4a0b6" + integrity sha512-uRgbuD9afFv0PDhQ/VXCmAwlYctIyKRzxztkqp1p/1yz/tn/hs+bG9kew9AI02PtlRO1mSc+32O+mMDXDer8hA== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/collections" "^3.12.3" - "@react-stately/list" "^3.12.1" - "@react-types/listbox" "^3.6.0" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/collections" "^3.12.7" + "@react-stately/list" "^3.13.0" + "@react-types/listbox" "^3.7.3" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/live-announcer@^3.4.2": - version "3.4.2" - resolved "https://registry.yarnpkg.com/@react-aria/live-announcer/-/live-announcer-3.4.2.tgz#3788b749272a0f2c09196b1a99c8cbdb6172565e" - integrity sha512-6+yNF9ZrZ4YJ60Oxy2gKI4/xy6WUv1iePDCFJkgpNVuOEYi8W8czff8ctXu/RPB25OJx5v2sCw9VirRogTo2zA== +"@react-aria/live-announcer@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@react-aria/live-announcer/-/live-announcer-3.4.4.tgz#0e6533940222208b323b71d56ac8e115b2121e6a" + integrity sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA== dependencies: "@swc/helpers" "^0.5.0" -"@react-aria/menu@^3.18.2": - version "3.18.2" - resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.18.2.tgz#3e0b0db37c1ea3cc2c27b4bb30f8e38cd586a4ad" - integrity sha512-90k+Ke1bhFWhR2zuRI6OwKWQrCpOD99n+9jhG96JZJZlNo5lB+5kS+ufG1LRv5GBnCug0ciLQmPMAfguVsCjEQ== +"@react-aria/menu@^3.19.2": + version "3.19.2" + resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.19.2.tgz#a506d0cd4443f4ddfb89df341937fc77ed77b47d" + integrity sha512-WzDLW2MotL0L5/LEwc5oGgISf2ODuw4FnRpF0Zk+J4tKFfC88odvKz848ubBvThRXuXEvL0BHY+WqtM+j9fn3g== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/overlays" "^3.27.0" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/collections" "^3.12.3" - "@react-stately/menu" "^3.9.3" - "@react-stately/selection" "^3.20.1" - "@react-stately/tree" "^3.8.9" - "@react-types/button" "^3.12.0" - "@react-types/menu" "^3.10.0" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/overlays" "^3.29.1" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/collections" "^3.12.7" + "@react-stately/menu" "^3.9.7" + "@react-stately/selection" "^3.20.5" + "@react-stately/tree" "^3.9.2" + "@react-types/button" "^3.14.0" + "@react-types/menu" "^3.10.4" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/meter@^3.4.22": - version "3.4.22" - resolved "https://registry.yarnpkg.com/@react-aria/meter/-/meter-3.4.22.tgz#25b53b6ee2b8b6aa6336122306cfac8a17ba3d58" - integrity sha512-A/30vrtJO0xqctS/ngE1Lp/w3Aq3MPcpdRHU5E06EUYotzRzHFE9sNmezWslkZ3NfYwA/mxLvgmrsOJSR0Hx6A== +"@react-aria/meter@^3.4.26": + version "3.4.26" + resolved "https://registry.yarnpkg.com/@react-aria/meter/-/meter-3.4.26.tgz#99df9192ebf0db71acc300d05145ba8951982c8d" + integrity sha512-BI+Ri0dkhx9jjf6yPbOLl69M6808Fi08KNEmserMEapy++5usB/8krh9ARuR0GZYUPFOcny0Ml0or/HqamyFvw== dependencies: - "@react-aria/progress" "^3.4.22" - "@react-types/meter" "^3.4.8" - "@react-types/shared" "^3.29.0" + "@react-aria/progress" "^3.4.26" + "@react-types/meter" "^3.4.12" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/numberfield@^3.11.13": - version "3.11.13" - resolved "https://registry.yarnpkg.com/@react-aria/numberfield/-/numberfield-3.11.13.tgz#5b48764609f0cb7fa35c41879b48740cf4ebd1bf" - integrity sha512-F73BVdIRV8VvKl0omhGaf0E7mdJ7pdPjDP3wYNf410t55BXPxmndItUKpGfxSbl8k6ZYLvQyOqkD6oWSfZXpZw== +"@react-aria/numberfield@^3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@react-aria/numberfield/-/numberfield-3.12.1.tgz#97ca905f1b72fa275711b98e426d15095fc82ae4" + integrity sha512-3KjxGgWiF4GRvIyqrE3nCndkkEJ68v86y0nx89TpAjdzg7gCgdXgU2Lr4BhC/xImrmlqCusw0IBUMhsEq9EQWA== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/spinbutton" "^3.6.14" - "@react-aria/textfield" "^3.17.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/form" "^3.1.3" - "@react-stately/numberfield" "^3.9.11" - "@react-types/button" "^3.12.0" - "@react-types/numberfield" "^3.8.10" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/spinbutton" "^3.6.18" + "@react-aria/textfield" "^3.18.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/form" "^3.2.1" + "@react-stately/numberfield" "^3.10.1" + "@react-types/button" "^3.14.0" + "@react-types/numberfield" "^3.8.14" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/overlays@^3.23.2", "@react-aria/overlays@^3.27.0": - version "3.27.0" - resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.27.0.tgz#08788d80ff5fce428ca2d9856d08602f0c1eeb2a" - integrity sha512-2vZVgL7FrloN5Rh8sAhadGADJbuWg69DdSJB3fd2/h5VvcEhnIfNPu9Ma5XmdkApDoTboIEsKZ4QLYwRl98w6w== +"@react-aria/overlays@^3.23.2", "@react-aria/overlays@^3.29.1": + version "3.29.1" + resolved "https://registry.yarnpkg.com/@react-aria/overlays/-/overlays-3.29.1.tgz#1a43a29709ad10971116b3d77de4e9b02107193d" + integrity sha512-Yz92XNPnbrTnxrvNrY/fXJ3iWaYNrj0q24ddvZNNKDcWak0S1/mQeUwNb+PwS2AryhFU5VQqKz5rNsM96TKmPQ== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/ssr" "^3.9.8" - "@react-aria/utils" "^3.28.2" - "@react-aria/visually-hidden" "^3.8.22" - "@react-stately/overlays" "^3.6.15" - "@react-types/button" "^3.12.0" - "@react-types/overlays" "^3.8.14" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/ssr" "^3.9.10" + "@react-aria/utils" "^3.30.1" + "@react-aria/visually-hidden" "^3.8.27" + "@react-stately/overlays" "^3.6.19" + "@react-types/button" "^3.14.0" + "@react-types/overlays" "^3.9.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/progress@^3.4.22": - version "3.4.22" - resolved "https://registry.yarnpkg.com/@react-aria/progress/-/progress-3.4.22.tgz#e65c7c1bbac1be85205e96d5b243e295b16614d4" - integrity sha512-wK2hath4C9HKgmjCH+iSrAs86sUKqqsYKbEKk9/Rj9rzXqHyaEK9EG0YZDnSjd8kX+N9hYcs5MfJl6AZMH4juQ== +"@react-aria/progress@^3.4.26": + version "3.4.26" + resolved "https://registry.yarnpkg.com/@react-aria/progress/-/progress-3.4.26.tgz#6e3429499b16297d3029a1a71e84b570d765ee48" + integrity sha512-EJBzbE0IjXrJ19ofSyNKDnqC70flUM0Z+9heMRPLi6Uz01o6Uuz9tjyzmoPnd9Q1jnTT7dCl7ydhdYTGsWFcUg== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/label" "^3.7.17" - "@react-aria/utils" "^3.28.2" - "@react-types/progress" "^3.5.11" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/label" "^3.7.21" + "@react-aria/utils" "^3.30.1" + "@react-types/progress" "^3.5.15" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/radio@^3.11.2": - version "3.11.2" - resolved "https://registry.yarnpkg.com/@react-aria/radio/-/radio-3.11.2.tgz#5bffae601e3c7b25ca8e201491b4db125e005ba9" - integrity sha512-6AFJHXMewJBgHNhqkN1qjgwwx6kmagwYD+3Z+hNK1UHTsKe1Uud5/IF7gPFCqlZeKxA+Lvn9gWiqJrQbtD2+wg== +"@react-aria/radio@^3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@react-aria/radio/-/radio-3.12.1.tgz#6330e7da6458a9b50cf1626805304a57f719c4fc" + integrity sha512-feZdMJyNp+UX03seIX0W6gdUk8xayTY+U0Ct61eci6YXzyyZoL2PVh49ojkbyZ2UZA/eXeygpdF5sgQrKILHCA== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/form" "^3.0.15" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/utils" "^3.28.2" - "@react-stately/radio" "^3.10.12" - "@react-types/radio" "^3.8.8" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/form" "^3.1.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/utils" "^3.30.1" + "@react-stately/radio" "^3.11.1" + "@react-types/radio" "^3.9.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/searchfield@^3.8.3": - version "3.8.3" - resolved "https://registry.yarnpkg.com/@react-aria/searchfield/-/searchfield-3.8.3.tgz#d34dd617d0d6d6fc059e384979e30d1509e164c1" - integrity sha512-t1DW3nUkPHyZhFhUbT+TdhvI8yZYvUPCuwl0FyraMRCQ4+ww5Ieu4n8JB9IGYmIUB/GWEbZlDHplu4s3efmliA== +"@react-aria/searchfield@^3.8.8": + version "3.8.8" + resolved "https://registry.yarnpkg.com/@react-aria/searchfield/-/searchfield-3.8.8.tgz#1504b20d257981b4586d4f10b44398a2c48cde0c" + integrity sha512-Yn6esCYEym3Cwrh/OZt6o/RFzsG2zyCAEZf7BhWk6NWUvP6aPwHgoSDVSjDN6YnnPn4yMqkqPnZulHV4+MvE/w== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/textfield" "^3.17.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/searchfield" "^3.5.11" - "@react-types/button" "^3.12.0" - "@react-types/searchfield" "^3.6.1" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/textfield" "^3.18.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/searchfield" "^3.5.15" + "@react-types/button" "^3.14.0" + "@react-types/searchfield" "^3.6.5" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/select@^3.15.4": - version "3.15.4" - resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.15.4.tgz#6621441d23624add367b40cd1dfdd4a2a24ce3d8" - integrity sha512-CipqXgdOfWsiHw/chfqd8t9IQpvehP+3uKLJx3ic4Uyj+FT/SxVmmjX0gyvVbZd00ltFCMJYO2xYKQUlbW2AtQ== +"@react-aria/select@^3.16.2": + version "3.16.2" + resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.16.2.tgz#61850011221a62b9680a1ccfd4d2dfab25604e2d" + integrity sha512-MwsOJ6FfPxzrLP6spnYg2SUeGKNm4m5vyH6GebecLxTO1ee7/YyTNP1xkrQTqPMP9xx6uqhzFLFuCym2b6ripA== dependencies: - "@react-aria/form" "^3.0.15" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/listbox" "^3.14.3" - "@react-aria/menu" "^3.18.2" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-aria/visually-hidden" "^3.8.22" - "@react-stately/select" "^3.6.12" - "@react-types/button" "^3.12.0" - "@react-types/select" "^3.9.11" - "@react-types/shared" "^3.29.0" + "@react-aria/form" "^3.1.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/listbox" "^3.14.8" + "@react-aria/menu" "^3.19.2" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-aria/visually-hidden" "^3.8.27" + "@react-stately/select" "^3.7.1" + "@react-types/button" "^3.14.0" + "@react-types/select" "^3.10.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/selection@^3.24.0": - version "3.24.0" - resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.24.0.tgz#b8d93e514bccba0e8c2545564d7eb50023560c88" - integrity sha512-RfGXVc04zz41NVIW89/a3quURZ4LT/GJLkiajQK2VjhisidPdrAWkcfjjWJj0n+tm5gPWbi9Rs5R/Rc8mrvq8Q== +"@react-aria/selection@^3.25.1": + version "3.25.1" + resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.25.1.tgz#fdd7724bd251ee72082d3b62aa891ce8957d4e8a" + integrity sha512-HG+k3rDjuhnXPdVyv9CKiebee2XNkFYeYZBxEGlK3/pFVBzndnc8BXNVrXSgtCHLs2d090JBVKl1k912BPbj0Q== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/selection" "^3.20.1" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-stately/selection" "^3.20.5" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/separator@^3.4.8": - version "3.4.8" - resolved "https://registry.yarnpkg.com/@react-aria/separator/-/separator-3.4.8.tgz#385922853411cfae61ad85fd8538932d06f832a5" - integrity sha512-ncuOSTBF/qbNumnW/IRz+xyr+Ud85eCF0Expw4XWhKjAZfzJd86MxPY5ZsxE7pYLOcRWdOSIH1/obwwwSz8ALQ== +"@react-aria/separator@^3.4.12": + version "3.4.12" + resolved "https://registry.yarnpkg.com/@react-aria/separator/-/separator-3.4.12.tgz#4d6ccdf011dfb04fba2da9fdab8b70c2c7e77fbb" + integrity sha512-rvFCPdOPMQKY/Bpv2jNzXtetCuBLYSRCvpzam1LpMaEgwau5yECbId66+M2UX/cscPccKNU537SM6ei2j7RGog== dependencies: - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/slider@^3.7.18": - version "3.7.18" - resolved "https://registry.yarnpkg.com/@react-aria/slider/-/slider-3.7.18.tgz#9aba6bd82b12299b6409672db465fc346b4418f4" - integrity sha512-GBVv5Rpvj/6JH2LnF1zVAhBmxGiuq7R8Ekqyr5kBrCc2ToF3PrTjfGc/mlh0eEtbj+NvAcnlgTx1/qosYt1sGw== +"@react-aria/slider@^3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-aria/slider/-/slider-3.8.1.tgz#26cb2dee6c19c72e859bcd76f2e8973dcbe9a29a" + integrity sha512-uPgwZQrcuqHaLU2prJtPEPIyN9ugZ7qGgi0SB2U8tvoODNVwuPvOaSsvR98Mn6jiAzMFNoWMydeIi+J1OjvWsQ== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/utils" "^3.28.2" - "@react-stately/slider" "^3.6.3" - "@react-types/shared" "^3.29.0" - "@react-types/slider" "^3.7.10" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/utils" "^3.30.1" + "@react-stately/slider" "^3.7.1" + "@react-types/shared" "^3.32.0" + "@react-types/slider" "^3.8.1" "@swc/helpers" "^0.5.0" -"@react-aria/spinbutton@^3.6.14": - version "3.6.14" - resolved "https://registry.yarnpkg.com/@react-aria/spinbutton/-/spinbutton-3.6.14.tgz#ba0de579975ea1ba4744874bd29274c9f367c70d" - integrity sha512-oSKe9p0Q/7W39eXRnLxlwJG5dQo4ffosRT3u2AtOcFkk2Zzj+tSQFzHQ4202nrWdzRnQ2KLTgUUNnUvXf0BJcg== +"@react-aria/spinbutton@^3.6.18": + version "3.6.18" + resolved "https://registry.yarnpkg.com/@react-aria/spinbutton/-/spinbutton-3.6.18.tgz#cabc24687fab7c311b97c490faeced41688e43e2" + integrity sha512-dnmh7sNsprhYTpqCJhcuc9QJ9C/IG/o9TkgW5a9qcd2vS+dzEgqAiJKIMbJFG9kiJymv2NwIPysF12IWix+J3A== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/utils" "^3.28.2" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/live-announcer" "^3.4.4" + "@react-aria/utils" "^3.30.1" + "@react-types/button" "^3.14.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/ssr@^3.9.8": - version "3.9.8" - resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.8.tgz#9c06f1860abac629517898c1b5424be5d03bc112" - integrity sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw== +"@react-aria/ssr@^3.9.10": + version "3.9.10" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.10.tgz#7fdc09e811944ce0df1d7e713de1449abd7435e6" + integrity sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ== dependencies: "@swc/helpers" "^0.5.0" -"@react-aria/switch@^3.7.2": - version "3.7.2" - resolved "https://registry.yarnpkg.com/@react-aria/switch/-/switch-3.7.2.tgz#5c27500b29d161aed996de75acbd0ca42bce00e8" - integrity sha512-vaREbp1gFjv+jEMXoXpNK7JYFO/jhwnSYAwEINNWnwf54IGeHvTPaB2NwolYSFvP4HAj8TKYbGFUSz7RKLhLgw== +"@react-aria/switch@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@react-aria/switch/-/switch-3.7.7.tgz#10a800d059e887411ffde7b185616f83a75357a7" + integrity sha512-auV3g1qh+d/AZk7Idw2BOcYeXfCD9iDaiGmlcLJb9Eaz4nkq8vOkQxIXQFrn9Xhb+PfQzmQYKkt5N6P2ZNsw/g== dependencies: - "@react-aria/toggle" "^3.11.2" - "@react-stately/toggle" "^3.8.3" - "@react-types/shared" "^3.29.0" - "@react-types/switch" "^3.5.10" + "@react-aria/toggle" "^3.12.1" + "@react-stately/toggle" "^3.9.1" + "@react-types/shared" "^3.32.0" + "@react-types/switch" "^3.5.14" "@swc/helpers" "^0.5.0" -"@react-aria/table@^3.17.2": - version "3.17.2" - resolved "https://registry.yarnpkg.com/@react-aria/table/-/table-3.17.2.tgz#447dc14c2ebf9841a532182bacbdbd7cd2c983f8" - integrity sha512-wsF3JqiAKcol1sfeNqTxyzH6+nxu0sAfyuh+XQfp1tvSGx15NifYeNKovNX4EPpUVkAI7jL5Le+eYeYYGELfnw== +"@react-aria/table@^3.17.7": + version "3.17.7" + resolved "https://registry.yarnpkg.com/@react-aria/table/-/table-3.17.7.tgz#492bbdd6e815ce4a988824c83f6816250400c384" + integrity sha512-FxXryGTxePgh8plIxlOMwXdleGWjK52vsmbRoqz66lTIHMUMLTmmm+Y0V3lBOIoaW1rxvKcolYgS79ROnbDYBw== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/grid" "^3.13.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/live-announcer" "^3.4.2" - "@react-aria/utils" "^3.28.2" - "@react-aria/visually-hidden" "^3.8.22" - "@react-stately/collections" "^3.12.3" - "@react-stately/flags" "^3.1.1" - "@react-stately/table" "^3.14.1" - "@react-types/checkbox" "^3.9.3" - "@react-types/grid" "^3.3.1" - "@react-types/shared" "^3.29.0" - "@react-types/table" "^3.12.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/grid" "^3.14.4" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/live-announcer" "^3.4.4" + "@react-aria/utils" "^3.30.1" + "@react-aria/visually-hidden" "^3.8.27" + "@react-stately/collections" "^3.12.7" + "@react-stately/flags" "^3.1.2" + "@react-stately/table" "^3.15.0" + "@react-types/checkbox" "^3.10.1" + "@react-types/grid" "^3.3.5" + "@react-types/shared" "^3.32.0" + "@react-types/table" "^3.13.3" "@swc/helpers" "^0.5.0" -"@react-aria/tabs@^3.10.2": - version "3.10.2" - resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.10.2.tgz#ea81b9c68963f8016343daec77832be1be6a0129" - integrity sha512-rpEgh//Gnew3le49tQVFOQ6ZyacJdaNUDXHt0ocguXb+2UrKtH54M8oIAE7E8KaB1puQlFXRs+Rjlr1rOlmjEQ== +"@react-aria/tabs@^3.10.7": + version "3.10.7" + resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.10.7.tgz#0ff066fa3252af78af24e98690c7aa9e1b2ebfce" + integrity sha512-iA1M6H+N+9GggsEy/6MmxpMpeOocwYgFy2EoEl3it24RVccY6iZT4AweJq96s5IYga5PILpn7VVcpssvhkPgeA== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/tabs" "^3.8.1" - "@react-types/shared" "^3.29.0" - "@react-types/tabs" "^3.3.14" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/tabs" "^3.8.5" + "@react-types/shared" "^3.32.0" + "@react-types/tabs" "^3.3.18" "@swc/helpers" "^0.5.0" -"@react-aria/tag@^3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@react-aria/tag/-/tag-3.5.2.tgz#12ec45e7e147658ceb885b047d1ec1a2861fd5a8" - integrity sha512-xZ5Df0x+xcDg6UTDvnjP4pu+XrmYVaYcqzF7RGoCD1KyRCHU5Czg9p+888NB0K+vnJHfNsQh6rmMhDUydXu9eg== +"@react-aria/tag@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-aria/tag/-/tag-3.7.1.tgz#b9f018f345e54e5ab99008c2c2c2104c91e31c0b" + integrity sha512-VpF26ez+QmEzTK8E9tXZ4cofa1wocjnIo/Bd1LCXgLCytnHAkYGxeIRm5QbznJ0aF/9UgR1QtMqhyRrCZg9QqA== dependencies: - "@react-aria/gridlist" "^3.12.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/list" "^3.12.1" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@react-aria/gridlist" "^3.14.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/list" "^3.13.0" + "@react-types/button" "^3.14.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/textfield@^3.17.2": - version "3.17.2" - resolved "https://registry.yarnpkg.com/@react-aria/textfield/-/textfield-3.17.2.tgz#cf8c00e2aecaf461263a73eb476a0b518b1f74b8" - integrity sha512-4KINB0HueYUHUgvi/ThTP27hu4Mv5ujG55pH3dmSRD4Olu/MRy1m/Psq72o8LTf4bTOM9ZP1rKccUg6xfaMidA== +"@react-aria/textfield@^3.18.1": + version "3.18.1" + resolved "https://registry.yarnpkg.com/@react-aria/textfield/-/textfield-3.18.1.tgz#73f40b30b2e5c09af9c2970473397035e315da72" + integrity sha512-8yCoirnQzbbQgdk5J5bqimEu3GhHZ9FXeMHez1OF+H+lpTwyTYQ9XgioEN3HKnVUBNEufG4lYkQMxTKJdq1v9g== dependencies: - "@react-aria/form" "^3.0.15" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/utils" "^3.28.2" - "@react-stately/form" "^3.1.3" - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" - "@react-types/textfield" "^3.12.1" + "@react-aria/form" "^3.1.1" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/utils" "^3.30.1" + "@react-stately/form" "^3.2.1" + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" + "@react-types/textfield" "^3.12.5" "@swc/helpers" "^0.5.0" -"@react-aria/toast@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@react-aria/toast/-/toast-3.0.2.tgz#c92dae0c7044ae791027d85d76559619dd38f7aa" - integrity sha512-iaiHDE1CKYM3BbNEp3A2Ed8YAlpXUGyY6vesKISdHEZ2lJ7r+1hbcFoTNdG8HfbB8Lz5vw8Wd2o+ZmQ2tnDY9Q== +"@react-aria/toast@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@react-aria/toast/-/toast-3.0.7.tgz#bf72a7ef266affbb3a23e073c8e5afc1a4326aad" + integrity sha512-nuxPQ7wcSTg9UNMhXl9Uwyc5you/D1RfwymI3VDa5OGTZdJOmV2j94nyjBfMO2168EYMZjw+wEovvOZphs2Pbw== dependencies: - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/landmark" "^3.0.2" - "@react-aria/utils" "^3.28.2" - "@react-stately/toast" "^3.1.0" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/landmark" "^3.0.6" + "@react-aria/utils" "^3.30.1" + "@react-stately/toast" "^3.1.2" + "@react-types/button" "^3.14.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/toggle@^3.11.2": - version "3.11.2" - resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.11.2.tgz#ebfe73d07f13fc46421abc8d7f7d56a707f81b2f" - integrity sha512-JOg8yYYCjLDnEpuggPo9GyXFaT/B238d3R8i/xQ6KLelpi3fXdJuZlFD6n9NQp3DJbE8Wj+wM5/VFFAi3cISpw== +"@react-aria/toggle@^3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@react-aria/toggle/-/toggle-3.12.1.tgz#2a32b7e8e2cbc0a8494350ba4981000093af882e" + integrity sha512-XaFiRs1KEcIT6bTtVY/KTQxw4kinemj/UwXw2iJTu9XS43hhJ/9cvj8KzNGrKGqaxTpOYj62TnSHZbSiFViHDA== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/toggle" "^3.8.3" - "@react-types/checkbox" "^3.9.3" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-stately/toggle" "^3.9.1" + "@react-types/checkbox" "^3.10.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/toolbar@3.0.0-beta.15": - version "3.0.0-beta.15" - resolved "https://registry.yarnpkg.com/@react-aria/toolbar/-/toolbar-3.0.0-beta.15.tgz#2b85e9a1f3e9185447e7164736cab7a859ed5f25" - integrity sha512-PNGpNIKIsCW8rxI9XXSADlLrSpikILJKKECyTRw9KwvXDRc44pezvdjGHCNinQcKsQoy5BtkK5cTSAyVqzzTXQ== +"@react-aria/toolbar@3.0.0-beta.20": + version "3.0.0-beta.20" + resolved "https://registry.yarnpkg.com/@react-aria/toolbar/-/toolbar-3.0.0-beta.20.tgz#e1b56dde51c59dc331d93783869505fb225786f4" + integrity sha512-Kxvqw+TpVOE/eSi8RAQ9xjBQ2uXe8KkRvlRNQWQsrzkZDkXhzqGfQuJnBmozFxqpzSLwaVqQajHFUSvPAScT8Q== dependencies: - "@react-aria/focus" "^3.20.2" - "@react-aria/i18n" "^3.12.8" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@react-aria/focus" "^3.21.1" + "@react-aria/i18n" "^3.12.12" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/tooltip@^3.8.2": - version "3.8.2" - resolved "https://registry.yarnpkg.com/@react-aria/tooltip/-/tooltip-3.8.2.tgz#b19be87d6472719b9c55ed4be57948f952e9e4de" - integrity sha512-ctVTgh1LXvmr1ve3ehAWfvlJR7nHYZeqhl/g1qnA+983LQtc1IF9MraCs92g0m7KpBwCihuA+aYwTPsUHfKfXg== +"@react-aria/tooltip@^3.8.7": + version "3.8.7" + resolved "https://registry.yarnpkg.com/@react-aria/tooltip/-/tooltip-3.8.7.tgz#69138ba63ca4ecec903e13d99c9cbf74980e2280" + integrity sha512-Aj7DPJYGZ9/+2ZfhkvbN7YMeA5qu4oy4LVQiMCpqNwcFzvhTAVhN7J7cS6KjA64fhd1shKm3BZ693Ez6lSpqwg== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/tooltip" "^3.5.3" - "@react-types/shared" "^3.29.0" - "@react-types/tooltip" "^3.4.16" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-stately/tooltip" "^3.5.7" + "@react-types/shared" "^3.32.0" + "@react-types/tooltip" "^3.4.20" "@swc/helpers" "^0.5.0" -"@react-aria/tree@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@react-aria/tree/-/tree-3.0.2.tgz#a4a98d9077325f4a4c9567d6762ffb086786603c" - integrity sha512-gr06Y1760+kdlDeUcGNR+PCuJMtlrdtNMGG1Z0fSygy8y7/zVdTOLQp0c1Q3pjL2nr7Unjz/H1xSgERParHsbg== +"@react-aria/tree@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@react-aria/tree/-/tree-3.1.3.tgz#061f87c07709e3810de7e3b2ec6f70596e54f6ac" + integrity sha512-CWjIvJS540Kzzxs1f4fF0ajPUfYoeptcA6MmXHBlCKE2euRSvKW6F1ZhvLVq81YsYWuAfBKnG2/JsTgBZnGPVQ== dependencies: - "@react-aria/gridlist" "^3.12.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/selection" "^3.24.0" - "@react-aria/utils" "^3.28.2" - "@react-stately/tree" "^3.8.9" - "@react-types/button" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@react-aria/gridlist" "^3.14.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/selection" "^3.25.1" + "@react-aria/utils" "^3.30.1" + "@react-stately/tree" "^3.9.2" + "@react-types/button" "^3.14.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-aria/utils@^3.25.2", "@react-aria/utils@^3.28.2": - version "3.28.2" - resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.28.2.tgz#f698bc54b2cb506c2f81d1ce92543ae39aae3968" - integrity sha512-J8CcLbvnQgiBn54eeEvQQbIOfBF3A1QizxMw9P4cl9MkeR03ug7RnjTIdJY/n2p7t59kLeAB3tqiczhcj+Oi5w== +"@react-aria/utils@^3.25.2", "@react-aria/utils@^3.30.1": + version "3.30.1" + resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.30.1.tgz#9eb704d4193674816e1e0eab758b12c2d69d7b0b" + integrity sha512-zETcbDd6Vf9GbLndO6RiWJadIZsBU2MMm23rBACXLmpRztkrIqPEb2RVdlLaq1+GklDx0Ii6PfveVjx+8S5U6A== dependencies: - "@react-aria/ssr" "^3.9.8" - "@react-stately/flags" "^3.1.1" - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" + "@react-aria/ssr" "^3.9.10" + "@react-stately/flags" "^3.1.2" + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" clsx "^2.0.0" -"@react-aria/visually-hidden@^3.8.22": - version "3.8.22" - resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.8.22.tgz#949771f98717db7d1e9d3362341d155fb1e9668d" - integrity sha512-EO3R8YTKZ7HkLl9k1Y2uBKYBgpJagth4/4W7mfpJZE24A3fQnCP8zx1sweXiAm0mirR4J6tNaK7Ia8ssP5TpOw== +"@react-aria/visually-hidden@^3.8.27": + version "3.8.27" + resolved "https://registry.yarnpkg.com/@react-aria/visually-hidden/-/visually-hidden-3.8.27.tgz#5e73b761b2ea932b30f818f88c9b290e6437f991" + integrity sha512-hD1DbL3WnjPnCdlQjwe19bQVRAGJyN0Aaup+s7NNtvZUn7AjoEH78jo8TE+L8yM7z/OZUQF26laCfYqeIwWn4g== dependencies: - "@react-aria/interactions" "^3.25.0" - "@react-aria/utils" "^3.28.2" - "@react-types/shared" "^3.29.0" + "@react-aria/interactions" "^3.25.5" + "@react-aria/utils" "^3.30.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" "@react-icons/all-files@^4.1.0": @@ -4540,7 +4319,7 @@ "@react-spring/shared" "~9.7.5" "@react-spring/types" "~9.7.5" -"@react-spring/rafz@^9.7.5", "@react-spring/rafz@~9.7.5": +"@react-spring/rafz@~9.7.5": version "9.7.5" resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.5.tgz#ee7959676e7b5d6a3813e8c17d5e50df98b95df9" integrity sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw== @@ -4558,7 +4337,7 @@ resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.5.tgz#e5dd180f3ed985b44fd2cd2f32aa9203752ef3e8" integrity sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g== -"@react-spring/web@9.4.5 || ^9.7.2", "@react-spring/web@^9.7.5": +"@react-spring/web@9.4.5 || ^9.7.2": version "9.7.5" resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.5.tgz#7d7782560b3a6fb9066b52824690da738605de80" integrity sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ== @@ -4568,485 +4347,485 @@ "@react-spring/shared" "~9.7.5" "@react-spring/types" "~9.7.5" -"@react-stately/calendar@^3.8.0": - version "3.8.0" - resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.8.0.tgz#c8b051ef97d940eb4c1b4ac140a48b71868b628f" - integrity sha512-YAuJiR9EtVThX91gU2ay/6YgPe0LvZWEssu4BS0Atnwk5cAo32gvF5FMta9ztH1LIULdZFaypU/C1mvnayMf+Q== - dependencies: - "@internationalized/date" "^3.8.0" - "@react-stately/utils" "^3.10.6" - "@react-types/calendar" "^3.7.0" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/checkbox@^3.6.13": - version "3.6.13" - resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.6.13.tgz#7229a286b7f0af3154ca537a46cebd89c59c1460" - integrity sha512-b8+bkOhobzuJ5bAA16JpYg1tM973eNXD3U4h/8+dckLndKHRjIwPvrL25tzKN7NcQp2LKVCauFesgI+Z+/2FJg== - dependencies: - "@react-stately/form" "^3.1.3" - "@react-stately/utils" "^3.10.6" - "@react-types/checkbox" "^3.9.3" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/collections@^3.12.3": - version "3.12.3" - resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.12.3.tgz#2bdaea476068dcc44c8b62f1cac28f20f52df097" - integrity sha512-QfSBME2QWDjUw/RmmUjrYl/j1iCYcYCIDsgZda1OeRtt63R11k0aqmmwrDRwCsA+Sv+D5QgkOp4KK+CokTzoVQ== - dependencies: - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/color@^3.8.4": +"@react-stately/calendar@^3.8.4": version "3.8.4" - resolved "https://registry.yarnpkg.com/@react-stately/color/-/color-3.8.4.tgz#8d5436eb2322221d9e20087662ca66dc7632fbf4" - integrity sha512-LXmfnJPWnL5q1/Z8Pn2d+9efrClLWCiK6c3IGXN8ZWcdR/cMJ/w9SY9f7evyXvmeUmdU1FTGgoSVqGfup3tSyA== + resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.8.4.tgz#c64b29010dc0aba78592cb9a85a09b3ee5a49e17" + integrity sha512-q9mq0ydOLS5vJoHLnYfSCS/vppfjbg0XHJlAoPR+w+WpYZF4wPP453SrlX9T1DbxCEYFTpcxcMk/O8SDW3miAw== dependencies: - "@internationalized/number" "^3.6.1" - "@internationalized/string" "^3.2.6" - "@react-stately/form" "^3.1.3" - "@react-stately/numberfield" "^3.9.11" - "@react-stately/slider" "^3.6.3" - "@react-stately/utils" "^3.10.6" - "@react-types/color" "^3.0.4" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@react-stately/utils" "^3.10.8" + "@react-types/calendar" "^3.7.4" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/combobox@^3.10.4": - version "3.10.4" - resolved "https://registry.yarnpkg.com/@react-stately/combobox/-/combobox-3.10.4.tgz#15d36405b9711ba0536b0de012413b11d45c143f" - integrity sha512-sgujLhukIGKskLDrOL4SAbO7WOgLsD7gSdjRQZ0f/e8bWMmUOWEp22T+X1hMMcuVRkRdXlEF1kH2/E6BVanXYw== +"@react-stately/checkbox@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-stately/checkbox/-/checkbox-3.7.1.tgz#7a0a42f94705acc21ca702ad14b067a69cef4d47" + integrity sha512-ezfKRJsDuRCLtNoNOi9JXCp6PjffZWLZ/vENW/gbRDL8i46RKC/HpfJrJhvTPmsLYazxPC99Me9iq3v0VoNCsw== dependencies: - "@react-stately/collections" "^3.12.3" - "@react-stately/form" "^3.1.3" - "@react-stately/list" "^3.12.1" - "@react-stately/overlays" "^3.6.15" - "@react-stately/select" "^3.6.12" - "@react-stately/utils" "^3.10.6" - "@react-types/combobox" "^3.13.4" - "@react-types/shared" "^3.29.0" + "@react-stately/form" "^3.2.1" + "@react-stately/utils" "^3.10.8" + "@react-types/checkbox" "^3.10.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/data@^3.12.3": - version "3.12.3" - resolved "https://registry.yarnpkg.com/@react-stately/data/-/data-3.12.3.tgz#0f26b5658b49f1f3d1d478f36167e3a80abf62e3" - integrity sha512-JYPNV1gd9OZm8xPay0exx5okFNgiwESNvdBHsfDC+f8BifRyFLdrvoaUGF0enKIeSQMB1oReFAxTAXtDZd27rA== +"@react-stately/collections@^3.12.7": + version "3.12.7" + resolved "https://registry.yarnpkg.com/@react-stately/collections/-/collections-3.12.7.tgz#6e01a8988696e62301690eb7cd1497568236f222" + integrity sha512-0kQc0mI986GOCQHvRy4L0JQiotIK/KmEhR9Mu/6V0GoSdqg5QeUe4kyoNWj3bl03uQXme80v0L2jLHt+fOHHjA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/datepicker@^3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@react-stately/datepicker/-/datepicker-3.14.0.tgz#5f7e282d56e038ac0ef1b620fb9c1ff86253af57" - integrity sha512-JSkQfKW0+WpPQyOOeRPBLwXkVfpTUwgZJDnHBCud5kEuQiFFyeAIbL57RNXc4AX2pzY3piQa6OHnjDGTfqClxQ== +"@react-stately/color@^3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-stately/color/-/color-3.9.1.tgz#268d1ac94c0d5b67398ae62f2fefcd86846b9267" + integrity sha512-fCj7fFamyuQbL++MOcf4W4d4aFWXYWJ2UI1dKhrXdqVz/ly9CBVjy/MHKQ6xZX2tEiuoPX5NexfxzKKiozE50Q== dependencies: - "@internationalized/date" "^3.8.0" - "@internationalized/string" "^3.2.6" - "@react-stately/form" "^3.1.3" - "@react-stately/overlays" "^3.6.15" - "@react-stately/utils" "^3.10.6" - "@react-types/datepicker" "^3.12.0" - "@react-types/shared" "^3.29.0" + "@internationalized/number" "^3.6.5" + "@internationalized/string" "^3.2.7" + "@react-stately/form" "^3.2.1" + "@react-stately/numberfield" "^3.10.1" + "@react-stately/slider" "^3.7.1" + "@react-stately/utils" "^3.10.8" + "@react-types/color" "^3.1.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/disclosure@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@react-stately/disclosure/-/disclosure-3.0.3.tgz#abf55875ef2e4118e516219d5652918f6fbd9eac" - integrity sha512-4kB+WDXVcrxCmJ+X6c23wa5Ax5dPSpm6Ef8DktLrLcUfJyfr+SWs5/IfkrYG0sOl3/u5OwyWe1pq3hDpzyDlLA== - dependencies: - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/dnd@^3.5.3": - version "3.5.3" - resolved "https://registry.yarnpkg.com/@react-stately/dnd/-/dnd-3.5.3.tgz#4a97c8041bac9411270692e43fe08eb2b311b79f" - integrity sha512-e4IodPF7fv9hR6jqSjiyrrFQ/6NbHNM5Ft1MJzCu6tJHvT+sl6qxIP5A+XR3wkjMpi4QW2WhVUmoFNbS/6ZAug== - dependencies: - "@react-stately/selection" "^3.20.1" - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/flags@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@react-stately/flags/-/flags-3.1.1.tgz#c47d540c4196798f4cc0ee83f844099b4d57b876" - integrity sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg== - dependencies: - "@swc/helpers" "^0.5.0" - -"@react-stately/form@^3.1.3": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@react-stately/form/-/form-3.1.3.tgz#79d7bdef5a86540511294db9f74fb151a82456a9" - integrity sha512-Jisgm0facSS3sAzHfSgshoCo3LxfO0wmQj98MOBCGXyVL+MSwx2ilb38eXIyBCzHJzJnPRTLaK/E4T49aph47A== - dependencies: - "@react-types/shared" "^3.29.0" - "@swc/helpers" "^0.5.0" - -"@react-stately/grid@^3.11.1": +"@react-stately/combobox@^3.11.1": version "3.11.1" - resolved "https://registry.yarnpkg.com/@react-stately/grid/-/grid-3.11.1.tgz#ff704976ff552cb99f25c2a7286c531018494bee" - integrity sha512-xMk2YsaIKkF8dInRLUFpUXBIqnYt88hehhq2nb65RFgsFFhngE/OkaFudSUzaYPc1KvHpW+oHqvseC+G1iDG2w== + resolved "https://registry.yarnpkg.com/@react-stately/combobox/-/combobox-3.11.1.tgz#8bc48b9e9725db3382c6d1afa3946394fc097a20" + integrity sha512-ZZh+SaAmddoY+MeJr470oDYA0nGaJm4xoHCBapaBA0JNakGC/wTzF/IRz3tKQT2VYK4rumr1BJLZQydGp7zzeg== dependencies: - "@react-stately/collections" "^3.12.3" - "@react-stately/selection" "^3.20.1" - "@react-types/grid" "^3.3.1" - "@react-types/shared" "^3.29.0" + "@react-stately/collections" "^3.12.7" + "@react-stately/form" "^3.2.1" + "@react-stately/list" "^3.13.0" + "@react-stately/overlays" "^3.6.19" + "@react-stately/select" "^3.7.1" + "@react-stately/utils" "^3.10.8" + "@react-types/combobox" "^3.13.8" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/list@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.12.1.tgz#b439faa41a1fca08367c24b0925e3060d5037ecd" - integrity sha512-N+YCInNZ2OpY0WUNvJWUTyFHtzE5yBtZ9DI4EHJDvm61+jmZ2s3HszOfa7j+7VOKq78VW3m5laqsQNWvMrLFrQ== +"@react-stately/data@^3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@react-stately/data/-/data-3.14.0.tgz#224d1aca726cad79b05f32ee37356ce931b1356d" + integrity sha512-3GUsOXatYohBX2wTQHnJKVQlFfYXnt7IoDDuIaUeM8kXlF+dRSFAOAfPUSGAph6lJz2ht4dq1SEl6ZL/u+dRlQ== dependencies: - "@react-stately/collections" "^3.12.3" - "@react-stately/selection" "^3.20.1" - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/menu@^3.9.3": - version "3.9.3" - resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.9.3.tgz#b768dd9d4b7e047893aab5365dd5c3f335767ad9" - integrity sha512-9x1sTX3Xq2Q3mJUHV+YN9MR36qNzgn8eBSLa40eaFDaOOtoJ+V10m7OriUfpjey7WzLBpq00Sfda54/PbQHZ0g== +"@react-stately/datepicker@^3.15.1": + version "3.15.1" + resolved "https://registry.yarnpkg.com/@react-stately/datepicker/-/datepicker-3.15.1.tgz#ca18ba954c1c1e083fb31a00c3ea76f1ed7bafab" + integrity sha512-t64iYPms9y+MEQgOAu0XUHccbEXWVUWBHJWnYvAmILCHY8ZAOeSPAT1g4v9nzyiApcflSNXgpsvbs9BBEsrWww== dependencies: - "@react-stately/overlays" "^3.6.15" - "@react-types/menu" "^3.10.0" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@internationalized/string" "^3.2.7" + "@react-stately/form" "^3.2.1" + "@react-stately/overlays" "^3.6.19" + "@react-stately/utils" "^3.10.8" + "@react-types/datepicker" "^3.13.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/numberfield@^3.9.11": - version "3.9.11" - resolved "https://registry.yarnpkg.com/@react-stately/numberfield/-/numberfield-3.9.11.tgz#2805ac70bf7d95f6c5b2d9c468ee86c8ea0a4b2d" - integrity sha512-gAFSZIHnZsgIWVPgGRUUpfW6zM7TCV5oS1SCY90ay5nrS7JCXurQbMrWJLOWHTdM5iSeYMgoyt68OK5KD0KHMw== +"@react-stately/disclosure@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@react-stately/disclosure/-/disclosure-3.0.7.tgz#dd1d4451fb6368cbc1d7a7f6679599f1585a518d" + integrity sha512-ogM2y02uhpGfSOaBKIDz+hEha8qBH6WIRHRkoqdF4sEaR1kfq8LvBWdP1e/OcqHAhuRr28P2Rf0TDicnAnN7uA== dependencies: - "@internationalized/number" "^3.6.1" - "@react-stately/form" "^3.1.3" - "@react-stately/utils" "^3.10.6" - "@react-types/numberfield" "^3.8.10" + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/overlays@^3.6.15": - version "3.6.15" - resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.6.15.tgz#5eae748a58e182200b8f84893ab693b21e7231e6" - integrity sha512-LBaGpXuI+SSd5HSGzyGJA0Gy09V2tl2G/r0lllTYqwt0RDZR6p7IrhdGVXZm6vI0oWEnih7yLC32krkVQrffgQ== +"@react-stately/dnd@^3.7.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@react-stately/dnd/-/dnd-3.7.0.tgz#2607fad45e32ed40e40623b8b9cc7a2a1f265bfe" + integrity sha512-DddpCVkqt6vUPHLqe/2FHxW/gkR4tEt7W0MbFcCeCLbc9lmvzOClPwNpjmU/3UnU+vPQnwGGUeF3HvaxduUq2Q== dependencies: - "@react-stately/utils" "^3.10.6" - "@react-types/overlays" "^3.8.14" + "@react-stately/selection" "^3.20.5" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/radio@^3.10.12": - version "3.10.12" - resolved "https://registry.yarnpkg.com/@react-stately/radio/-/radio-3.10.12.tgz#79eb7d9263eed9b162b525dce35199d8414e2f95" - integrity sha512-hFH45CXVa7uyXeTYQy7LGR0SnmGnNRx7XnEXS25w4Ch6BpH8m8SAbhKXqysgcmsE3xrhRas7P9zWw7wI24G28Q== +"@react-stately/flags@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@react-stately/flags/-/flags-3.1.2.tgz#5c8e5ae416d37d37e2e583d2fcb3a046293504f2" + integrity sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg== dependencies: - "@react-stately/form" "^3.1.3" - "@react-stately/utils" "^3.10.6" - "@react-types/radio" "^3.8.8" - "@react-types/shared" "^3.29.0" "@swc/helpers" "^0.5.0" -"@react-stately/searchfield@^3.5.11": - version "3.5.11" - resolved "https://registry.yarnpkg.com/@react-stately/searchfield/-/searchfield-3.5.11.tgz#44f1e24ed5e4199d3f88802ae36813b717f44496" - integrity sha512-vOgK3kgkYcyjTLsBABVzoQL9w6qBamnWAQICcw5OkA6octnF7NZ5DqdjkwnMY95KOGchiTlD5tNNHrz0ekeGiw== +"@react-stately/form@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@react-stately/form/-/form-3.2.1.tgz#5c8caad11ea952f0fa1835ae31da67c208e96a54" + integrity sha512-btgOPXkwvd6fdWKoepy5Ue43o2932OSkQxozsR7US1ffFLcQc3SNlADHaRChIXSG8ffPo9t0/Sl4eRzaKu3RgQ== dependencies: - "@react-stately/utils" "^3.10.6" - "@react-types/searchfield" "^3.6.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/select@^3.6.12": - version "3.6.12" - resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.6.12.tgz#24bd59113f4bb999b943655793e985fdf3d44b52" - integrity sha512-5o/NAaENO/Gxs1yui5BHLItxLnDPSQJ5HDKycuD0/gGC17BboAGEY/F9masiQ5qwRPe3JEc0QfvMRq3yZVNXog== +"@react-stately/grid@^3.11.5": + version "3.11.5" + resolved "https://registry.yarnpkg.com/@react-stately/grid/-/grid-3.11.5.tgz#1e523cee3048cee0a812050fd5383c4eb3e0c0e3" + integrity sha512-4cNjGYaNkcVS2wZoNHUrMRICBpkHStYw57EVemP7MjiWEVu53kzPgR1Iwmti2WFCpi1Lwu0qWNeCfzKpXW4BTg== dependencies: - "@react-stately/form" "^3.1.3" - "@react-stately/list" "^3.12.1" - "@react-stately/overlays" "^3.6.15" - "@react-types/select" "^3.9.11" - "@react-types/shared" "^3.29.0" + "@react-stately/collections" "^3.12.7" + "@react-stately/selection" "^3.20.5" + "@react-types/grid" "^3.3.5" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/selection@^3.20.1": - version "3.20.1" - resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.20.1.tgz#a2a849dd443bc4cf898e0239ab1f25d83532143b" - integrity sha512-K9MP6Rfg2yvFoY2Cr+ykA7bP4EBXlGaq5Dqfa1krvcXlEgMbQka5muLHdNXqjzGgcwPmS1dx1NECD15q63NtOw== +"@react-stately/list@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.13.0.tgz#0429c212cbb674d585ec68a3d440300f60021c73" + integrity sha512-Panv8TmaY8lAl3R7CRhyUadhf2yid6VKsRDBCBB1FHQOOeL7lqIraz/oskvpabZincuaIUWqQhqYslC4a6dvuA== dependencies: - "@react-stately/collections" "^3.12.3" - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" + "@react-stately/collections" "^3.12.7" + "@react-stately/selection" "^3.20.5" + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/slider@^3.6.3": - version "3.6.3" - resolved "https://registry.yarnpkg.com/@react-stately/slider/-/slider-3.6.3.tgz#88a460be021fc6cc240a16b74943267d294520ae" - integrity sha512-755X1jhpRD1bqf/5Ax1xuSpZbnG/0EEHGOowH28FLYKy5+1l4QVDGPFYxLB9KzXPdRAr9EF0j2kRhH2d8MCksQ== +"@react-stately/menu@^3.9.7": + version "3.9.7" + resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.9.7.tgz#f7bab58d8ced5a2e4d960efbaf18dd27b13ac510" + integrity sha512-mfz1YoCgtje61AGxVdQaAFLlOXt9vV5dd1lQljYUPRafA/qu5Ursz4fNVlcavWW9GscebzFQErx+y0oSP7EUtQ== dependencies: - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" - "@react-types/slider" "^3.7.10" + "@react-stately/overlays" "^3.6.19" + "@react-types/menu" "^3.10.4" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/table@^3.14.1": - version "3.14.1" - resolved "https://registry.yarnpkg.com/@react-stately/table/-/table-3.14.1.tgz#fb86ca78ee5263220d2c562ff07849b3f081e493" - integrity sha512-7P5h4YBAv3B/7BGq/kln+xSKgJCSq4xjt4HmJA7ZkGnEksUPUokBNQdWwZsy3lX/mwunaaKR9x/YNIu7yXB02g== +"@react-stately/numberfield@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-stately/numberfield/-/numberfield-3.10.1.tgz#df6c790e56670ede8132e194ddb26a6fc4743bfe" + integrity sha512-lXABmcTneVvXYMGTgZvTCr4E+upOi7VRLL50ZzTMJqHwB/qlEQPAam3dmddQRwIsuCM3MEnL7bSZFFlSYAtkEw== dependencies: - "@react-stately/collections" "^3.12.3" - "@react-stately/flags" "^3.1.1" - "@react-stately/grid" "^3.11.1" - "@react-stately/selection" "^3.20.1" - "@react-stately/utils" "^3.10.6" - "@react-types/grid" "^3.3.1" - "@react-types/shared" "^3.29.0" - "@react-types/table" "^3.12.0" + "@internationalized/number" "^3.6.5" + "@react-stately/form" "^3.2.1" + "@react-stately/utils" "^3.10.8" + "@react-types/numberfield" "^3.8.14" "@swc/helpers" "^0.5.0" -"@react-stately/tabs@^3.8.1": - version "3.8.1" - resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.8.1.tgz#bf1f27fb59cf618f7ef01d1861e7c573ec552b5b" - integrity sha512-1TBbt2BXbemstb/gEYw/NVt3esi5WvgWQW5Z7G8nDzLkpnMHOZXueoUkMxsdm0vhE8p0M9fsJQCMXKvCG3JzJg== +"@react-stately/overlays@^3.6.19": + version "3.6.19" + resolved "https://registry.yarnpkg.com/@react-stately/overlays/-/overlays-3.6.19.tgz#8c5fbe20cc56c594a13ad19ae1a1ef424e67c954" + integrity sha512-swZXfDvxTYd7tKEpijEHBFFaEmbbnCvEhGlmrAz4K72cuRR9O5u+lcla8y1veGBbBSzrIdKNdBoIIJ+qQH+1TQ== dependencies: - "@react-stately/list" "^3.12.1" - "@react-types/shared" "^3.29.0" - "@react-types/tabs" "^3.3.14" + "@react-stately/utils" "^3.10.8" + "@react-types/overlays" "^3.9.1" "@swc/helpers" "^0.5.0" -"@react-stately/toast@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@react-stately/toast/-/toast-3.1.0.tgz#77a3a02a151fcd7103d103738bd3886229aaf576" - integrity sha512-9W2+evz+EARrjkR1QPLlOL5lcNpVo6PjMAIygRSaCPJ6ftQAZ6B+7xTFGPFabWh83gwXQDUgoSwC3/vosvxZaQ== +"@react-stately/radio@^3.11.1": + version "3.11.1" + resolved "https://registry.yarnpkg.com/@react-stately/radio/-/radio-3.11.1.tgz#a4c2eb9296699fed7040b97ccdf088d3d6355f66" + integrity sha512-ld9KWztI64gssg7zSZi9li21sG85Exb+wFPXtCim1TtpnEpmRtB05pXDDS3xkkIU/qOL4eMEnnLO7xlNm0CRIA== + dependencies: + "@react-stately/form" "^3.2.1" + "@react-stately/utils" "^3.10.8" + "@react-types/radio" "^3.9.1" + "@react-types/shared" "^3.32.0" + "@swc/helpers" "^0.5.0" + +"@react-stately/searchfield@^3.5.15": + version "3.5.15" + resolved "https://registry.yarnpkg.com/@react-stately/searchfield/-/searchfield-3.5.15.tgz#683b1f8a5726e0637a7c8f19fbe6c0f270bfb75e" + integrity sha512-6LVVvm6Z60fetYLLa4B2Q/BIY+fSSknLTw8sjlV+iDEPAknj7MqWtoLz2gSQRTFKvyO7ZCjJoar8ZU/JEqcm+w== + dependencies: + "@react-stately/utils" "^3.10.8" + "@react-types/searchfield" "^3.6.5" + "@swc/helpers" "^0.5.0" + +"@react-stately/select@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.7.1.tgz#2bb1a52f345b301a3073aa62dba4934697b5bf0c" + integrity sha512-vZt4j9yVyOTWWJoP9plXmYaPZH2uMxbjcGMDbiShwsFiK8C2m9b3Cvy44TZehfzCWzpMVR/DYxEYuonEIGA82Q== + dependencies: + "@react-stately/form" "^3.2.1" + "@react-stately/list" "^3.13.0" + "@react-stately/overlays" "^3.6.19" + "@react-types/select" "^3.10.1" + "@react-types/shared" "^3.32.0" + "@swc/helpers" "^0.5.0" + +"@react-stately/selection@^3.20.5": + version "3.20.5" + resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.20.5.tgz#11f4fae7b6038ca167a9bd1513a57c353d308bbd" + integrity sha512-YezWUNEn2pz5mQlbhmngiX9HqQsruLSXlkrAzB1DD6aliGrUvPKufTTGCixOaB8KVeCamdiFAgx1WomNplzdQA== + dependencies: + "@react-stately/collections" "^3.12.7" + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" + "@swc/helpers" "^0.5.0" + +"@react-stately/slider@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@react-stately/slider/-/slider-3.7.1.tgz#14f7b995eec95c3b25baa270480547afd4d62d51" + integrity sha512-J+G18m1bZBCNQSXhxGd4GNGDUVonv4Sg7fZL+uLhXUy1x71xeJfFdKaviVvZcggtl0/q5InW41PXho7EouMDEg== + dependencies: + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" + "@react-types/slider" "^3.8.1" + "@swc/helpers" "^0.5.0" + +"@react-stately/table@^3.15.0": + version "3.15.0" + resolved "https://registry.yarnpkg.com/@react-stately/table/-/table-3.15.0.tgz#eb84fcf530140fb2e269c22cef21bd3c3b1da1e2" + integrity sha512-KbvkrVF3sb25IPwyte9JcG5/4J7TgjHSsw7D61d/T/oUFMYPYVeolW9/2y+6u48WPkDJE8HJsurme+HbTN0FQA== + dependencies: + "@react-stately/collections" "^3.12.7" + "@react-stately/flags" "^3.1.2" + "@react-stately/grid" "^3.11.5" + "@react-stately/selection" "^3.20.5" + "@react-stately/utils" "^3.10.8" + "@react-types/grid" "^3.3.5" + "@react-types/shared" "^3.32.0" + "@react-types/table" "^3.13.3" + "@swc/helpers" "^0.5.0" + +"@react-stately/tabs@^3.8.5": + version "3.8.5" + resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.8.5.tgz#2447fd3aba1ea05ce9536f3626f9bdafb97bfd67" + integrity sha512-gdeI+NUH3hfqrxkJQSZkt+Zw4G2DrYJRloq/SGxu/9Bu5QD/U0psU2uqxQNtavW5qTChFK+D30rCPXpKlslWAA== + dependencies: + "@react-stately/list" "^3.13.0" + "@react-types/shared" "^3.32.0" + "@react-types/tabs" "^3.3.18" + "@swc/helpers" "^0.5.0" + +"@react-stately/toast@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@react-stately/toast/-/toast-3.1.2.tgz#0502040b6bd57479eaba1bca2f4c66e9e957e55a" + integrity sha512-HiInm7bck32khFBHZThTQaAF6e6/qm57F4mYRWdTq8IVeGDzpkbUYibnLxRhk0UZ5ybc6me+nqqPkG/lVmM42Q== dependencies: "@swc/helpers" "^0.5.0" use-sync-external-store "^1.4.0" -"@react-stately/toggle@^3.8.3": - version "3.8.3" - resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.8.3.tgz#181aaa4e4ecca970cc04d4189699c90f6f39006f" - integrity sha512-4T2V3P1RK4zEFz4vJjUXUXyB0g4Slm6stE6Ry20fzDWjltuW42cD2lmrd7ccTO/CXFmHLECcXQLD4GEbOj0epA== +"@react-stately/toggle@^3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.9.1.tgz#ae706abfebce11989ea370cdf54e821b70b6672f" + integrity sha512-L6yUdE8xZfQhw4aEFZduF8u4v0VrpYrwWEA4Tu/4qwGIPukH0wd2W21Zpw+vAiLOaDKnxel1nXX68MWnm4QXpw== dependencies: - "@react-stately/utils" "^3.10.6" - "@react-types/checkbox" "^3.9.3" - "@react-types/shared" "^3.29.0" + "@react-stately/utils" "^3.10.8" + "@react-types/checkbox" "^3.10.1" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/tooltip@^3.5.3": - version "3.5.3" - resolved "https://registry.yarnpkg.com/@react-stately/tooltip/-/tooltip-3.5.3.tgz#2960dd592ab1ae4cfdc02c9dda56968e17f93fc6" - integrity sha512-btfy/gQ3Eccudx//4HkyQ+CRr3vxbLs74HYHthaoJ9GZbRj/3XDzfUM2X16zRoqTZVrIz/AkUj7AfGfsitU5nQ== +"@react-stately/tooltip@^3.5.7": + version "3.5.7" + resolved "https://registry.yarnpkg.com/@react-stately/tooltip/-/tooltip-3.5.7.tgz#7c86942a8b5ee2fad890c09e01718a4939694e7f" + integrity sha512-GYh764BcYZz+Lclyutyir5I3elNo+vVNYzeNOKmPGZCE3p5B+/8lgZAHKxnRc9qmBlxvofnhMcuQxAPlBhoEkw== dependencies: - "@react-stately/overlays" "^3.6.15" - "@react-types/tooltip" "^3.4.16" + "@react-stately/overlays" "^3.6.19" + "@react-types/tooltip" "^3.4.20" "@swc/helpers" "^0.5.0" -"@react-stately/tree@^3.8.9": - version "3.8.9" - resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.8.9.tgz#03ace9eca113c42f797a149ee032e080887731c4" - integrity sha512-j/LLI9UvbqcfOdl2v9m3gET3etUxoQzv3XdryNAbSkg0jTx8/13Fgi/Xp98bUcNLfynfeGW5P/fieU71sMkGog== +"@react-stately/tree@^3.9.2": + version "3.9.2" + resolved "https://registry.yarnpkg.com/@react-stately/tree/-/tree-3.9.2.tgz#f9960d7e6a5418a637ad9ea17e607308fcd393e5" + integrity sha512-jsT1WZZhb7GRmg1iqoib9bULsilIK5KhbE8WrcfIml8NYr4usP4DJMcIYfRuiRtPLhKtUvHSoZ5CMbinPp8PUQ== dependencies: - "@react-stately/collections" "^3.12.3" - "@react-stately/selection" "^3.20.1" - "@react-stately/utils" "^3.10.6" - "@react-types/shared" "^3.29.0" + "@react-stately/collections" "^3.12.7" + "@react-stately/selection" "^3.20.5" + "@react-stately/utils" "^3.10.8" + "@react-types/shared" "^3.32.0" "@swc/helpers" "^0.5.0" -"@react-stately/utils@^3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.6.tgz#2ae25c2773e53a4ebdaf39264aa27145b758dc1b" - integrity sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA== +"@react-stately/utils@^3.10.8": + version "3.10.8" + resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.10.8.tgz#fdb9d172f7bbc2d083e69190f5ef0edfa4b4392f" + integrity sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g== dependencies: "@swc/helpers" "^0.5.0" -"@react-types/breadcrumbs@^3.7.12": - version "3.7.12" - resolved "https://registry.yarnpkg.com/@react-types/breadcrumbs/-/breadcrumbs-3.7.12.tgz#042c5e1b1d20b0ef6346365d7a5965bd6f7f7437" - integrity sha512-+LvGEADlv11mLQjxEAZriptSYJJTP+2OIFEKx0z9mmpp+8jTlEHFhAnRVaE6I9QCxcDB5F6q/olfizSwOPOMIg== +"@react-types/breadcrumbs@^3.7.16": + version "3.7.16" + resolved "https://registry.yarnpkg.com/@react-types/breadcrumbs/-/breadcrumbs-3.7.16.tgz#f9842047a1e7b4eac9d2ea911fd35e221d2a192f" + integrity sha512-4J+7b9y6z8QGZqvsBSWQfebx6aIbc+1unQqnZCAlJl9EGzlI6SGdXRsURGkOUGJCV2GqY8bSocc8AZbRXpQ0XQ== dependencies: - "@react-types/link" "^3.6.0" - "@react-types/shared" "^3.29.0" + "@react-types/link" "^3.6.4" + "@react-types/shared" "^3.32.0" -"@react-types/button@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.12.0.tgz#3e6957be95360124a1cad91cb5414099e3c78f83" - integrity sha512-YrASNa+RqGQpzJcxNAahzNuTYVID1OE6HCorrEOXIyGS3EGogHsQmFs9OyThXnGHq6q4rLlA806/jWbP9uZdxA== +"@react-types/button@^3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.14.0.tgz#8b7d1387960bd81ff2c0aa5565d3fb407f6a59b2" + integrity sha512-pXt1a+ElxiZyWpX0uznyjy5Z6EHhYxPcaXpccZXyn6coUo9jmCbgg14xR7Odo+JcbfaaISzZTDO7oGLVTcHnpA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/calendar@^3.7.0": - version "3.7.0" - resolved "https://registry.yarnpkg.com/@react-types/calendar/-/calendar-3.7.0.tgz#3e152d01e376256ccf54eb1b28c7520c8521fb22" - integrity sha512-RiEfX2ZTcvfRktQc5obOJtNTgW+UwjNOUW5yf9CLCNOSM07e0w5jtC1ewsOZZbcctMrMCljjL8niGWiBv1wQ1Q== +"@react-types/calendar@^3.7.4": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@react-types/calendar/-/calendar-3.7.4.tgz#8a630b75ce081bb656620ec64204a2b0b27ba84d" + integrity sha512-MZDyXtvdHl8CKQGYBkjYwc4ABBq6Mb4Fu7k/4boQAmMQ5Rtz29ouBCJrAs0BpR14B8ZMGzoNIolxS5RLKBmFSA== dependencies: - "@internationalized/date" "^3.8.0" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@react-types/shared" "^3.32.0" -"@react-types/checkbox@^3.9.3": - version "3.9.3" - resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.9.3.tgz#f74ed23f1d14c5003240ae08f8ef66610ec66eb5" - integrity sha512-h6wmK7CraKHKE6L13Ut+CtnjRktbMRhkCSorv7eg82M6p4PDhZ7mfDSh13IlGR4sryT8Ka+aOjOU+EvMrKiduA== +"@react-types/checkbox@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-types/checkbox/-/checkbox-3.10.1.tgz#6cd2e7dd4e27ba017593b8140d0571afcf2b7114" + integrity sha512-8ZqBoGBxtn6U/znpmyutGtBBaafUzcZnbuvYjwyRSONTrqQ0IhUq6jI/jbnE9r9SslIkbMB8IS1xRh2e63qmEQ== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/color@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@react-types/color/-/color-3.0.4.tgz#00d3ff6dbba261a83a15646e6d90a2a0b60148da" - integrity sha512-D6Uea8kYGaoZRHgemJ0b0+iXbrvABP8RzsctL8Yp5QVyGgYJDMO8/7eZ3tdtGs/V8Iv+yCzG4yBexPA95i6tEg== +"@react-types/color@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@react-types/color/-/color-3.1.1.tgz#18b4d809703bb37efbf96f0b11b2335a3c252edd" + integrity sha512-zBF1Op4AO3mlygUq2gFhEoK3gZp2HgwCMUKkCzoDbrvcaahhVbDbfhRxgXKM/2dg7WkgsqhokdkjYV2mGQadRQ== dependencies: - "@react-types/shared" "^3.29.0" - "@react-types/slider" "^3.7.10" + "@react-types/shared" "^3.32.0" + "@react-types/slider" "^3.8.1" -"@react-types/combobox@^3.13.4": - version "3.13.4" - resolved "https://registry.yarnpkg.com/@react-types/combobox/-/combobox-3.13.4.tgz#cc6b5fc5d5fa0d1018d878ac2b19485696e2b59b" - integrity sha512-4mX7eZ/Bv3YWzEzLEZAF/TfKM+I+SCsvnm/cHqOJq3jEE8aVU1ql4Q1+3+SvciX3pfFIfeKlu9S3oYKRT5WIgg== +"@react-types/combobox@^3.13.8": + version "3.13.8" + resolved "https://registry.yarnpkg.com/@react-types/combobox/-/combobox-3.13.8.tgz#de88e8eb51ec057f5004630668f910e1afd11c96" + integrity sha512-HGC3X9hmDRsjSZcFiflvJ7vbIgQ2gX/ZDxo1HVtvQqUDbgQCVakCcCdrB44aYgHFnyDiO6hyp7Y7jXtDBaEIIA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/datepicker@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@react-types/datepicker/-/datepicker-3.12.0.tgz#ae2a8e689e7a78fa967450154a2b7896796c1285" - integrity sha512-dw/xflOdQPQ3uEABaBrZRTvjsMRu5/VZjRx9ygc64sX2N7HKIt+foMPXKJ+1jhtki2p4gigNVjcnJndJHoj9SA== +"@react-types/datepicker@^3.13.1": + version "3.13.1" + resolved "https://registry.yarnpkg.com/@react-types/datepicker/-/datepicker-3.13.1.tgz#488989d4ff7605cf80af6fb0d31cb227456201d2" + integrity sha512-ub+g5pS3WOo5P/3FRNsQSwvlb9CuLl2m6v6KBkRXc5xqKhFd7UjvVpL6Oi/1zwwfow4itvD1t7l1XxgCo7wZ6Q== dependencies: - "@internationalized/date" "^3.8.0" - "@react-types/calendar" "^3.7.0" - "@react-types/overlays" "^3.8.14" - "@react-types/shared" "^3.29.0" + "@internationalized/date" "^3.9.0" + "@react-types/calendar" "^3.7.4" + "@react-types/overlays" "^3.9.1" + "@react-types/shared" "^3.32.0" -"@react-types/dialog@^3.5.17": - version "3.5.17" - resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.5.17.tgz#35897ebe2ddde1b814f968caa50ffcf97864324d" - integrity sha512-rKe2WrT272xuCH13euegBGjJAORYXJpHsX2hlu/f02TmMG4nSLss9vKBnY2N7k7nci65k5wDTW6lcsvQ4Co5zQ== +"@react-types/dialog@^3.5.21": + version "3.5.21" + resolved "https://registry.yarnpkg.com/@react-types/dialog/-/dialog-3.5.21.tgz#f47302e0a15e75dfe57e868f3496f5c0a0cecd7b" + integrity sha512-jF1gN4bvwYamsLjefaFDnaSKxTa3Wtvn5f7WLjNVZ8ICVoiMBMdUJXTlPQHAL4YWqtCj4hK/3uimR1E+Pwd7Xw== dependencies: - "@react-types/overlays" "^3.8.14" - "@react-types/shared" "^3.29.0" + "@react-types/overlays" "^3.9.1" + "@react-types/shared" "^3.32.0" -"@react-types/grid@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@react-types/grid/-/grid-3.3.1.tgz#8f41f5fb6d1c213b34bc80bc8416b1a837378f56" - integrity sha512-bPDckheJiHSIzSeSkLqrO6rXRLWvciFJr9rpCjq/+wBj6HsLh2iMpkB/SqmRHTGpPlJvlu0b7AlxK1FYE0QSKA== +"@react-types/grid@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@react-types/grid/-/grid-3.3.5.tgz#5764d4fed4a7f2a721bd77d1043e27686c5dbeee" + integrity sha512-hG6J2KDfmOHitkWoCa/9DvY1nTO2wgMIApcFoqLv7AWJr9CzvVqo5tIhZZCXiT1AvU2kafJxu9e7sr5GxAT2YA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/link@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@react-types/link/-/link-3.6.0.tgz#5bac8cb4fafb0b0f273f617f8522d02448c59609" - integrity sha512-BQ5Tktb+fUxvtqksAJZuP8Z/bpmnQ/Y/zgwxfU0OKmIWkKMUsXY+e0GBVxwFxeh39D77stpVxRsTl7NQrjgtSw== +"@react-types/link@^3.6.4": + version "3.6.4" + resolved "https://registry.yarnpkg.com/@react-types/link/-/link-3.6.4.tgz#99d6cc04e0d5e768c51dc1261fceeecf47557234" + integrity sha512-eLpIgOPf7GW4DpdMq8UqiRJkriend1kWglz5O9qU+/FM6COtvRnQkEeRhHICUaU2NZUvMRQ30KaGUo3eeZ6b+g== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/listbox@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.6.0.tgz#3016f170a67e9ac0190405a61f438406309dc5ff" - integrity sha512-+1ugDKTxson/WNOQZO4BfrnQ6cGDt+72mEytXMsSsd4aEC+x3RyUv6NKwdOl4n602cOreo0MHtap1X2BOACVoQ== +"@react-types/listbox@^3.7.3": + version "3.7.3" + resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.7.3.tgz#20946c0fd253be35e83180b21bd27676553ad0a3" + integrity sha512-ONgror9uyGmIer5XxpRRNcc8QFVWiOzINrMKyaS8G4l3aP52ZwYpRfwMAVtra8lkVNvXDmO7hthPZkB6RYdNOA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/menu@^3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.10.0.tgz#85b807ee348801ac59036b9aa6c7bae4d9a1cd36" - integrity sha512-DKMqEmUmarVCK0jblNkSlzSH53AAsxWCX9RaKZeP9EnRs2/l1oZRuiQVHlOQRgYwEigAXa2TrwcX4nnxZ+U36Q== +"@react-types/menu@^3.10.4": + version "3.10.4" + resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.10.4.tgz#57c0259b149f80fde9230cc1a494d6ac581d09b4" + integrity sha512-jCFVShLq3eASiuznenjoKBv3j0Jy2KQilAjBxdEp56WkZ5D338y/oY5zR6d25u9M0QslpI0DgwC8BwU7MCsPnw== dependencies: - "@react-types/overlays" "^3.8.14" - "@react-types/shared" "^3.29.0" + "@react-types/overlays" "^3.9.1" + "@react-types/shared" "^3.32.0" -"@react-types/meter@^3.4.8": - version "3.4.8" - resolved "https://registry.yarnpkg.com/@react-types/meter/-/meter-3.4.8.tgz#d7a9c56971226f1e1ccea264a164a634c0a4c8b7" - integrity sha512-uXmHdUDbAo7L3EkytrUrU6DLOFUt63s9QSTcDp+vwyWoshY4/4Dm4JARdmhJU2ZP1nb2Sy45ASeMvSBw3ia2oA== +"@react-types/meter@^3.4.12": + version "3.4.12" + resolved "https://registry.yarnpkg.com/@react-types/meter/-/meter-3.4.12.tgz#86d08d7ce462eb57127f562280b7a26ce0efa1a4" + integrity sha512-rx+yrwdesSabPworWRMpQnuT69gm8xt58cAfTDV9eSY1Jo+lO5OPp0OIyKb+U0q/whf60wnn2hsVnXm2fBXKhA== dependencies: - "@react-types/progress" "^3.5.11" + "@react-types/progress" "^3.5.15" -"@react-types/numberfield@^3.8.10": - version "3.8.10" - resolved "https://registry.yarnpkg.com/@react-types/numberfield/-/numberfield-3.8.10.tgz#56b42ebce833bb54feb24dadffdfb37e6089bc70" - integrity sha512-mdb4lMC4skO8Eqd0GeU4lJgDTEvqIhtINB5WCzLVZFrFVuxgWDoU5otsu0lbWhCnUA7XWQxupGI//TC1LLppjQ== - dependencies: - "@react-types/shared" "^3.29.0" - -"@react-types/overlays@^3.8.14": +"@react-types/numberfield@^3.8.14": version "3.8.14" - resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.8.14.tgz#75b5e27579bde3db4b231f6dbe230a5494531896" - integrity sha512-XJS67KHYhdMvPNHXNGdmc85gE+29QT5TwC58V4kxxHVtQh9fYzEEPzIV8K84XWSz04rRGe3fjDgRNbcqBektWQ== + resolved "https://registry.yarnpkg.com/@react-types/numberfield/-/numberfield-3.8.14.tgz#5a71fed846853176b262c4192025fd18157257dc" + integrity sha512-tlGEHJyeQSMlUoO4g9ekoELGJcqsjc/+/FAxo6YQMhQSkuIdkUKZg3UEBKzif4hLw787u80e1D0SxPUi3KO2oA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/progress@^3.5.11": - version "3.5.11" - resolved "https://registry.yarnpkg.com/@react-types/progress/-/progress-3.5.11.tgz#76af0ef249e1c54536429321f62abafe8d1ea9da" - integrity sha512-CysuMld/lycOckrnlvrlsVoJysDPeBnUYBChwtqwiv4ZNRXos+wgAL1ows6dl7Nr57/FH5B4v5gf9AHEo7jUvw== +"@react-types/overlays@^3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-types/overlays/-/overlays-3.9.1.tgz#b6e750d6e4bcf3aaf38fa8a5f69a817f37840934" + integrity sha512-UCG3TOu8FLk4j0Pr1nlhv0opcwMoqbGEOUvsSr6ITN6Qs2y0j+KYSYQ7a4+04m3dN//8+9Wjkkid8k+V1dV2CA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/radio@^3.8.8": - version "3.8.8" - resolved "https://registry.yarnpkg.com/@react-types/radio/-/radio-3.8.8.tgz#dbb3940408c38ed073ad441f05be4318e5013398" - integrity sha512-QfAIp+0CnRSnoRTJVXUEPi+9AvFvRzWLIKEnE9OmgXjuvJCU3QNiwd8NWjNeE+94QBEVvAZQcqGU+44q5poxNg== +"@react-types/progress@^3.5.15": + version "3.5.15" + resolved "https://registry.yarnpkg.com/@react-types/progress/-/progress-3.5.15.tgz#f676d003b9595d216a831eed58966ed91ceb305c" + integrity sha512-3SYvEyRt7vq7w0sc6wBYmkPqLMZbhH8FI3Lrnn9r3y8+69/efRjVmmJvwjm1z+c6rukszc2gCjUGTsMPQxVk2w== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/searchfield@^3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@react-types/searchfield/-/searchfield-3.6.1.tgz#ef0ebb9b94606647c5b5d6a374f28e9cf3e637a6" - integrity sha512-XR4tYktxHxGJufpO0MTAPknIbmN5eZqXCZwTdBS4tecihf9iGDsXmrBOs+M7LEnil67GaZcFrMhKxOMVpLwZAg== +"@react-types/radio@^3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@react-types/radio/-/radio-3.9.1.tgz#4d37e935cd3cc482a5dfaf92cf51068f07e70750" + integrity sha512-DUCN3msm8QZ0MJrP55FmqMONaadYq6JTxihYFGMLP+NoKRnkxvXqNZ2PlkAOLGy3y4RHOnOF8O1LuJqFCCuxDw== dependencies: - "@react-types/shared" "^3.29.0" - "@react-types/textfield" "^3.12.1" + "@react-types/shared" "^3.32.0" -"@react-types/select@^3.9.11": - version "3.9.11" - resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.9.11.tgz#6dbfcc82366d25edc2f9718e74088729dffb17ef" - integrity sha512-uEpQCgDlrq/5fW05FgNEsqsqpvZVKfHQO9Mp7OTqGtm4UBNAbcQ6hOV7MJwQCS25Lu2luzOYdgqDUN8eAATJVQ== +"@react-types/searchfield@^3.6.5": + version "3.6.5" + resolved "https://registry.yarnpkg.com/@react-types/searchfield/-/searchfield-3.6.5.tgz#de43905d990104d640cc507c728e5e12cd1013cf" + integrity sha512-5hI+Hb1U0bSxrJLvEwFEQfk7n3S+GO4c5W/0WZBG00YlYDY9asr1V0oU1WRmKPJJlRpyfG6PkMHDC3jhdj89ew== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" + "@react-types/textfield" "^3.12.5" -"@react-types/shared@^3.29.0": - version "3.29.0" - resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.29.0.tgz#f29bdad3bff1336aaa754d7abc420da2f014d931" - integrity sha512-IDQYu/AHgZimObzCFdNl1LpZvQW/xcfLt3v20sorl5qRucDVj4S9os98sVTZ4IRIBjmS+MkjqpR5E70xan7ooA== - -"@react-types/slider@^3.7.10": - version "3.7.10" - resolved "https://registry.yarnpkg.com/@react-types/slider/-/slider-3.7.10.tgz#fa3797a9a91670b8e3a6f763521058d4ac725db5" - integrity sha512-Yb8wbpu2gS7AwvJUuz0IdZBRi6eIBZq32BSss4UHX0StA8dtR1/K4JeTsArxwiA3P0BA6t0gbR6wzxCvVA9fRw== +"@react-types/select@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.10.1.tgz#4c19deb68250233d1acffc929b0beb1b04de2dd0" + integrity sha512-teANUr1byOzGsS/r2j7PatV470JrOhKP8En9lscfnqW5CeUghr+0NxkALnPkiEhCObi/Vu8GIcPareD0HNhtFA== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/switch@^3.5.10": - version "3.5.10" - resolved "https://registry.yarnpkg.com/@react-types/switch/-/switch-3.5.10.tgz#b099548d5671dfade4c715e8b492aa4a82e76e0e" - integrity sha512-YyNhx4CvuJ0Rvv7yMuQaqQuOIeg+NwLV00NHHJ+K0xEANSLcICLOLPNMOqRIqLSQDz5vDI705UKk8gVcxqPX5g== - dependencies: - "@react-types/shared" "^3.29.0" +"@react-types/shared@^3.32.0": + version "3.32.0" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.32.0.tgz#6c105ef05e1bd84ab04531e707074dc2a0b3ce07" + integrity sha512-t+cligIJsZYFMSPFMvsJMjzlzde06tZMOIOFa1OV5Z0BcMowrb2g4mB57j/9nP28iJIRYn10xCniQts+qadrqQ== -"@react-types/table@^3.12.0": - version "3.12.0" - resolved "https://registry.yarnpkg.com/@react-types/table/-/table-3.12.0.tgz#0163725f5672849ebbde5ae278989605a163b643" - integrity sha512-dmTzjCYwHf2HBOeTa/CEL177Aox0f0mkeLF5nQw/2z6SBolfmYoAwVTPxTaYFVu4MkEJxQTz9AuAsJvCbRJbhg== +"@react-types/slider@^3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@react-types/slider/-/slider-3.8.1.tgz#e2b1a11491cb6957584dc725513e574eddde4105" + integrity sha512-WxiQWj6iQr5Uft0/KcB9XSr361XnyTmL6eREZZacngA9CjPhRWYP3BRDPcCTuP7fj9Yi4QKMrryyjHqMHP8OKQ== dependencies: - "@react-types/grid" "^3.3.1" - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/tabs@^3.3.14": - version "3.3.14" - resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.3.14.tgz#e9d29fc780902c3b9ae4495a5a2f899585ef8ed7" - integrity sha512-/uKsA7L2dctKU0JEaBWerlX+3BoXpKUFr3kHpRUoH66DSGvAo34vZ7kv/BHMZifJenIbF04GhDBsGp1zjrQKBg== +"@react-types/switch@^3.5.14": + version "3.5.14" + resolved "https://registry.yarnpkg.com/@react-types/switch/-/switch-3.5.14.tgz#827e841b5f8948569650f7bac1784c685a7ab21e" + integrity sha512-M8kIv97i+ejCel4Ho+Y7tDbpOehymGwPA4ChxibeyD32+deyxu5B6BXxgKiL3l+oTLQ8ihLo3sRESdPFw8vpQg== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" -"@react-types/textfield@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@react-types/textfield/-/textfield-3.12.1.tgz#272f7f97e72dd4ec1debd5202e2f1c4296b65a18" - integrity sha512-6YTAMCKjEGuXg0A4bZA77j5QJ1a6yFviMUWsCIL6Dxq5K3TklzVsbAduSbHomPPuvkNTBSW4+TUJrVSnoTjMNA== +"@react-types/table@^3.13.3": + version "3.13.3" + resolved "https://registry.yarnpkg.com/@react-types/table/-/table-3.13.3.tgz#83fb11c63e71e726a63246dd3d12caa7c97e90d7" + integrity sha512-/kY/VlXN+8l9saySd6igcsDQ3x8pOVFJAWyMh6gOaOVN7HOJkTMIchmqS+ATa4nege8jZqcdzyGeAmv7mN655A== dependencies: - "@react-types/shared" "^3.29.0" + "@react-types/grid" "^3.3.5" + "@react-types/shared" "^3.32.0" -"@react-types/tooltip@^3.4.16": - version "3.4.16" - resolved "https://registry.yarnpkg.com/@react-types/tooltip/-/tooltip-3.4.16.tgz#25533d36dab850522b01b7dcfc3abe72a63d3a26" - integrity sha512-XEyKeqR3YxqJcR0cpigLGEBeRTEzrB0cu++IaADdqXJ8dBzS6s8y9EgR5UvKZmX1CQOBvMfXyYkj7nmJ039fOw== +"@react-types/tabs@^3.3.18": + version "3.3.18" + resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.3.18.tgz#c84bf3d113d606d2af8ba52f5465e8b6bb61b707" + integrity sha512-yX/AVlGS7VXCuy2LSm8y8nxUrKVBgnLv+FrtkLqf6jUMtD4KP3k1c4+GPHeScR0HcYzCQF7gCF3Skba1RdYoug== dependencies: - "@react-types/overlays" "^3.8.14" - "@react-types/shared" "^3.29.0" + "@react-types/shared" "^3.32.0" + +"@react-types/textfield@^3.12.5": + version "3.12.5" + resolved "https://registry.yarnpkg.com/@react-types/textfield/-/textfield-3.12.5.tgz#b9b4cd1b8f8b907d756c8323fb7fc2ac5b39a225" + integrity sha512-VXez8KIcop87EgIy00r+tb30xokA309TfJ32Qv5qOYB5SMqoHnb6SYvWL8Ih2PDqCo5eBiiGesSaWYrHnRIL8Q== + dependencies: + "@react-types/shared" "^3.32.0" + +"@react-types/tooltip@^3.4.20": + version "3.4.20" + resolved "https://registry.yarnpkg.com/@react-types/tooltip/-/tooltip-3.4.20.tgz#af71dde12afb8a33a7b5bb3d8ff0cda6c4ee3999" + integrity sha512-tF1yThwvgSgW8Gu/CLL0p92AUldHR6szlwhwW+ewT318sQlfabMGO4xlCNFdxJYtqTpEXk2rlaVrBuaC//du0w== + dependencies: + "@react-types/overlays" "^3.9.1" + "@react-types/shared" "^3.32.0" "@remix-run/router@1.23.0": version "1.23.0" @@ -5092,9 +4871,9 @@ resolve "^1.22.1" "@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0": - version "5.1.4" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a" - integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz#57ba1b0cbda8e7a3c597a4853c807b156e21a7b4" + integrity sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q== dependencies: "@types/estree" "^1.0.0" estree-walker "^2.0.2" @@ -5105,10 +4884,10 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@rushstack/eslint-patch@^1.10.3", "@rushstack/eslint-patch@^1.3.3": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz#75dce8e972f90bba488e2b0cc677fb233aa357ab" - integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ== +"@rushstack/eslint-patch@^1.10.3": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz#326a7b46f6d4cfa54ae25bb888551697873069b4" + integrity sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw== "@sapphire/utilities@^3.11.0": version "3.18.2" @@ -5171,6 +4950,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sinclair/typebox@^0.34.0": + version "0.34.41" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.41.tgz#aa51a6c1946df2c5a11494a2cdb9318e026db16c" + integrity sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g== + "@sinonjs/commons@^1.7.0": version "1.8.6" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" @@ -5606,9 +5390,9 @@ global "^4.4.0" "@storybook/client-logger@^6.4.0 || >=6.5.0-0": - version "8.6.12" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.6.12.tgz#50e1f693465122956107aad581d4826e354cf673" - integrity sha512-yvUfcH604qsX6s6O9n+U7giEw1MIFZqJ9Q3qvef05/EU4Xy8xdErol+q0wvLPI5O4/26AQMOB9EqavGqTFt6Ng== + version "8.6.14" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.6.14.tgz#2b067b606c4ad5c07458c9d76b0b3a63abe94f76" + integrity sha512-DqB54TiXr58nCS2QO2YxxY14uc0yGEhgTgtw1eIGFEC1RKxgbOos+4lDtwhU7EYI6kHewVlZ4NXi85wnj/Qq+A== "@storybook/components@6.5.16": version "6.5.16" @@ -5836,9 +5620,9 @@ global "^4.4.0" "@storybook/instrumenter@^6.4.0 || >=6.5.0-0": - version "8.6.12" - resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-8.6.12.tgz#7a99e061c3b9574b38d5677540852ba2b330e6a5" - integrity sha512-VK5fYAF8jMwWP/u3YsmSwKGh+FeSY8WZn78flzRUwirp2Eg1WWjsqPRubAk7yTpcqcC/km9YMF3KbqfzRv2s/A== + version "8.6.14" + resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-8.6.14.tgz#85bf47e34348f17dfbb99080312eefb2f535bd65" + integrity sha512-iG4MlWCcz1L7Yu8AwgsnfVAmMbvyRSk700Mfy2g4c8y5O+Cv1ejshE1LBBsCwHgkuqU0H4R0qu4g23+6UnUemQ== dependencies: "@storybook/global" "^5.0.0" "@vitest/utils" "^2.1.1" @@ -6254,18 +6038,11 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" -"@swc/counter@0.1.3", "@swc/counter@^0.1.3": +"@swc/counter@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/helpers@0.5.15": - version "0.5.15" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" - integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== - dependencies: - tslib "^2.8.0" - "@swc/helpers@0.5.5": version "0.5.5" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.5.tgz#12689df71bfc9b21c4f4ca00ae55f2f16c8b77c0" @@ -6288,41 +6065,34 @@ dependencies: remove-accents "0.5.0" -"@tanstack/query-core@5.74.3": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.74.3.tgz#1fc97bd9a47f2acdf9f49737b1e6969e7bbcb7d7" - integrity sha512-Mqk+5o3qTuAiZML248XpNH8r2cOzl15+LTbUsZQEwvSvn1GU4VQhvqzAbil36p+MBxpr/58oBSnRzhrBevDhfg== +"@tanstack/query-core@5.87.1": + version "5.87.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.87.1.tgz#9b8b9331714749d505a7ceb0ae52b6ad6ae8a461" + integrity sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA== -"@tanstack/query-devtools@5.73.3": - version "5.73.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.73.3.tgz#8fc9872ed4408964b2c560d811caee491cfbdc3c" - integrity sha512-hBQyYwsOuO7QOprK75NzfrWs/EQYjgFA0yykmcvsV62q0t6Ua97CU3sYgjHx0ZvxkXSOMkY24VRJ5uv9f5Ik4w== +"@tanstack/query-devtools@5.87.3": + version "5.87.3" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.87.3.tgz#0cfff30823f837d6b9ead08f8d1b16459203fdb2" + integrity sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg== "@tanstack/react-query-devtools@^5.64.2": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.74.3.tgz#3b079cd938b4f00d779e233e7fbb96c90264bf0f" - integrity sha512-H7TsOBB1fRCuuawrBzKMoIszqqILr2IN5oGLYMl7QG7ERJpMdc4hH8OwzBhVxJnmKeGwgtTQgcdKepfoJCWvFg== + version "5.87.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.87.3.tgz#f3681d5c228a7c9028015f7b16b3df17e640f8bb" + integrity sha512-uV7m4/m58jU4OaLEyiPLRoXnL5H5E598lhFLSXIcK83on+ZXW7aIfiu5kwRwe1qFa4X4thH8wKaxz1lt6jNmAA== dependencies: - "@tanstack/query-devtools" "5.73.3" + "@tanstack/query-devtools" "5.87.3" "@tanstack/react-query-next-experimental@^5.66.0": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-next-experimental/-/react-query-next-experimental-5.74.3.tgz#cb2a5018af39df3e4fb93706e5d481d76fe5dabd" - integrity sha512-SHU52CPTovF6RFupa3H9MHXSzdMeRv/gk9BwNximExKeRkS9v8B8EhQOs8oxXiKp13ScThmKAz8FXtKwlcTJqg== + version "5.87.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-next-experimental/-/react-query-next-experimental-5.87.1.tgz#5b2da5b4f937d7325443c938cb4e40e896ec33e7" + integrity sha512-AeZRThgx9cLkcRfE8W+qasgoyPvO4aJafdxqz+uHvCskjDkNt5Y3FAxuqIb9By1Z6VhA9ktyAEUgI/nqJGABiw== "@tanstack/react-query@^5.64.2": - version "5.74.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.74.3.tgz#f7acd825abaea091f009d1c3f115212e45c4ee74" - integrity sha512-QrycUn0wxjVPzITvQvOxFRdhlAwIoOQSuav7qWD4SWCoKCdLbyRZ2vji2GuBq/glaxbF4wBx3fqcYRDOt8KDTA== + version "5.87.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.87.1.tgz#074bd2238173f49ef8804a9b6d94f63374828a78" + integrity sha512-YKauf8jfMowgAqcxj96AHs+Ux3m3bWT1oSVKamaRPXSnW2HqSznnTCEkAVqctF1e/W9R/mPcyzzINIgpOH94qg== dependencies: - "@tanstack/query-core" "5.74.3" - -"@tanstack/react-table@8.20.5": - version "8.20.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.5.tgz#19987d101e1ea25ef5406dce4352cab3932449d8" - integrity sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA== - dependencies: - "@tanstack/table-core" "8.20.5" + "@tanstack/query-core" "5.87.1" "@tanstack/react-table@8.20.6": version "8.20.6" @@ -6338,13 +6108,6 @@ dependencies: "@tanstack/table-core" "8.21.3" -"@tanstack/react-virtual@3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.6.tgz#f90f97d50a8d83dcd3c3a2d425aadbb55d4837db" - integrity sha512-xaSy6uUxB92O8mngHZ6CvbhGuqxQ5lIZWCBy+FjhrbHmOwc6BnOnKkYm2FsB1/BpKw/+FVctlMbEtI+F6I1aJg== - dependencies: - "@tanstack/virtual-core" "3.10.6" - "@tanstack/react-virtual@3.11.2": version "3.11.2" resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" @@ -6353,11 +6116,11 @@ "@tanstack/virtual-core" "3.11.2" "@tanstack/react-virtual@^3.10.5": - version "3.13.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz#30243c8c3166673caf66bfbf5352e1b314a3a4cd" - integrity sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA== + version "3.13.12" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz#d372dc2783739cc04ec1a728ca8203937687a819" + integrity sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA== dependencies: - "@tanstack/virtual-core" "3.13.6" + "@tanstack/virtual-core" "3.13.12" "@tanstack/table-core@8.20.5": version "8.20.5" @@ -6369,125 +6132,120 @@ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== -"@tanstack/virtual-core@3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.6.tgz#babe3989b2344a5f12fc64129f9bbed5d3402999" - integrity sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw== - "@tanstack/virtual-core@3.11.2": version "3.11.2" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== -"@tanstack/virtual-core@3.13.6": - version "3.13.6" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz#329f962f1596b3280736c266a982897ed2112157" - integrity sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg== +"@tanstack/virtual-core@3.13.12": + version "3.13.12" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578" + integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA== -"@tauri-apps/api@^2.0.0", "@tauri-apps/api@^2.4.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.5.0.tgz#f1c67b5505ba689621b549f9d0165389337586dc" - integrity sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA== +"@tauri-apps/api@^2.4.0", "@tauri-apps/api@^2.6.0", "@tauri-apps/api@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.8.0.tgz#0348a2b3ba5982ec67a7d569f329b4a55d7d5f1e" + integrity sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw== -"@tauri-apps/cli-darwin-arm64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz#930ce605d5624e6a11ed9f57f17c605a4443a3f5" - integrity sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ== +"@tauri-apps/cli-darwin-arm64@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.8.4.tgz#ec09c0673dd1816e89910cf31ddc7d94c5cf077f" + integrity sha512-BKu8HRkYV01SMTa7r4fLx+wjgtRK8Vep7lmBdHDioP6b8XH3q2KgsAyPWfEZaZIkZ2LY4SqqGARaE9oilNe0oA== -"@tauri-apps/cli-darwin-x64@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz#f7272a8b79e66af95da62c7a3ae81a916864b35f" - integrity sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw== +"@tauri-apps/cli-darwin-x64@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.8.4.tgz#b9c274fedce570da1910559add68657d264019db" + integrity sha512-imb9PfSd/7G6VAO7v1bQ2A3ZH4NOCbhGJFLchxzepGcXf9NKkfun157JH9mko29K6sqAwuJ88qtzbKCbWJTH9g== -"@tauri-apps/cli-linux-arm-gnueabihf@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.5.0.tgz#faca4c693226b95c85662c6cbf55a715f86ddcc7" - integrity sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg== +"@tauri-apps/cli-linux-arm-gnueabihf@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.8.4.tgz#657131a05f422b9141277f0668d370e8d671bdc0" + integrity sha512-Ml215UnDdl7/fpOrF1CNovym/KjtUbCuPgrcZ4IhqUCnhZdXuphud/JT3E8X97Y03TZ40Sjz8raXYI2ET0exzw== -"@tauri-apps/cli-linux-arm64-gnu@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.5.0.tgz#7c49104baf401baae7286764516f38c950a5d394" - integrity sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ== +"@tauri-apps/cli-linux-arm64-gnu@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.8.4.tgz#35a14541e09b6548b811626d1a5d2574932116ef" + integrity sha512-pbcgBpMyI90C83CxE5REZ9ODyIlmmAPkkJXtV398X3SgZEIYy5TACYqlyyv2z5yKgD8F8WH4/2fek7+jH+ZXAw== -"@tauri-apps/cli-linux-arm64-musl@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz#c1f0943ccb0faea036a2fc854c44dce384d769e1" - integrity sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q== +"@tauri-apps/cli-linux-arm64-musl@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.8.4.tgz#bdd9deea17e0c2e4edf511a071c87670616af8a3" + integrity sha512-zumFeaU1Ws5Ay872FTyIm7z8kfzEHu8NcIn8M6TxbJs0a7GRV21KBdpW1zNj2qy7HynnpQCqjAYXTUUmm9JAOw== -"@tauri-apps/cli-linux-riscv64-gnu@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.5.0.tgz#1c9f3caf027c1b818253eaa4b24ef6f88bd30f46" - integrity sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA== +"@tauri-apps/cli-linux-riscv64-gnu@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.8.4.tgz#ac3c751ce5727fbd1da280f0aa2fb444fcd706b5" + integrity sha512-qiqbB3Zz6IyO201f+1ojxLj65WYj8mixL5cOMo63nlg8CIzsP23cPYUrx1YaDPsCLszKZo7tVs14pc7BWf+/aQ== -"@tauri-apps/cli-linux-x64-gnu@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.5.0.tgz#2835a941f1f2c50014a92cff00d2f9e6d1aaec0e" - integrity sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w== +"@tauri-apps/cli-linux-x64-gnu@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.8.4.tgz#7b2000b5e6597dc62f48cb67ee98f61b54493a19" + integrity sha512-TaqaDd9Oy6k45Hotx3pOf+pkbsxLaApv4rGd9mLuRM1k6YS/aw81YrsMryYPThrxrScEIUcmNIHaHsLiU4GMkw== -"@tauri-apps/cli-linux-x64-musl@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz#c1dbb908d6762928ba65c477d9cd8e9a1a7890cf" - integrity sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ== +"@tauri-apps/cli-linux-x64-musl@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.8.4.tgz#5dd9f6e666e004e00313d86a5d71480f7ac1269a" + integrity sha512-ot9STAwyezN8w+bBHZ+bqSQIJ0qPZFlz/AyscpGqB/JnJQVDFQcRDmUPFEaAtt2UUHSWzN3GoTJ5ypqLBp2WQA== -"@tauri-apps/cli-win32-arm64-msvc@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.5.0.tgz#5ff5045afef27314727a657429c71cc07cf2fd31" - integrity sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q== +"@tauri-apps/cli-win32-arm64-msvc@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.8.4.tgz#75eadbad9ae5726cc53139bbeae4c4a8fc4a92be" + integrity sha512-+2aJ/g90dhLiOLFSD1PbElXX3SoMdpO7HFPAZB+xot3CWlAZD1tReUFy7xe0L5GAR16ZmrxpIDM9v9gn5xRy/w== -"@tauri-apps/cli-win32-ia32-msvc@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.5.0.tgz#288fe9c6c5bb3ff94ac0a6c1d23985bf453eb873" - integrity sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg== +"@tauri-apps/cli-win32-ia32-msvc@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.8.4.tgz#60e6cdad4fb59d91a194652581742d08c952f0a7" + integrity sha512-yj7WDxkL1t9Uzr2gufQ1Hl7hrHuFKTNEOyascbc109EoiAqCp0tgZ2IykQqOZmZOHU884UAWI1pVMqBhS/BfhA== -"@tauri-apps/cli-win32-x64-msvc@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.5.0.tgz#9e398e07f8a255aaa3400b1dc43fb985bb99cdb0" - integrity sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ== +"@tauri-apps/cli-win32-x64-msvc@2.8.4": + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.8.4.tgz#44461625197c531537ccba84b64c804a0d7228ae" + integrity sha512-XuvGB4ehBdd7QhMZ9qbj/8icGEatDuBNxyYHbLKsTYh90ggUlPa/AtaqcC1Fo69lGkTmq9BOKrs1aWSi7xDonA== "@tauri-apps/cli@^2.4.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.5.0.tgz#828cefd5c64be9f96a0379583f9ad25e17c43208" - integrity sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg== + version "2.8.4" + resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.8.4.tgz#ff8347e43164f73922356c10ba5cdbaf567ffe98" + integrity sha512-ejUZBzuQRcjFV+v/gdj/DcbyX/6T4unZQjMSBZwLzP/CymEjKcc2+Fc8xTORThebHDUvqoXMdsCZt8r+hyN15g== optionalDependencies: - "@tauri-apps/cli-darwin-arm64" "2.5.0" - "@tauri-apps/cli-darwin-x64" "2.5.0" - "@tauri-apps/cli-linux-arm-gnueabihf" "2.5.0" - "@tauri-apps/cli-linux-arm64-gnu" "2.5.0" - "@tauri-apps/cli-linux-arm64-musl" "2.5.0" - "@tauri-apps/cli-linux-riscv64-gnu" "2.5.0" - "@tauri-apps/cli-linux-x64-gnu" "2.5.0" - "@tauri-apps/cli-linux-x64-musl" "2.5.0" - "@tauri-apps/cli-win32-arm64-msvc" "2.5.0" - "@tauri-apps/cli-win32-ia32-msvc" "2.5.0" - "@tauri-apps/cli-win32-x64-msvc" "2.5.0" + "@tauri-apps/cli-darwin-arm64" "2.8.4" + "@tauri-apps/cli-darwin-x64" "2.8.4" + "@tauri-apps/cli-linux-arm-gnueabihf" "2.8.4" + "@tauri-apps/cli-linux-arm64-gnu" "2.8.4" + "@tauri-apps/cli-linux-arm64-musl" "2.8.4" + "@tauri-apps/cli-linux-riscv64-gnu" "2.8.4" + "@tauri-apps/cli-linux-x64-gnu" "2.8.4" + "@tauri-apps/cli-linux-x64-musl" "2.8.4" + "@tauri-apps/cli-win32-arm64-msvc" "2.8.4" + "@tauri-apps/cli-win32-ia32-msvc" "2.8.4" + "@tauri-apps/cli-win32-x64-msvc" "2.8.4" "@tauri-apps/plugin-clipboard-manager@^2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.2.2.tgz#19559f2b80dd6c692b4e7b884515b5b693ca9791" - integrity sha512-bZvDLMqfcNmsw7Ag8I49jlaCjdpDvvlJHnpp6P+Gg/3xtpSERdwlDxm7cKGbs2mj46dsw4AuG3RoAgcpwgioUA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.3.0.tgz#1d0dea11aa88970cf94a153a052cb0d6634bcacd" + integrity sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA== dependencies: - "@tauri-apps/api" "^2.0.0" + "@tauri-apps/api" "^2.6.0" "@tauri-apps/plugin-opener@^2.2.6": - version "2.2.6" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz#a4dc328541708d40e3bb969231974353a4ad6983" - integrity sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w== + version "2.5.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-opener/-/plugin-opener-2.5.0.tgz#f4ed07559065616bf14012686f5273070a39e003" + integrity sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA== dependencies: - "@tauri-apps/api" "^2.0.0" + "@tauri-apps/api" "^2.8.0" "@tauri-apps/plugin-shell@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.2.1.tgz#586ab725ef622ba65a946bff1a3e166cee181903" - integrity sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.3.1.tgz#e92fb07e7bcf48ba647e5c8ef78e2ea8b469db15" + integrity sha512-jjs2WGDO/9z2pjNlydY/F5yYhNsscv99K5lCmU5uKjsVvQ3dRlDhhtVYoa4OLDmktLtQvgvbQjCFibMl6tgGfw== dependencies: - "@tauri-apps/api" "^2.0.0" + "@tauri-apps/api" "^2.8.0" "@tauri-apps/plugin-updater@^2.0.0": - version "2.7.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.7.0.tgz#a4390bdfd858106bdc6cc3d05d52efc9e0528ea3" - integrity sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ== + version "2.9.0" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.9.0.tgz#ba50b4e644fe19fa6f8465bd86d48119b0d3f41c" + integrity sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg== dependencies: - "@tauri-apps/api" "^2.0.0" + "@tauri-apps/api" "^2.6.0" "@tauri-apps/tauri-forage@^1.0.0-beta.2": version "1.0.0-beta.2" @@ -6559,26 +6317,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - "@tufjs/canonical-json@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz#eade9fd1f537993bc1f0949f3aea276ecc4fab31" @@ -6592,10 +6330,10 @@ "@tufjs/canonical-json" "1.0.0" minimatch "^9.0.0" -"@tybys/wasm-util@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" - integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== +"@tybys/wasm-util@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369" + integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ== dependencies: tslib "^2.4.0" @@ -6631,11 +6369,11 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz#968cdc2366ec3da159f61166428ee40f370e56c2" - integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng== + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== dependencies: - "@babel/types" "^7.20.7" + "@babel/types" "^7.28.2" "@types/big.js@^6.1.6": version "6.2.2" @@ -6643,9 +6381,9 @@ integrity sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA== "@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + version "1.19.6" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" "@types/node" "*" @@ -6685,16 +6423,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-color@*", "@types/d3-color@^3.0.0", "@types/d3-color@^3.1.3": +"@types/d3-color@*", "@types/d3-color@^3.0.0": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== -"@types/d3-color@^2": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.6.tgz#88a9a06afea2d3a400ea650a6c3e60e9bf9b0f2a" - integrity sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg== - "@types/d3-delaunay@^6.0.4": version "6.0.4" resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" @@ -6710,21 +6443,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.4.5.tgz#6392303c2ca3c287c3a1a2046455cd0a0bd50bbe" integrity sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA== -"@types/d3-geo@^2": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-2.0.7.tgz#7ad48988e9af14e1e8490e25e926e5050992b397" - integrity sha512-RIXlxPdxvX+LAZFv+t78CuYpxYag4zuw9mZc+AwfB8tZpKU90rMEn2il2ADncmeZlb7nER9dDsJpRisA3lRvjA== - dependencies: - "@types/geojson" "*" - -"@types/d3-interpolate@^2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz#063a3b81cda9f74660cc8ac4533854d5b45ff12f" - integrity sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg== - dependencies: - "@types/d3-color" "^2" - -"@types/d3-interpolate@^3.0.1", "@types/d3-interpolate@^3.0.4": +"@types/d3-interpolate@^3.0.1": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== @@ -6748,11 +6467,6 @@ dependencies: "@types/d3-time" "*" -"@types/d3-selection@^2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-2.0.5.tgz#360d92460947b272362d3be340e494f2e55c6171" - integrity sha512-71BorcY0yXl12S7lvb01JdaN9TpeUHBDb4RRhSq8U8BEkX/nIk5p7Byho+ZRTsx5nYLMpAbY3qt5EhqFzfGJlw== - "@types/d3-shape@^3.1.0", "@types/d3-shape@^3.1.6": version "3.1.7" resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.7.tgz#2b7b423dc2dfe69c8c93596e673e37443348c555" @@ -6770,7 +6484,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-3.0.4.tgz#f972bdd7be1048184577cf235a44721a78c6bb4b" integrity sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg== -"@types/d3-time@*", "@types/d3-time@^3.0.0", "@types/d3-time@^3.0.3": +"@types/d3-time@*", "@types/d3-time@^3.0.0": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== @@ -6785,14 +6499,6 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== -"@types/d3-zoom@^2": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-2.0.7.tgz#bd0c7376c53f90be28340507f519bc91a4736120" - integrity sha512-JWke4E8ZyrKUQ68ESTWSK16fVb0OYnaiJ+WXJRYxKLn4aXU0o4CLYxMWBEiouUfO3TTCoyroOrGPcBG6u1aAxA== - dependencies: - "@types/d3-interpolate" "^2" - "@types/d3-selection" "^2" - "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -6823,10 +6529,10 @@ dependencies: "@types/estree" "*" -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" - integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/estree@^0.0.51": version "0.0.51" @@ -6834,9 +6540,9 @@ integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": - version "5.0.6" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz#41fec4ea20e9c7b22f024ab88a95c6bb288f51b8" - integrity sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA== + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6" + integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ== dependencies: "@types/node" "*" "@types/qs" "*" @@ -6854,18 +6560,18 @@ "@types/send" "*" "@types/express@*": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.1.tgz#138d741c6e5db8cc273bec5285cd6e9d0779fc9f" - integrity sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ== + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956" + integrity sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^5.0.0" "@types/serve-static" "*" "@types/express@^4.17.13": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + version "4.17.23" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.23.tgz#35af3193c640bfd4d7fe77191cd0ed411a433bef" + integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" @@ -6884,18 +6590,12 @@ resolved "https://registry.yarnpkg.com/@types/flat/-/flat-5.0.5.tgz#2304df0b2b1e6dde50d81f029593e0a1bc2474d3" integrity sha512-nPLljZQKSnac53KDUDzuzdRfGI0TDb5qPrb+SrQyN3MtdQrOnGsKniHN1iYZsJEBIVQve94Y6gNz22sgISZq+Q== -"@types/geojson@*": - version "7946.0.16" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" - integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== - "@types/glob@*": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" - integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-9.0.0.tgz#7b942fafe09c55671912b34f04e8e4676faf32b1" + integrity sha512-00UxlRaIUvYm4R4W9WYkN8/J+kV8fmOQ7okeH6YFtGWFMt3odD45tpG5yA5wnL7HE6lLgjaTW5n14ju2hl2NNA== dependencies: - "@types/minimatch" "^5.1.2" - "@types/node" "*" + glob "*" "@types/glob@^7.1.1": version "7.2.0" @@ -6937,9 +6637,9 @@ integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== "@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== "@types/http-proxy@^1.17.8": version "1.17.16" @@ -6953,7 +6653,7 @@ resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.3.tgz#548f851db5d30a12abeea2569ba75890dbf89425" integrity sha512-/CLhCW79JUeLKznI6mbVieGbl4QU5Hfn+6udw1YHZoofASjbQ5zaP5LzAUZYDpRYEjS4/P+DhEgyJ/PQmGGTWw== -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== @@ -6965,7 +6665,7 @@ dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^3.0.0": +"@types/istanbul-reports@^3.0.0", "@types/istanbul-reports@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== @@ -6973,12 +6673,12 @@ "@types/istanbul-lib-report" "*" "@types/jest@*": - version "29.5.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" - integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + version "30.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" + expect "^30.0.0" + pretty-format "^30.0.0" "@types/jest@^27.0.1": version "27.5.2" @@ -6988,7 +6688,7 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -6999,9 +6699,9 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/lodash@^4.14.167", "@types/lodash@^4.14.175": - version "4.17.16" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a" - integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + version "4.17.20" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93" + integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA== "@types/long@^4.0.1": version "4.0.2" @@ -7027,10 +6727,12 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== -"@types/minimatch@*", "@types/minimatch@^5.1.2": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" - integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== +"@types/minimatch@*": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-6.0.0.tgz#4d207b1cc941367bdcd195a3a781a7e4fc3b1e03" + integrity sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA== + dependencies: + minimatch "*" "@types/minimatch@^3.0.3": version "3.0.5" @@ -7048,26 +6750,26 @@ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node-fetch@^2.5.7": - version "2.6.12" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" - integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + version "2.6.13" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee" + integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw== dependencies: "@types/node" "*" - form-data "^4.0.0" + form-data "^4.0.4" "@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + version "1.3.14" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" + integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== dependencies: "@types/node" "*" "@types/node@*", "@types/node@>=13.7.0": - version "22.14.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.1.tgz#53b54585cec81c21eee3697521e31312d6ca1e6f" - integrity sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw== + version "24.3.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.1.tgz#b0a3fb2afed0ef98e8d7f06d46ef6349047709f3" + integrity sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g== dependencies: - undici-types "~6.21.0" + undici-types "~7.10.0" "@types/node@10.12.18": version "10.12.18" @@ -7080,11 +6782,11 @@ integrity sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw== "@types/node@^20": - version "20.17.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.30.tgz#1d93f656d3b869dbef7b796568ac457606ba58d0" - integrity sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg== + version "20.19.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.13.tgz#b79004a05068e28fb2de281b3a44c5c993650e59" + integrity sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g== dependencies: - undici-types "~6.19.2" + undici-types "~6.21.0" "@types/normalize-package-data@^2.4.0": version "2.4.4" @@ -7118,10 +6820,10 @@ resolved "https://registry.yarnpkg.com/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#ee1bd8c9f7a01b3445786aad0ef23aba5f511a44" integrity sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA== -"@types/prop-types@*", "@types/prop-types@^15.7.12", "@types/prop-types@^15.7.14", "@types/prop-types@^15.7.2": - version "15.7.14" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" - integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== +"@types/prop-types@*", "@types/prop-types@^15.7.12", "@types/prop-types@^15.7.14", "@types/prop-types@^15.7.15", "@types/prop-types@^15.7.2": + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== "@types/qrcode.react@^1.0.2": version "1.0.5" @@ -7131,9 +6833,9 @@ "@types/react" "*" "@types/qs@*", "@types/qs@^6.9.18", "@types/qs@^6.9.5": - version "6.9.18" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.18.tgz#877292caa91f7c1b213032b34626505b746624c2" - integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA== + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" @@ -7146,19 +6848,9 @@ integrity sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg== "@types/react-dom@^18", "@types/react-dom@^18.0.10": - version "18.3.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.6.tgz#fa59a5e9a33499a792af6c1130f55921ef49d268" - integrity sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw== - -"@types/react-simple-maps@^3.0.6": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@types/react-simple-maps/-/react-simple-maps-3.0.6.tgz#96728a17d3808cc17072db687b083872dc24fb26" - integrity sha512-hR01RXt6VvsE41FxDd+Bqm1PPGdKbYjCYVtCgh38YeBPt46z3SwmWPWu2L3EdCAP6bd6VYEgztucihRw1C0Klg== - dependencies: - "@types/d3-geo" "^2" - "@types/d3-zoom" "^2" - "@types/geojson" "*" - "@types/react" "*" + version "18.3.7" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f" + integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ== "@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.11", "@types/react-transition-group@^4.4.12": version "4.4.12" @@ -7166,16 +6858,16 @@ integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== "@types/react@*": - version "19.1.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.2.tgz#11df86f66f188f212c90ecb537327ec68bfd593f" - integrity sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw== + version "19.1.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.12.tgz#7bfaa76aabbb0b4fe0493c21a3a7a93d33e8937b" + integrity sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w== dependencies: csstype "^3.0.2" "@types/react@^18", "@types/react@^18.0.26": - version "18.3.20" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.20.tgz#b0dccda9d2f1bc24d2a04b1d0cb5d0b9a3576ad3" - integrity sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg== + version "18.3.24" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.24.tgz#f6a5a4c613242dfe3af0dcee2b4ec47b92d9b6bd" + integrity sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -7191,14 +6883,14 @@ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/semver@^7.3.12", "@types/semver@^7.3.8": - version "7.7.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" - integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA== + version "7.7.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528" + integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA== "@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + version "0.17.5" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.5.tgz#d991d4f2b16f2b1ef497131f00a9114290791e74" + integrity sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w== dependencies: "@types/mime" "^1" "@types/node" "*" @@ -7211,9 +6903,9 @@ "@types/express" "*" "@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.8.tgz#8180c3fbe4a70e8f00b9f70b9ba7f08f35987877" + integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== dependencies: "@types/http-errors" "*" "@types/node" "*" @@ -7231,7 +6923,7 @@ resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.6.tgz#164e169dd061795b50b83c19e4d3be09f8d3a454" integrity sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g== -"@types/stack-utils@^2.0.0": +"@types/stack-utils@^2.0.0", "@types/stack-utils@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== @@ -7327,7 +7019,7 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.8": +"@types/yargs@^17.0.33", "@types/yargs@^17.0.8": version "17.0.33" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== @@ -7356,19 +7048,19 @@ tsutils "^3.21.0" "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz#9beb9e4fbfdde40410e96587cc56dded1942cdf1" - integrity sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q== + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz#4d730c2becd8e47ef76e59f68aee0fb560927cfc" + integrity sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.30.1" - "@typescript-eslint/type-utils" "8.30.1" - "@typescript-eslint/utils" "8.30.1" - "@typescript-eslint/visitor-keys" "8.30.1" + "@typescript-eslint/scope-manager" "8.43.0" + "@typescript-eslint/type-utils" "8.43.0" + "@typescript-eslint/utils" "8.43.0" + "@typescript-eslint/visitor-keys" "8.43.0" graphemer "^1.4.0" - ignore "^5.3.1" + ignore "^7.0.0" natural-compare "^1.4.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" "@typescript-eslint/experimental-utils@^5.3.0": version "5.62.0" @@ -7387,26 +7079,24 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.43.0.tgz#4024159925e7671f1782bdd3498bdcfbd48f9137" + integrity sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "8.43.0" + "@typescript-eslint/types" "8.43.0" + "@typescript-eslint/typescript-estree" "8.43.0" + "@typescript-eslint/visitor-keys" "8.43.0" debug "^4.3.4" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.30.1.tgz#8a9fa650b046e64656e21d4fdff86535b6a084b6" - integrity sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg== +"@typescript-eslint/project-service@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.43.0.tgz#958dbaa16fbd1e81d46ab86e139f6276757140f8" + integrity sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw== dependencies: - "@typescript-eslint/scope-manager" "8.30.1" - "@typescript-eslint/types" "8.30.1" - "@typescript-eslint/typescript-estree" "8.30.1" - "@typescript-eslint/visitor-keys" "8.30.1" + "@typescript-eslint/tsconfig-utils" "^8.43.0" + "@typescript-eslint/types" "^8.43.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -7417,21 +7107,18 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/scope-manager@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz#009ebc09cc6e7e0dd67898a0e9a70d295361c6b9" + integrity sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.43.0" + "@typescript-eslint/visitor-keys" "8.43.0" -"@typescript-eslint/scope-manager@8.30.1": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz#f99c7efd53b5ff9fb57e55be71eb855603fd80b7" - integrity sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg== - dependencies: - "@typescript-eslint/types" "8.30.1" - "@typescript-eslint/visitor-keys" "8.30.1" +"@typescript-eslint/tsconfig-utils@8.43.0", "@typescript-eslint/tsconfig-utils@^8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz#e6721dba183d61769a90ffdad202aebc383b18c8" + integrity sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA== "@typescript-eslint/type-utils@5.62.0": version "5.62.0" @@ -7443,30 +7130,26 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/type-utils@8.30.1": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz#151ee0529d6e6df19d8a3a23e81c809d2e4f6b1a" - integrity sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA== +"@typescript-eslint/type-utils@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz#29ea2e34eeae5b8e9fe4f4730c5659fa330aa04e" + integrity sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg== dependencies: - "@typescript-eslint/typescript-estree" "8.30.1" - "@typescript-eslint/utils" "8.30.1" + "@typescript-eslint/types" "8.43.0" + "@typescript-eslint/typescript-estree" "8.43.0" + "@typescript-eslint/utils" "8.43.0" debug "^4.3.4" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" "@typescript-eslint/types@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - -"@typescript-eslint/types@8.30.1": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.30.1.tgz#20ff6d66ab3d8fe0533aeb7092a487393d53f925" - integrity sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw== +"@typescript-eslint/types@8.43.0", "@typescript-eslint/types@^8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.43.0.tgz#00d34a5099504eb1b263e022cc17c4243ff2302e" + integrity sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw== "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" @@ -7481,33 +7164,21 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/typescript-estree@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz#39e5d431239b4d90787072ae0c2290cbd3e0a562" + integrity sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/typescript-estree@8.30.1": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz#f5c133e4a76a54d25607434f2c276409d7bec4ba" - integrity sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ== - dependencies: - "@typescript-eslint/types" "8.30.1" - "@typescript-eslint/visitor-keys" "8.30.1" + "@typescript-eslint/project-service" "8.43.0" + "@typescript-eslint/tsconfig-utils" "8.43.0" + "@typescript-eslint/types" "8.43.0" + "@typescript-eslint/visitor-keys" "8.43.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" "@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0": version "5.62.0" @@ -7523,15 +7194,15 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@8.30.1": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.30.1.tgz#23d4824394765948fe73dc7113892f85fdc80efd" - integrity sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ== +"@typescript-eslint/utils@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.43.0.tgz#5c391133a52f8500dfdabd7026be72a537d7b59e" + integrity sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.30.1" - "@typescript-eslint/types" "8.30.1" - "@typescript-eslint/typescript-estree" "8.30.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.43.0" + "@typescript-eslint/types" "8.43.0" + "@typescript-eslint/typescript-estree" "8.43.0" "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" @@ -7541,21 +7212,13 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/visitor-keys@8.43.0": + version "8.43.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz#633d3414afec3cf0a0e4583e1575f4101ef51d30" + integrity sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw== dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - -"@typescript-eslint/visitor-keys@8.30.1": - version "8.30.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz#510955ef1fb56e08da4b7953a3377258e5942e36" - integrity sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA== - dependencies: - "@typescript-eslint/types" "8.30.1" - eslint-visitor-keys "^4.2.0" + "@typescript-eslint/types" "8.43.0" + eslint-visitor-keys "^4.2.1" "@uidotdev/usehooks@^2.4.1": version "2.4.1" @@ -7567,100 +7230,115 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@unrs/resolver-binding-darwin-arm64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz#0c64ebe422a3d05ada91d8ba84e037383742c955" - integrity sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ== +"@unrs/resolver-binding-android-arm-eabi@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" + integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw== -"@unrs/resolver-binding-darwin-x64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz#57210874eca22ec3a07039c97c028fb19c0c6d57" - integrity sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw== +"@unrs/resolver-binding-android-arm64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f" + integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g== -"@unrs/resolver-binding-freebsd-x64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz#4519371d0ad8e557a86623d8497e3abcdcb5ae43" - integrity sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw== +"@unrs/resolver-binding-darwin-arm64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf" + integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== -"@unrs/resolver-binding-linux-arm-gnueabihf@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz#4fc05aec9e65a6478003a0b9034a06ac0da886ab" - integrity sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA== +"@unrs/resolver-binding-darwin-x64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc" + integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ== -"@unrs/resolver-binding-linux-arm-musleabihf@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz#c24b35dd5818fcd25569425b1dc1a98a883e248b" - integrity sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg== +"@unrs/resolver-binding-freebsd-x64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b" + integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw== -"@unrs/resolver-binding-linux-arm64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz#07dc8478a0a356d343790208dc557d6d053689af" - integrity sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g== +"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a" + integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw== -"@unrs/resolver-binding-linux-arm64-musl@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz#169e531731f7e462dffa410034a1d06a7a921aa8" - integrity sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw== +"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3" + integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw== -"@unrs/resolver-binding-linux-ppc64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz#f6ad2ff47d74c8158b28a18536a71a8ecf84a17f" - integrity sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA== +"@unrs/resolver-binding-linux-arm64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d" + integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== -"@unrs/resolver-binding-linux-riscv64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz#2f3986cb44f285f90d27e87cee8b4059de3ffbdd" - integrity sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA== +"@unrs/resolver-binding-linux-arm64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0" + integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== -"@unrs/resolver-binding-linux-s390x-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz#813ea07833012bc34ecc59f023e422b421138761" - integrity sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw== +"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44" + integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== -"@unrs/resolver-binding-linux-x64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz#18b0d7553268fa490db92be578ac4b0fd8cae049" - integrity sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA== +"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9" + integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== -"@unrs/resolver-binding-linux-x64-musl@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz#04541e98d16e358c695393251e365bc3d802dfa4" - integrity sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ== +"@unrs/resolver-binding-linux-riscv64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165" + integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== -"@unrs/resolver-binding-wasm32-wasi@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz#7a2ae7467c4c52d53c20ad7fc2bace1b23de8168" - integrity sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg== +"@unrs/resolver-binding-linux-s390x-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94" + integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== + +"@unrs/resolver-binding-linux-x64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935" + integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== + +"@unrs/resolver-binding-linux-x64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6" + integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== + +"@unrs/resolver-binding-wasm32-wasi@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d" + integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ== dependencies: - "@napi-rs/wasm-runtime" "^0.2.8" + "@napi-rs/wasm-runtime" "^0.2.11" -"@unrs/resolver-binding-win32-arm64-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz#11deb282b8ce73fab26f1d04df0fa4d6363752c2" - integrity sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg== +"@unrs/resolver-binding-win32-arm64-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35" + integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw== -"@unrs/resolver-binding-win32-ia32-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz#2a5d414912379425bd395ea15901a5dd5febc7c1" - integrity sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw== +"@unrs/resolver-binding-win32-ia32-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6" + integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ== -"@unrs/resolver-binding-win32-x64-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz#5768c6bba4a27833a48a8a77e50eb01b520d0962" - integrity sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw== +"@unrs/resolver-binding-win32-x64-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" + integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== "@vanilla-extract/css-utils@^0.1.4": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@vanilla-extract/css-utils/-/css-utils-0.1.4.tgz#ea9c919109ce642f6ebdb0d11145187003d49758" - integrity sha512-3WRxMGa/VQaL32jZqRUpnnoVFSws5iPIUpQr+XlT4jXhtMeKYcA20rFK2k2Amkg04sqrO84A8hNMeABWZQesEg== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@vanilla-extract/css-utils/-/css-utils-0.1.6.tgz#f83df9b8d30e65dae2c301c711546212e5246a53" + integrity sha512-iICpaHma0s2EEnQDw/JRqudQJwYw1JERyWfIllNQplps226KVphjGb3jyGMiBK5Waw69RD3q4gulgRVQAQmKmA== "@vanilla-extract/css@^1.15.5": - version "1.17.1" - resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.17.1.tgz#5e91d6bfbc5848a264bddcaf31d601f6f67e5cc1" - integrity sha512-tOHQXHm10FrJeXKFeWE09JfDGN/tvV6mbjwoNB9k03u930Vg021vTnbrCwVLkECj9Zvh/SHLBHJ4r2flGqfovw== + version "1.17.4" + resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.17.4.tgz#c73353992b8243e8ab140582bf6d673ebc709b0a" + integrity sha512-m3g9nQDWPtL+sTFdtCGRMI1Vrp86Ay4PBYq1Bo7Bnchj5ElNtAJpOqD+zg+apthVA4fB7oVpMWNjwpa6ElDWFQ== dependencies: "@emotion/hash" "^0.9.0" - "@vanilla-extract/private" "^1.0.6" + "@vanilla-extract/private" "^1.0.9" css-what "^6.1.0" cssesc "^3.0.0" csstype "^3.0.7" @@ -7673,21 +7351,21 @@ picocolors "^1.0.0" "@vanilla-extract/dynamic@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@vanilla-extract/dynamic/-/dynamic-2.1.2.tgz#b1d1c1e0e392934c5a3bbb53f99069a7721311ac" - integrity sha512-9BGMciD8rO1hdSPIAh1ntsG4LPD3IYKhywR7VOmmz9OO4Lx1hlwkSg3E6X07ujFx7YuBfx0GDQnApG9ESHvB2A== + version "2.1.5" + resolved "https://registry.yarnpkg.com/@vanilla-extract/dynamic/-/dynamic-2.1.5.tgz#2e2721d5e17071c161e3fdf29b8204772e3bbabc" + integrity sha512-QGIFGb1qyXQkbzx6X6i3+3LMc/iv/ZMBttMBL+Wm/DetQd36KsKsFg5CtH3qy+1hCA/5w93mEIIAiL4fkM8ycw== dependencies: - "@vanilla-extract/private" "^1.0.6" + "@vanilla-extract/private" "^1.0.9" -"@vanilla-extract/private@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@vanilla-extract/private/-/private-1.0.6.tgz#f10bbf3189f7b827d0bd7f804a6219dd03ddbdd4" - integrity sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw== +"@vanilla-extract/private@^1.0.6", "@vanilla-extract/private@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@vanilla-extract/private/-/private-1.0.9.tgz#bb8aaf72d2e04439792f2e389d9b705cfe691bc0" + integrity sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA== "@vanilla-extract/recipes@^0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@vanilla-extract/recipes/-/recipes-0.5.5.tgz#da34e247be2c3d70e01ecfeb53310daadc608b74" - integrity sha512-VadU7+IFUwLNLMgks29AHav/K5h7DOEfTU91RItn5vwdPfzduodNg317YbgWCcpm7FSXkuR3B3X8ZOi95UOozA== + version "0.5.7" + resolved "https://registry.yarnpkg.com/@vanilla-extract/recipes/-/recipes-0.5.7.tgz#6677a267ca278e2277ac8ab41b64e57e84c1150c" + integrity sha512-Fvr+htdyb6LVUu+PhH61UFPhwkjgDEk8L4Zq9oIdte42sntpKrgFy90MyTRtGwjVALmrJ0pwRUVr8UoByYeW8A== "@vitest/pretty-format@2.1.9": version "2.1.9" @@ -8120,6 +7798,11 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" +acorn-import-phases@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== + acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -8130,13 +7813,6 @@ acorn-walk@^7.1.1, acorn-walk@^7.2.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.1.1: - version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" @@ -8147,10 +7823,10 @@ acorn@^7.1.1, acorn@^7.4.0, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.11.0, acorn@^8.14.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.14.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" - integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== +acorn@^8.15.0, acorn@^8.2.4, acorn@^8.9.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== add-stream@^1.0.0: version "1.0.0" @@ -8170,9 +7846,9 @@ agent-base@6, agent-base@^6.0.2: debug "4" agent-base@^7.1.0, agent-base@^7.1.2: - version "7.1.3" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" - integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== agentkeepalive@^4.2.1: version "4.6.0" @@ -8306,9 +7982,9 @@ ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^3.2.1: version "3.2.1" @@ -8324,15 +8000,15 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: +ansi-styles@^5.0.0, ansi-styles@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== ansi-to-html@^0.6.11: version "0.6.15" @@ -8363,9 +8039,9 @@ app-root-dir@^1.0.2: integrity sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g== "aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.1.0.tgz#75500a190313d95c64e871e7e4284c6ac219f0b1" + integrity sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew== aproba@^1.1.1: version "1.2.0" @@ -8388,11 +8064,6 @@ are-we-there-yet@^3.0.0: delegates "^1.0.0" readable-stream "^3.6.0" -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -8460,17 +8131,19 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== -array-includes@^3.0.3, array-includes@^3.1.6, array-includes@^3.1.8: - version "3.1.8" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" - integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== +array-includes@^3.0.3, array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - is-string "^1.0.7" + es-abstract "^1.24.0" + es-object-atoms "^1.1.1" + get-intrinsic "^1.3.0" + is-string "^1.1.1" + math-intrinsics "^1.1.0" array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" @@ -8506,7 +8179,7 @@ array.prototype.findlast@^1.2.5: es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" -array.prototype.findlastindex@^1.2.5: +array.prototype.findlastindex@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== @@ -8519,7 +8192,7 @@ array.prototype.findlastindex@^1.2.5: es-object-atoms "^1.1.1" es-shim-unscopables "^1.1.0" -array.prototype.flat@^1.2.1, array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: +array.prototype.flat@^1.2.1, array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== @@ -8649,7 +8322,7 @@ async-function@^1.0.0: resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== -async@^3.2.3: +async@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== @@ -8704,7 +8377,7 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== -axios@^0.21.1, axios@^0.21.2: +axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -8712,12 +8385,12 @@ axios@^0.21.1, axios@^0.21.2: follow-redirects "^1.14.0" axios@^1.0.0, axios@^1.3.3, axios@^1.6.0: - version "1.8.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" - integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6" + integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA== dependencies: follow-redirects "^1.15.6" - form-data "^4.0.0" + form-data "^4.0.4" proxy-from-env "^1.1.0" axobject-query@^4.1.0: @@ -8804,13 +8477,13 @@ babel-plugin-named-exports-order@^0.0.2: resolved "https://registry.yarnpkg.com/babel-plugin-named-exports-order/-/babel-plugin-named-exports-order-0.0.2.tgz#ae14909521cf9606094a2048239d69847540cb09" integrity sha512-OgOYHOLoRK+/mvXU9imKHlG6GkPLYrUCvFXG/CM93R/aNNO8pOOF4aS+S8CCHMDQoNSeiOYEZb/G6RwL95Jktw== -babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.13" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz#7d445f0e0607ebc8fb6b01d7e8fb02069b91dd8b" - integrity sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g== +babel-plugin-polyfill-corejs2@^0.4.14: + version "0.4.14" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz#8101b82b769c568835611542488d463395c2ef8f" + integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg== dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.4" + "@babel/compat-data" "^7.27.7" + "@babel/helper-define-polyfill-provider" "^0.6.5" semver "^6.3.1" babel-plugin-polyfill-corejs3@^0.1.0: @@ -8821,20 +8494,20 @@ babel-plugin-polyfill-corejs3@^0.1.0: "@babel/helper-define-polyfill-provider" "^0.1.5" core-js-compat "^3.8.1" -babel-plugin-polyfill-corejs3@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" - integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.3" - core-js-compat "^3.40.0" + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" -babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz#428c615d3c177292a22b4f93ed99e358d7906a9b" - integrity sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw== +babel-plugin-polyfill-regenerator@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz#32752e38ab6f6767b92650347bf26a31b16ae8c5" + integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.4" + "@babel/helper-define-polyfill-provider" "^0.6.5" babel-plugin-react-docgen@^4.2.1: version "4.2.1" @@ -8860,9 +8533,9 @@ babel-plugin-root-import@^6.6.0: slash "^3.0.0" babel-preset-current-node-syntax@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" - integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -8976,9 +8649,9 @@ bignumber.js@9.1.2: integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== bignumber.js@^9.1.2: - version "9.2.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.2.1.tgz#3ad0854ad933560a25bbc7c93bc3b7ea6edcad85" - integrity sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw== + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== binary-extensions@^1.0.0: version "1.13.1" @@ -9032,14 +8705,14 @@ bluebird@^3.5.5: integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: - version "4.12.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.1.tgz#215741fe3c9dba2d7e12c001d0cfdbae43975ba7" - integrity sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg== + version "4.12.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99" + integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== bn.js@^5.2.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + version "5.2.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" + integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== body-parser@1.20.3: version "1.20.3" @@ -9099,17 +8772,17 @@ bplist-parser@^0.1.0: big-integer "^1.6.7" brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" @@ -9219,15 +8892,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.24.4: - version "4.24.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" - integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.25.3: + version "4.25.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.4.tgz#ebdd0e1d1cf3911834bab3a6cd7b917d9babf5af" + integrity sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg== dependencies: - caniuse-lite "^1.0.30001688" - electron-to-chromium "^1.5.73" + caniuse-lite "^1.0.30001737" + electron-to-chromium "^1.5.211" node-releases "^2.0.19" - update-browserslist-db "^1.1.1" + update-browserslist-db "^1.1.3" bs-logger@0.x: version "0.2.6" @@ -9539,10 +9212,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688: - version "1.0.30001713" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz#6b33a8857e6c7dcb41a0caa2dd0f0489c823a52d" - integrity sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001737: + version "1.0.30001741" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz#67fb92953edc536442f3c9da74320774aa523143" + integrity sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw== capture-exit@^2.0.0: version "2.0.0" @@ -9567,11 +9240,11 @@ ccount@^2.0.0: integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== chain-registry@^1.69.64: - version "1.69.185" - resolved "https://registry.yarnpkg.com/chain-registry/-/chain-registry-1.69.185.tgz#980c3db42c49a7499772d8effb181de928a8c181" - integrity sha512-SZWx00a4ekaf4+J/pM4WAvImrUzzckmC2yt+QkcXEjdmaokT/lKyr1YJ6KcjgBIeAWKq6hIbTinJO1VnaviY5w== + version "1.69.334" + resolved "https://registry.yarnpkg.com/chain-registry/-/chain-registry-1.69.334.tgz#197743eacc731e8e15118305670f37b35b792557" + integrity sha512-Vgx3aHg/QSH0/1hxwcEH28DrxETYFXgWEAvgmWBbGSmRBc2PpWCPVi5q4rCFEZXs7sc83we5an8IYLrVhXvbmg== dependencies: - "@chain-registry/types" "^0.50.116" + "@chain-registry/types" "^0.50.207" chalk@4.1.0: version "4.1.0" @@ -9598,7 +9271,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -9646,10 +9319,10 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chardet@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.0.tgz#1007f441a1ae9f9199a4a67f6e978fb0aa9aa3fe" + integrity sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA== chokidar@^2.1.8: version "2.1.8" @@ -9670,7 +9343,7 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: +chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -9717,6 +9390,11 @@ ci-info@^3.2.0, ci-info@^3.6.1: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +ci-info@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.0.tgz#c39b1013f8fdbd28cd78e62318357d02da160cd7" + integrity sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.6.tgz#8fe672437d01cd6c4561af5334e0cc50ff1955f7" @@ -9740,11 +9418,6 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.3.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - cldr-compact-number@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cldr-compact-number/-/cldr-compact-number-0.4.0.tgz#d74bb8cedab92ca63032423a1bd26aaaf76b9b2c" @@ -9987,7 +9660,7 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== -commander@2, commander@^2.19.0, commander@^2.20.0: +commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -10043,15 +9716,15 @@ compressible@~2.0.18: mime-db ">= 1.43.0 < 2" compression@^1.7.4: - version "1.8.0" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.0.tgz#09420efc96e11a0f44f3a558de59e321364180f7" - integrity sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA== + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== dependencies: bytes "3.1.2" compressible "~2.0.18" debug "2.6.9" negotiator "~0.6.4" - on-headers "~1.0.2" + on-headers "~1.1.0" safe-buffer "5.2.1" vary "~1.1.2" @@ -10239,22 +9912,22 @@ copy-to-clipboard@^3.3.3: dependencies: toggle-selection "^1.0.6" -core-js-compat@^3.40.0, core-js-compat@^3.8.1: - version "3.41.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.41.0.tgz#4cdfce95f39a8f27759b667cf693d96e5dda3d17" - integrity sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A== +core-js-compat@^3.43.0, core-js-compat@^3.8.1: + version "3.45.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.45.1.tgz#424f3f4af30bf676fd1b67a579465104f64e9c7a" + integrity sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA== dependencies: - browserslist "^4.24.4" + browserslist "^4.25.3" core-js-pure@^3.23.3: - version "3.41.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.41.0.tgz#349fecad168d60807a31e83c99d73d786fe80811" - integrity sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q== + version "3.45.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.45.1.tgz#b129d86a5f7f8380378577c7eaee83608570a05a" + integrity sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ== core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2: - version "3.41.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.41.0.tgz#57714dafb8c751a6095d028a7428f1fb5834a776" - integrity sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA== + version "3.45.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.45.1.tgz#5810e04a1b4e9bc5ddaa4dd12e702ff67300634d" + integrity sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg== core-util-is@~1.0.0: version "1.0.3" @@ -10331,7 +10004,7 @@ create-ecdh@^4.0.4: bn.js "^4.1.0" elliptic "^6.5.3" -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: +create-hash@^1.1.0, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -10342,7 +10015,17 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hash@~1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + integrity sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -10354,11 +10037,6 @@ create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-fetch@^3.1.5: version "3.2.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" @@ -10386,10 +10064,10 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -crossws@^0.3.3: - version "0.3.4" - resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.4.tgz#06164c6495ea99152ea7557c99310b52d9be9b29" - integrity sha512-uj0O1ETYX1Bh6uSgktfPvwDiPYGQ3aI4qVsaC/LWpkIzGj1nUYm5FK3K+t11oOlpN01lGbprFCH4wBlKdJjVgw== +crossws@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.3.5.tgz#daad331d44148ea6500098bc858869f3a5ab81a6" + integrity sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA== dependencies: uncrypto "^0.1.3" @@ -10494,9 +10172,9 @@ css-select@^4.1.3: nth-check "^2.0.1" css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + version "5.2.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== dependencies: boolbase "^1.0.0" css-what "^6.1.0" @@ -10537,9 +10215,9 @@ css-vendor@^2.0.8: is-in-browser "^1.0.2" css-what@^6.0.1, css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + version "6.2.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== css.escape@^1.5.1: version "1.5.1" @@ -10632,11 +10310,11 @@ cssstyle@^2.3.0: cssom "~0.3.6" cssstyle@^4.2.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.3.0.tgz#83db22d1aec8eb7e5ecd812b4d14a17fb3dd243d" - integrity sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ== + version "4.6.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.6.0.tgz#ea18007024e3167f4f105315f3ec2d982bf48ed9" + integrity sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg== dependencies: - "@asamuzakjp/css-color" "^3.1.1" + "@asamuzakjp/css-color" "^3.2.0" rrweb-cssom "^0.8.0" csstype@^3.0.2, csstype@^3.0.7, csstype@^3.1.3: @@ -10656,7 +10334,7 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.2.tgz#673b5f233bf34d8e602b949429f8171d9121bea3" integrity sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA== -d3-array@2, d3-array@^2.5.0: +d3-array@2: version "2.12.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== @@ -10670,11 +10348,6 @@ d3-array@2, d3-array@^2.5.0: dependencies: internmap "1 - 2" -"d3-color@1 - 2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" - integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== - "d3-color@1 - 3", d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" @@ -10687,24 +10360,6 @@ d3-delaunay@^6.0.4: dependencies: delaunator "5" -"d3-dispatch@1 - 2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" - integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== - -d3-drag@2: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-2.0.0.tgz#9eaf046ce9ed1c25c88661911c1d5a4d8eb7ea6d" - integrity sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w== - dependencies: - d3-dispatch "1 - 2" - d3-selection "2" - -"d3-ease@1 - 2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-2.0.0.tgz#fd1762bfca00dae4bacea504b1d628ff290ac563" - integrity sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ== - d3-ease@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" @@ -10720,20 +10375,6 @@ d3-format@^1.4.4: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== -d3-geo@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-2.0.2.tgz#c065c1b71fe8c5f1be657e5f43d9bdd010383c40" - integrity sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA== - dependencies: - d3-array "^2.5.0" - -"d3-interpolate@1 - 2": - version "2.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" - integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== - dependencies: - d3-color "1 - 2" - "d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" @@ -10765,11 +10406,6 @@ d3-scale@^4.0.2: d3-time "2.1.1 - 3" d3-time-format "2 - 4" -d3-selection@2, d3-selection@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" - integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== - d3-shape@^3.1.0, d3-shape@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" @@ -10798,7 +10434,7 @@ d3-time-format@^3.0.0: dependencies: d3-array "2" -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0, d3-time@^3.1.0: +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== @@ -10810,38 +10446,11 @@ d3-time@^1.0.11: resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== -"d3-timer@1 - 2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" - integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== - d3-timer@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== -d3-transition@2: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-2.0.0.tgz#366ef70c22ef88d1e34105f507516991a291c94c" - integrity sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog== - dependencies: - d3-color "1 - 2" - d3-dispatch "1 - 2" - d3-ease "1 - 2" - d3-interpolate "1 - 2" - d3-timer "1 - 2" - -d3-zoom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-2.0.0.tgz#f04d0afd05518becce879d04709c47ecd93fba54" - integrity sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw== - dependencies: - d3-dispatch "1 - 2" - d3-drag "2" - d3-interpolate "1 - 2" - d3-selection "2" - d3-transition "2" - damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -10920,10 +10529,10 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" @@ -10958,14 +10567,14 @@ decimal.js-light@^2.4.1: integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== decimal.js@^10.2.1, decimal.js@^10.4.3, decimal.js@^10.5.0: - version "10.5.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.5.0.tgz#0f371c7cf6c4898ce0afb09836db73cd82010f22" - integrity sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw== + version "10.6.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== decode-named-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz#5d6ce68792808901210dac42a8e9853511e2b8bf" - integrity sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w== + version "1.2.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed" + integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q== dependencies: character-entities "^2.0.0" @@ -10980,9 +10589,9 @@ dedent@0.7.0, dedent@^0.7.0: integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== dedent@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" - integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + version "1.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.0.tgz#c1f9445335f0175a96587be245a282ff451446ca" + integrity sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ== deep-equal@^2.0.5: version "2.2.3" @@ -11109,7 +10718,7 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" -delaunator@5, delaunator@^5.0.1: +delaunator@5: version "5.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== @@ -11159,7 +10768,7 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -destr@^2.0.3: +destr@^2.0.3, destr@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== @@ -11182,9 +10791,9 @@ detect-indent@^5.0.0: integrity sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g== detect-libc@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" - integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" + integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== detect-newline@^3.0.0: version "3.1.0" @@ -11238,11 +10847,6 @@ diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - diff@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" @@ -11366,10 +10970,10 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -dompurify@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.5.tgz#11b108656a5fb72b24d916df17a1421663d7129c" - integrity sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ== +dompurify@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad" + integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -11431,9 +11035,9 @@ dotenv-webpack@^7.0.3: dotenv-defaults "^2.0.2" dotenv@^16.0.3: - version "16.5.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" - integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== dotenv@^8.0.0, dotenv@^8.2.0: version "8.6.0" @@ -11496,10 +11100,10 @@ ejs@^3.1.7: dependencies: jake "^10.8.5" -electron-to-chromium@^1.5.73: - version "1.5.137" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz#53a7fef3ea9f7eb5fcf704454050ff930c43ed92" - integrity sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA== +electron-to-chromium@^1.5.211: + version "1.5.215" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz#200c8d69b1270af6126837b6b1f95077c3a347b1" + integrity sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ== elliptic@^6.4.0, elliptic@^6.5.3, elliptic@^6.5.4, elliptic@^6.5.5: version "6.6.1" @@ -11552,9 +11156,9 @@ encoding@^0.1.13: iconv-lite "^0.6.2" end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== dependencies: once "^1.4.0" @@ -11576,10 +11180,10 @@ enhanced-resolve@^4.5.0: memory-fs "^0.5.0" tapable "^1.0.0" -enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0: - version "5.18.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" - integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.3, enhanced-resolve@^5.7.0: + version "5.18.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz#9b5f4c5c076b8787c78fe540392ce76a88855b44" + integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -11604,11 +11208,16 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -11650,27 +11259,27 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: - version "1.23.9" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606" - integrity sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA== +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328" + integrity sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg== dependencies: array-buffer-byte-length "^1.0.2" arraybuffer.prototype.slice "^1.0.4" available-typed-arrays "^1.0.7" call-bind "^1.0.8" - call-bound "^1.0.3" + call-bound "^1.0.4" data-view-buffer "^1.0.2" data-view-byte-length "^1.0.2" data-view-byte-offset "^1.0.1" es-define-property "^1.0.1" es-errors "^1.3.0" - es-object-atoms "^1.0.0" + es-object-atoms "^1.1.1" es-set-tostringtag "^2.1.0" es-to-primitive "^1.3.0" function.prototype.name "^1.1.8" - get-intrinsic "^1.2.7" - get-proto "^1.0.0" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" get-symbol-description "^1.1.0" globalthis "^1.0.4" gopd "^1.2.0" @@ -11682,21 +11291,24 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 is-array-buffer "^3.0.5" is-callable "^1.2.7" is-data-view "^1.0.2" + is-negative-zero "^2.0.3" is-regex "^1.2.1" + is-set "^2.0.3" is-shared-array-buffer "^1.0.4" is-string "^1.1.1" is-typed-array "^1.1.15" - is-weakref "^1.1.0" + is-weakref "^1.1.1" math-intrinsics "^1.1.0" - object-inspect "^1.13.3" + object-inspect "^1.13.4" object-keys "^1.1.1" object.assign "^4.1.7" own-keys "^1.0.1" - regexp.prototype.flags "^1.5.3" + regexp.prototype.flags "^1.5.4" safe-array-concat "^1.1.3" safe-push-apply "^1.0.0" safe-regex-test "^1.1.0" set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" string.prototype.trim "^1.2.10" string.prototype.trimend "^1.0.9" string.prototype.trimstart "^1.0.8" @@ -11705,7 +11317,7 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 typed-array-byte-offset "^1.0.4" typed-array-length "^1.0.7" unbox-primitive "^1.1.0" - which-typed-array "^1.1.18" + which-typed-array "^1.1.19" es-array-method-boxes-properly@^1.0.0: version "1.0.0" @@ -11760,9 +11372,9 @@ es-iterator-helpers@^1.2.1: safe-array-concat "^1.1.3" es-module-lexer@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" - integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" @@ -11869,21 +11481,6 @@ eslint-config-airbnb@^19.0.2, eslint-config-airbnb@^19.0.4: object.assign "^4.1.2" object.entries "^1.1.5" -eslint-config-next@14.1.4: - version "14.1.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.4.tgz#22f2ba4c0993e991249d863656a64c204bae542c" - integrity sha512-cihIahbhYAWwXJwZkAaRPpUi5t9aOi/HdfWXOjZeUOqNWXHD8X22kd1KG58Dc3MVaRx3HoR/oMGk2ltcrqDn8g== - dependencies: - "@next/eslint-plugin-next" "14.1.4" - "@rushstack/eslint-patch" "^1.3.3" - "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.28.1" - eslint-plugin-jsx-a11y "^6.7.1" - eslint-plugin-react "^7.33.2" - eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" - eslint-config-next@15.0.3: version "15.0.3" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.0.3.tgz#b483585260d5e55050d4ab87e053c88089ae12ee" @@ -11901,9 +11498,9 @@ eslint-config-next@15.0.3: eslint-plugin-react-hooks "^5.0.0" eslint-config-prettier@^8.3.0, eslint-config-prettier@^8.5.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" - integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== + version "8.10.2" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz#0642e53625ebc62c31c24726b0f050df6bd97a2e" + integrity sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A== eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -11923,48 +11520,48 @@ eslint-import-resolver-root-import@^1.0.4: json5 "^2.1.0" eslint-import-resolver-typescript@^3.5.2: - version "3.10.0" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.0.tgz#5bca4c579e17174e95bf67526b424d07b46c352e" - integrity sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ== + version "3.10.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz#23dac32efa86a88e2b8232eb244ac499ad636db2" + integrity sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ== dependencies: "@nolyfill/is-core-module" "1.0.39" debug "^4.4.0" get-tsconfig "^4.10.0" is-bun-module "^2.0.0" stable-hash "^0.0.5" - tinyglobby "^0.2.12" - unrs-resolver "^1.3.2" + tinyglobby "^0.2.13" + unrs-resolver "^1.6.2" -eslint-module-utils@^2.12.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" - integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== +eslint-module-utils@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff" + integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw== dependencies: debug "^3.2.7" -eslint-plugin-import@^2.25.4, eslint-plugin-import@^2.28.1, eslint-plugin-import@^2.31.0: - version "2.31.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" - integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== +eslint-plugin-import@^2.25.4, eslint-plugin-import@^2.31.0: + version "2.32.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" + integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== dependencies: "@rtsao/scc" "^1.1.0" - array-includes "^3.1.8" - array.prototype.findlastindex "^1.2.5" - array.prototype.flat "^1.3.2" - array.prototype.flatmap "^1.3.2" + array-includes "^3.1.9" + array.prototype.findlastindex "^1.2.6" + array.prototype.flat "^1.3.3" + array.prototype.flatmap "^1.3.3" debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.12.0" + eslint-module-utils "^2.12.1" hasown "^2.0.2" - is-core-module "^2.15.1" + is-core-module "^2.16.1" is-glob "^4.0.3" minimatch "^3.1.2" object.fromentries "^2.0.8" object.groupby "^1.0.3" - object.values "^1.2.0" + object.values "^1.2.1" semver "^6.3.1" - string.prototype.trimend "^1.0.8" + string.prototype.trimend "^1.0.9" tsconfig-paths "^3.15.0" eslint-plugin-jest@^26.1.1: @@ -11974,7 +11571,7 @@ eslint-plugin-jest@^26.1.1: dependencies: "@typescript-eslint/utils" "^5.10.0" -eslint-plugin-jsx-a11y@^6.10.0, eslint-plugin-jsx-a11y@^6.5.1, eslint-plugin-jsx-a11y@^6.7.1: +eslint-plugin-jsx-a11y@^6.10.0, eslint-plugin-jsx-a11y@^6.5.1: version "6.10.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== @@ -12005,9 +11602,9 @@ eslint-plugin-mocha@^10.0.3: rambda "^7.4.0" eslint-plugin-prettier@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" - integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + version "4.2.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.5.tgz#91ca3f2f01a84f1272cce04e9717550494c0fe06" + integrity sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg== dependencies: prettier-linter-helpers "^1.0.0" @@ -12016,17 +11613,12 @@ eslint-plugin-react-hooks@^4.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": - version "5.0.0-canary-7118f5dd7-20230705" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz#4d55c50e186f1a2b0636433d2b0b2f592ddbccfd" - integrity sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw== - eslint-plugin-react-hooks@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== -eslint-plugin-react@^7.29.2, eslint-plugin-react@^7.33.2, eslint-plugin-react@^7.35.0: +eslint-plugin-react@^7.29.2, eslint-plugin-react@^7.35.0: version "7.37.5" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== @@ -12112,10 +11704,10 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint-visitor-keys@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" - integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== eslint@^7.18.0: version "7.32.0" @@ -12393,23 +11985,24 @@ expect@^28.1.3: jest-message-util "^28.1.3" jest-util "^28.1.3" -expect@^29.0.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" - integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== +expect@^30.0.0: + version "30.1.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.1.2.tgz#094909c2443f76b9e208fafac4a315aaaf924580" + integrity sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg== dependencies: - "@jest/expect-utils" "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" + "@jest/expect-utils" "30.1.2" + "@jest/get-type" "30.1.0" + jest-matcher-utils "30.1.2" + jest-message-util "30.1.0" + jest-mock "30.0.5" + jest-util "30.0.5" exponential-backoff@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.2.tgz#a8f26adb96bf78e8cd8ad1037928d5e5c0679d91" integrity sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA== -express@^4.17.1, express@^4.17.3, express@^4.18.2: +express@^4.17.1, express@^4.17.3: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== @@ -12466,15 +12059,6 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" @@ -12559,9 +12143,9 @@ fast-redact@^3.0.0: integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastest-levenshtein@^1.0.12: version "1.0.16" @@ -12610,10 +12194,10 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fdir@^6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" - integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== fetch-retry@^5.0.2: version "5.0.6" @@ -12793,9 +12377,9 @@ focus-lock@^0.8.0: tslib "^1.9.3" follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" @@ -12817,7 +12401,7 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -foreground-child@^3.1.0: +foreground-child@^3.1.0, foreground-child@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -12876,23 +12460,25 @@ fork-ts-checker-webpack-plugin@^7.2.1: tapable "^2.2.1" form-data@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.3.tgz#349c8f2c9d8f8f0c879ee0eb7cc0d300018d6b09" - integrity sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w== + version "3.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.4.tgz#938273171d3f999286a4557528ce022dc2c98df1" + integrity sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" es-set-tostringtag "^2.1.0" + hasown "^2.0.2" mime-types "^2.1.35" -form-data@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" - integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== +form-data@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" es-set-tostringtag "^2.1.0" + hasown "^2.0.2" mime-types "^2.1.12" forwarded@0.2.0: @@ -12907,7 +12493,7 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -fresh@0.5.2: +fresh@0.5.2, fresh@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== @@ -12935,9 +12521,9 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: universalify "^2.0.0" fs-extra@^11.1.0, fs-extra@^11.1.1: - version "11.3.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" - integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== + version "11.3.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.1.tgz#ba7a1f97a85f94c6db2e52ff69570db3671d5a74" + integrity sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -12968,9 +12554,9 @@ fs-minipass@^3.0.0: minipass "^7.0.3" fs-monkey@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" - integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.1.0.tgz#632aa15a20e71828ed56b24303363fb1414e5997" + integrity sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw== fs-write-stream-atomic@^1.0.8: version "1.0.10" @@ -13142,9 +12728,9 @@ get-symbol-description@^1.0.2, get-symbol-description@^1.1.0: get-intrinsic "^1.2.6" get-tsconfig@^4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb" - integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A== + version "4.10.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== dependencies: resolve-pkg-maps "^1.0.0" @@ -13244,16 +12830,17 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== +glob@*: + version "11.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.3.tgz#9d8087e6d72ddb3c4707b1d2778f80ea3eaefcd6" + integrity sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA== dependencies: - foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + foreground-child "^3.3.1" + jackspeak "^4.1.1" + minimatch "^10.0.3" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" glob@7.1.4: version "7.1.4" @@ -13320,11 +12907,6 @@ global@^4.4.0: min-document "^2.19.0" process "^0.11.10" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^13.19.0, globals@^13.24.0, globals@^13.6.0, globals@^13.9.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -13392,19 +12974,19 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -h3@^1.15.0: - version "1.15.1" - resolved "https://registry.yarnpkg.com/h3/-/h3-1.15.1.tgz#59d6f70d7ef619fad74ecdf465a08fff898033bb" - integrity sha512-+ORaOBttdUm1E2Uu/obAyCguiI7MbBvsLTndc3gyK3zU+SYLoZXlyCP9Xgy0gikkGufFLTZXCXD6+4BsufnmHA== +h3@^1.15.4: + version "1.15.4" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.15.4.tgz#022ab3563bbaf2108c25375c40460f3e54a5fe02" + integrity sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ== dependencies: cookie-es "^1.2.2" - crossws "^0.3.3" + crossws "^0.3.5" defu "^6.1.4" - destr "^2.0.3" + destr "^2.0.5" iron-webcrypto "^1.2.1" - node-mock-http "^1.0.0" + node-mock-http "^1.0.2" radix3 "^1.1.2" - ufo "^1.5.4" + ufo "^1.6.1" uncrypto "^0.1.3" handle-thing@^2.0.0: @@ -13513,6 +13095,13 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + integrity sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw== + dependencies: + inherits "^2.0.1" + hash-base@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" @@ -13651,11 +13240,6 @@ hex-rgb@^4.1.0: resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776" integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== -highlight-words@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-1.2.2.tgz#9875b75d11814d7356b24f23feeb7d77761fa867" - integrity sha512-Mf4xfPXYm8Ay1wTibCrHpNWeR2nUMynMVFkXCi4mbl+TEgmNOe+I4hV7W3OCZcSvzGL6kupaqpfHOemliMTGxQ== - highlight-words@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-2.0.0.tgz#06853d68f1f7c8e59d6ef2dd072fe2f64fc93936" @@ -13786,9 +13370,9 @@ html-void-elements@^1.0.0: integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== html-webpack-plugin@>=5.0.0, html-webpack-plugin@^5.0.0, html-webpack-plugin@^5.3.2: - version "5.6.3" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz#a31145f0fee4184d53a794f9513147df1e653685" - integrity sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg== + version "5.6.4" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz#d8cb0f7edff7745ae7d6cccb0bff592e9f7f7959" + integrity sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw== dependencies: "@types/html-minifier-terser" "^6.0.0" html-minifier-terser "^6.0.2" @@ -13822,9 +13406,9 @@ htmlparser2@^6.1.0: entities "^2.0.0" http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" + integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== http-deceiver@^1.2.7: version "1.2.7" @@ -13883,7 +13467,7 @@ http-proxy-agent@^7.0.2: agent-base "^7.1.0" debug "^4.3.4" -http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6: +http-proxy-middleware@^2.0.3: version "2.0.9" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== @@ -13955,14 +13539,14 @@ i18next@^24.2.2: dependencies: "@babel/runtime" "^7.26.10" -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.6.3, iconv-lite@^0.6.2: +iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -13982,9 +13566,9 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== idb-keyval@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" - integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== + version "6.2.2" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a" + integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg== ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" @@ -13996,11 +13580,6 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - ignore-walk@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-5.0.1.tgz#5f199e23e1288f518d90358d461387788a154776" @@ -14020,20 +13599,25 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.4, ignore@^5.2.0, ignore@^5.3.1: +ignore@^5.0.4, ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== immer@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" - integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== + version "10.1.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.3.tgz#e38a0b97db59949d31d9b381b04c2e441b1c3747" + integrity sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw== import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" @@ -14128,15 +13712,15 @@ inline-style-parser@0.2.4: integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q== inquirer@^8.2.4: - version "8.2.6" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" - integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg== + version "8.2.7" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.7.tgz#62f6b931a9b7f8735dc42db927316d8fb6f71de8" + integrity sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA== dependencies: + "@inquirer/external-editor" "^1.0.0" ansi-escapes "^4.2.1" chalk "^4.1.1" cli-cursor "^3.1.0" cli-width "^3.0.0" - external-editor "^3.0.3" figures "^3.0.0" lodash "^4.17.21" mute-stream "0.0.8" @@ -14182,13 +13766,10 @@ intl-messageformat@^10.1.0: "@formatjs/icu-messageformat-parser" "2.11.2" tslib "^2.8.0" -ip-address@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" - integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== - dependencies: - jsbn "1.1.0" - sprintf-js "^1.1.3" +ip-address@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed" + integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA== ip@^2.0.0: version "2.0.1" @@ -14351,7 +13932,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.0, is-core-module@^2.5.0, is-core-module@^2.8.1: +is-core-module@^2.13.0, is-core-module@^2.16.0, is-core-module@^2.16.1, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -14524,6 +14105,11 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + is-number-object@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" @@ -14717,7 +14303,7 @@ is-weakmap@^2.0.2: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-weakref@^1.0.2, is-weakref@^1.1.0: +is-weakref@^1.0.2, is-weakref@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== @@ -14797,12 +14383,12 @@ isobject@^4.0.0: integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== isomorphic-dompurify@^2.21.0: - version "2.23.0" - resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.23.0.tgz#9f0a50410c6ab5014cee64df386426a1d8b0be23" - integrity sha512-f9w5fPJwlu+VK1uowFy4eWYgd7uxl0nQJbtorGp1OAs6JeY1qPkBQKNee1RXrnr68GqZ86PwQ6LF/5rW1TrOZQ== + version "2.26.0" + resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.26.0.tgz#ea6201953d38488445171443393855fa700ea906" + integrity sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw== dependencies: - dompurify "^3.2.5" - jsdom "^26.0.0" + dompurify "^3.2.6" + jsdom "^26.1.0" isomorphic-unfetch@^3.1.0: version "3.1.0" @@ -14852,9 +14438,9 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.1.3, istanbul-reports@^3.1.4: - version "3.1.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -14884,15 +14470,6 @@ iterator.prototype@^1.1.4: has-symbols "^1.1.0" set-function-name "^2.0.2" -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -14902,15 +14479,21 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jake@^10.8.5: - version "10.9.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" - integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== +jackspeak@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.1.tgz#96876030f450502047fc7e8c7fcf8ce8124e43ae" + integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== dependencies: - async "^3.2.3" - chalk "^4.0.2" + "@isaacs/cliui" "^8.0.2" + +jake@^10.8.5: + version "10.9.4" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.4.tgz#d626da108c63d5cfb00ab5c25fadc7e0084af8e6" + integrity sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA== + dependencies: + async "^3.2.6" filelist "^1.0.4" - minimatch "^3.1.2" + picocolors "^1.1.1" jest-changed-files@^27.5.1: version "27.5.1" @@ -14994,7 +14577,17 @@ jest-config@^27.5.1: slash "^3.0.0" strip-json-comments "^3.1.1" -"jest-diff@>=29.4.3 < 30", jest-diff@^29.4.1, jest-diff@^29.7.0: +jest-diff@30.1.2: + version "30.1.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.1.2.tgz#8ff4217e5b63fef49a5b37462999d8f5299a4eb4" + integrity sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ== + dependencies: + "@jest/diff-sequences" "30.0.1" + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + pretty-format "30.0.5" + +"jest-diff@>=29.4.3 < 30", jest-diff@^29.4.1: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== @@ -15154,6 +14747,16 @@ jest-leak-detector@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" +jest-matcher-utils@30.1.2: + version "30.1.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz#3f1b63949f740025aff740c6c6a1b653ae370fbb" + integrity sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ== + dependencies: + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + jest-diff "30.1.2" + pretty-format "30.0.5" + jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" @@ -15174,15 +14777,20 @@ jest-matcher-utils@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-matcher-utils@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" - integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== +jest-message-util@30.1.0: + version "30.1.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.1.0.tgz#653a9bb1a33306eddf13455ce0666ba621b767c4" + integrity sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg== dependencies: - chalk "^4.0.0" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - pretty-format "^29.7.0" + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.0.5" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + micromatch "^4.0.8" + pretty-format "30.0.5" + slash "^3.0.0" + stack-utils "^2.0.6" jest-message-util@^27.5.1: version "27.5.1" @@ -15214,20 +14822,14 @@ jest-message-util@^28.1.3: slash "^3.0.0" stack-utils "^2.0.3" -jest-message-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" - integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== +jest-mock@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.0.5.tgz#ef437e89212560dd395198115550085038570bdd" + integrity sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ== dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.3" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - stack-utils "^2.0.3" + "@jest/types" "30.0.5" + "@types/node" "*" + jest-util "30.0.5" jest-mock@^27.0.6, jest-mock@^27.5.1: version "27.5.1" @@ -15242,6 +14844,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +jest-regex-util@30.0.1: + version "30.0.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== + jest-regex-util@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" @@ -15376,6 +14983,18 @@ jest-snapshot@^27.5.1: pretty-format "^27.5.1" semver "^7.3.2" +jest-util@30.0.5: + version "30.0.5" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.0.5.tgz#035d380c660ad5f1748dff71c4105338e05f8669" + integrity sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g== + dependencies: + "@jest/types" "30.0.5" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.2" + jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" @@ -15412,18 +15031,6 @@ jest-util@^28.1.3: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - jest-validate@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" @@ -15517,11 +15124,6 @@ js-yaml@^3.10.0, js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -jsbn@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" - integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== - jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -15555,7 +15157,7 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" -jsdom@^26.0.0: +jsdom@^26.1.0: version "26.1.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-26.1.0.tgz#ab5f1c1cafc04bd878725490974ea5e8bf0c72b3" integrity sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg== @@ -15654,9 +15256,9 @@ jsonc-parser@^3.0.0: integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" optionalDependencies: @@ -15806,12 +15408,12 @@ language-tags@^1.0.9: language-subtag-registry "^0.3.20" launch-editor@^2.6.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.10.0.tgz#5ca3edfcb9667df1e8721310f3a40f1127d4bc42" - integrity sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA== + version "2.11.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.11.1.tgz#61a0b7314a42fd84a6cbb564573d9e9ffcf3d72b" + integrity sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg== dependencies: - picocolors "^1.0.0" - shell-quote "^1.8.1" + picocolors "^1.1.1" + shell-quote "^1.8.3" lazy-universal-dotenv@^3.0.1: version "3.0.1" @@ -15824,71 +15426,71 @@ lazy-universal-dotenv@^3.0.1: dotenv "^8.0.0" dotenv-expand "^5.1.0" -lefthook-darwin-arm64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.11.10.tgz#a3a350b938549ec02db60812413aa34e8c066da4" - integrity sha512-Rufl8BRP77GRFtgNwW95/FHPD0VDfu5bRyzASPcyVrFczJiBK1glAHRdYrErBDNqJhEEjkyv9+EkCZS/MnDKPQ== +lefthook-darwin-arm64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.12.4.tgz#b7ba93cf29c22b3bc35902f04191acaa6c2230b9" + integrity sha512-/eBd9GnBS9Js2ZsHzipj2cV8siFex/g6MgBSeIxsHBJNkQFq4O42ItWxUir5Q43zFvZCjGizBlhklbmubGOZfg== -lefthook-darwin-x64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-darwin-x64/-/lefthook-darwin-x64-1.11.10.tgz#a46532cf5df197684088540a9f2bbd430e412095" - integrity sha512-3ReMyC103S+RozcYQlej9RVa1tKr9t8/PGqXbCiWcPAgA9To3GywPk8533qzTs7Nz9fYDiqJMYyQoXovX0Q4SA== +lefthook-darwin-x64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-darwin-x64/-/lefthook-darwin-x64-1.12.4.tgz#c9ff53dc7d921f4c14cb2cef44b8b50815f6002e" + integrity sha512-WDO0oR3pIAIBTZtn4/4dC0GRyrfJtPGckYbqshpH4Fkuxyy7nRGy3su+uY8kiiVYLy/nvELY2eoqnT1Rp4njFQ== -lefthook-freebsd-arm64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.11.10.tgz#7f94cc3f2c48d31904999f4c15bec5cb8a93fcb2" - integrity sha512-UQOdQuvoVEe0HnoVX4Uz8beegndBDKE6Igo5flV3OkrBuO1Cz7dGbTQwzsYg6gBLYUOa8Ecb3Xur80oviQqwnA== +lefthook-freebsd-arm64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.12.4.tgz#8effacf775b8c087db8bc2dba25048fd8e4ec6ac" + integrity sha512-/VNBWQvAsLuVilS7JB+pufTjuoj06Oz5YdGWUCo6u2XCKZ6UHzwDtGDJ0+3JQMSg8613gHmAdkGoByKjxqZSkQ== -lefthook-freebsd-x64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.11.10.tgz#bd560b29a0f710dcf39ef084080639976896cfec" - integrity sha512-IkoywmTzw9dKDtN34HJ8AZkbY3CGu1XpAVU08pIIvlhv0y7PlLGHYTdmx90SC1d4FhTlTMyiANgXyIaAnXjucw== +lefthook-freebsd-x64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.12.4.tgz#bddc3c4b7d68e16177a1b509bacd8200734068df" + integrity sha512-bY6klVVeBoiQEimb/z5TC5IFyczak9VOVQ8b+S/QAy+tvKo9TY6FdGwy7yxgoqTzfEkirDQxVOkalQsM/11xsg== -lefthook-linux-arm64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-linux-arm64/-/lefthook-linux-arm64-1.11.10.tgz#8756fe32646a1522eb16505afa03a644ef93533a" - integrity sha512-l/lH4FSljNSIetcptPKLI5sTBpjS6dJZ4gk9oXoGM0ftvb22AlLcZI4l6NFCC1oLVWM0CbhkbStDGTI5txsVaA== +lefthook-linux-arm64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-linux-arm64/-/lefthook-linux-arm64-1.12.4.tgz#3f5b0383b570aaed666d22ed8cbdb02b44fdcb9a" + integrity sha512-iU+tPCNcX1pztk5Zjs02+sOnjZj9kCrLn6pg954WMr9dZTIaEBljRV+ybBP/5zLlv2wfv5HFBDKDKNRYjOVF+A== -lefthook-linux-x64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-linux-x64/-/lefthook-linux-x64-1.11.10.tgz#61d9745262ce8b6bec0f8317fc3e21871dee06ce" - integrity sha512-yAIIP711p7t0Z9zLfPtdSx1d7pSgtnuVC5B9PANud3I0JOs82aCzmqpc9Q/zp+imWXdI2PpZlFyKx8GLrDW5BQ== +lefthook-linux-x64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-linux-x64/-/lefthook-linux-x64-1.12.4.tgz#7176d48b24640dcf5ea2afd0d52309c9ca3d3662" + integrity sha512-IXYUSBYetftYmdii2aGIjv7kxO2m+jTYjaEoldtCDcXAPz/yV78Xx2WzY/LYNJsJ1vzbUhBqVOeRCHCwLXusTQ== -lefthook-openbsd-arm64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.11.10.tgz#cb6bc0ba8bd45f9b2c1947fce5cad4ad105e6a2f" - integrity sha512-OAqg9BLsTaeioCJduzZrRLupA2dhTOwHOX0GkO4HTSrOD85JuEPqr5RbYoJ7zuzTQcJEXTJYzaeATM2QHjp/aQ== +lefthook-openbsd-arm64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.12.4.tgz#0a5dbc2fb68f74f2b5fa943e4c99f53b1f4811a1" + integrity sha512-3DFLbqAlAeoqo//PE20NcGKJzBqAMbS/roPvaJ9DYA95MSywMig2jxyDoZbBhyP/J/iuFO3op7emtwgwousckA== -lefthook-openbsd-x64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.11.10.tgz#170d4ce87a9e97c83728ccce1baedb9ac82ae6ba" - integrity sha512-EiUU3mFvqcUdnj3gt0V0gRpQQp0b70cLDSA0LgZyFMM4UimeMbA7OgNYl72RKJgrHcTPHrQc4Vj7Mowbhb/X5w== +lefthook-openbsd-x64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.12.4.tgz#7b6922fc98e0acc2ede63bc6ce3915f8a6c8c121" + integrity sha512-Nlxn3lXHK3hRDL5bP5W6+LleE9CRIc6GJ84xTo9EPwI40utsM8olAm+pFFRnE9szkHvQTkXwoBhqi2C5laxoGQ== -lefthook-windows-arm64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-windows-arm64/-/lefthook-windows-arm64-1.11.10.tgz#b21f232a700fc24c56786142fcab0d1d5a54131f" - integrity sha512-clKfI95dCpzxJ1zVgcuYWlSl2oNbtAALoMGqYrzJsoy+CAi+vIs54sqJoGOE60+zrVbdk65z8hriCoYNr98SgA== +lefthook-windows-arm64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-windows-arm64/-/lefthook-windows-arm64-1.12.4.tgz#1c3f0c787f26d3dddde4a9cf9e7c9dc1ecedb4f3" + integrity sha512-tWOfrTC9GNheaFXFt49G5nbBUYLqd2NBb5XW97dSLO/lU81cvuvRsMKZFBrq48LvByT7PLwEuibMuO1TminhHA== -lefthook-windows-x64@1.11.10: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook-windows-x64/-/lefthook-windows-x64-1.11.10.tgz#d985b76a9ec9f3da2b9662da96165787005e9dfa" - integrity sha512-zpf/0sG50xsGnwVG/a2giUbmaM/g0uIRqxN5qBbmwKCf0P4PPD2r1xiFZNDb520+tUTC1lWe0RWVoSSwZbBQRA== +lefthook-windows-x64@1.12.4: + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook-windows-x64/-/lefthook-windows-x64-1.12.4.tgz#6101f18c60f144cba19c41bba16a796167406f8d" + integrity sha512-3B295z3tdcdDrKrY98b/cSm4Elb/TXWMVQuH2xW15CJp9QY6jsgRpFJyBdyz4ggrPFhNUVnLKCpm6/saqeZWHA== lefthook@^1.8.5: - version "1.11.10" - resolved "https://registry.yarnpkg.com/lefthook/-/lefthook-1.11.10.tgz#94cc557a55dae92c53c91542fa1ea80968b34137" - integrity sha512-nuiRqBADcRiU6dzwf2H1zBCsdcWGEOsxY8hqoXw5nkEuoTEYN1Bwi2vskHXjIzJ62iCOCo4FZhcHBAzT9gwL5g== + version "1.12.4" + resolved "https://registry.yarnpkg.com/lefthook/-/lefthook-1.12.4.tgz#c5735a8ab27a5cbcdce4e05f0a2ab70d7895fa7e" + integrity sha512-VhTFYGT55pD2hytjcn6Lckb0tCbG1Cke6rszTWVQVJpnJZ0EqQW+Pl+JYQLlruR8MO4RGFVU0UBUw17/g9TYxA== optionalDependencies: - lefthook-darwin-arm64 "1.11.10" - lefthook-darwin-x64 "1.11.10" - lefthook-freebsd-arm64 "1.11.10" - lefthook-freebsd-x64 "1.11.10" - lefthook-linux-arm64 "1.11.10" - lefthook-linux-x64 "1.11.10" - lefthook-openbsd-arm64 "1.11.10" - lefthook-openbsd-x64 "1.11.10" - lefthook-windows-arm64 "1.11.10" - lefthook-windows-x64 "1.11.10" + lefthook-darwin-arm64 "1.12.4" + lefthook-darwin-x64 "1.12.4" + lefthook-freebsd-arm64 "1.12.4" + lefthook-freebsd-x64 "1.12.4" + lefthook-linux-arm64 "1.12.4" + lefthook-linux-x64 "1.12.4" + lefthook-openbsd-arm64 "1.12.4" + lefthook-openbsd-x64 "1.12.4" + lefthook-windows-arm64 "1.12.4" + lefthook-windows-x64 "1.12.4" lerna@^7.3.0: version "7.4.2" @@ -16237,9 +15839,9 @@ loud-rejection@^1.0.0: signal-exit "^3.0.0" loupe@^3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" - integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== + version "3.2.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.2.1.tgz#0095cf56dc5b7a9a7c08ff5b1a8796ec8ad17e76" + integrity sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== lower-case@^2.0.2: version "2.0.2" @@ -16253,6 +15855,11 @@ lru-cache@^10.2.0, lru-cache@^10.4.3: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.0.0: + version "11.2.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.1.tgz#d426ac471521729c6c1acda5f7a633eadaa28db2" + integrity sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -16302,11 +15909,11 @@ magic-string@^0.27.0: "@jridgewell/sourcemap-codec" "^1.4.13" magic-string@^0.30.2: - version "0.30.17" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" - integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + version "0.30.19" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9" + integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw== dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/sourcemap-codec" "^1.5.5" make-dir@4.0.0, make-dir@^4.0.0: version "4.0.0" @@ -16429,16 +16036,6 @@ marked@^4.0.16: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== -material-react-table@^2.12.1: - version "2.13.3" - resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-2.13.3.tgz#c61de4105efb3eb09697ed5fc2544d174675de31" - integrity sha512-xeyAEG6UYG3qgBIo17epAP5zsWT1pH0uCEkaUxvhki9sGcP35OqfOMSZJNhISvmqEqXKYHdqKbZI6iOwsg1sYA== - dependencies: - "@tanstack/match-sorter-utils" "8.19.4" - "@tanstack/react-table" "8.20.5" - "@tanstack/react-virtual" "3.10.6" - highlight-words "1.2.2" - material-react-table@^3.0.3: version "3.2.1" resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.2.1.tgz#56f595755cab3b669b399999fed9eb305fbb6dd7" @@ -17013,9 +16610,9 @@ min-indent@^1.0.0: integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== mini-css-extract-plugin@^2.2.2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz#966031b468917a5446f4c24a80854b2947503c5b" - integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== + version "2.9.4" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz#cafa1a42f8c71357f49cd1566810d74ff1cb0200" + integrity sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" @@ -17030,6 +16627,13 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== +minimatch@*, minimatch@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" + integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== + dependencies: + "@isaacs/brace-expansion" "^5.0.0" + minimatch@3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" @@ -17037,13 +16641,6 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -17065,7 +16662,7 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0, minimatch@^9.0.1, minimatch@^9.0.4: +minimatch@^9.0.0, minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -17275,12 +16872,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3, ms@~2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -17315,16 +16907,16 @@ mute-stream@^1.0.0, mute-stream@~1.0.0: integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== nan@^2.12.1, nan@^2.13.2: - version "2.22.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" - integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== + version "2.23.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.23.0.tgz#24aa4ddffcc37613a2d2935b97683c1ec96093c6" + integrity sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ== nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== -nanoid@^3.3.1, nanoid@^3.3.6, nanoid@^3.3.8: +nanoid@^3.3.1, nanoid@^3.3.11, nanoid@^3.3.6: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -17346,6 +16938,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-postinstall@^0.3.0: + version "0.3.3" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.3.tgz#93d045c6b576803ead126711d3093995198c6eb9" + integrity sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -17376,12 +16973,12 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw== -next@14.2.21: - version "14.2.21" - resolved "https://registry.yarnpkg.com/next/-/next-14.2.21.tgz#f6da9e2abba1a0e4ca7a5273825daf06632554ba" - integrity sha512-rZmLwucLHr3/zfDMYbJXbw0ZeoBpirxkXuvsJbk7UPorvPYZhP7vq7aHbKnU7dQNCYIimRrbB2pp3xmf+wsYUg== +next@^14.2.26: + version "14.2.32" + resolved "https://registry.yarnpkg.com/next/-/next-14.2.32.tgz#279b544f0c8ed023c33454ce4d563d3e05c2f3fb" + integrity sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg== dependencies: - "@next/env" "14.2.21" + "@next/env" "14.2.32" "@swc/helpers" "0.5.5" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -17389,38 +16986,15 @@ next@14.2.21: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.2.21" - "@next/swc-darwin-x64" "14.2.21" - "@next/swc-linux-arm64-gnu" "14.2.21" - "@next/swc-linux-arm64-musl" "14.2.21" - "@next/swc-linux-x64-gnu" "14.2.21" - "@next/swc-linux-x64-musl" "14.2.21" - "@next/swc-win32-arm64-msvc" "14.2.21" - "@next/swc-win32-ia32-msvc" "14.2.21" - "@next/swc-win32-x64-msvc" "14.2.21" - -next@^15.2.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/next/-/next-15.3.0.tgz#f8aa64ab584664db75648bd87d5d68c02eb2d56f" - integrity sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ== - dependencies: - "@next/env" "15.3.0" - "@swc/counter" "0.1.3" - "@swc/helpers" "0.5.15" - busboy "1.6.0" - caniuse-lite "^1.0.30001579" - postcss "8.4.31" - styled-jsx "5.1.6" - optionalDependencies: - "@next/swc-darwin-arm64" "15.3.0" - "@next/swc-darwin-x64" "15.3.0" - "@next/swc-linux-arm64-gnu" "15.3.0" - "@next/swc-linux-arm64-musl" "15.3.0" - "@next/swc-linux-x64-gnu" "15.3.0" - "@next/swc-linux-x64-musl" "15.3.0" - "@next/swc-win32-arm64-msvc" "15.3.0" - "@next/swc-win32-x64-msvc" "15.3.0" - sharp "^0.34.1" + "@next/swc-darwin-arm64" "14.2.32" + "@next/swc-darwin-x64" "14.2.32" + "@next/swc-linux-arm64-gnu" "14.2.32" + "@next/swc-linux-arm64-musl" "14.2.32" + "@next/swc-linux-x64-gnu" "14.2.32" + "@next/swc-linux-x64-musl" "14.2.32" + "@next/swc-win32-arm64-msvc" "14.2.32" + "@next/swc-win32-ia32-msvc" "14.2.32" + "@next/swc-win32-x64-msvc" "14.2.32" nice-try@^1.0.4: version "1.0.5" @@ -17461,10 +17035,10 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch-native@^1.6.4, node-fetch-native@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz#ae1d0e537af35c2c0b0de81cbff37eedd410aa37" - integrity sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ== +node-fetch-native@^1.6.4, node-fetch-native@^1.6.7: + version "1.6.7" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz#9d09ca63066cc48423211ed4caf5d70075d76a71" + integrity sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q== node-fetch@2.6.7: version "2.6.7" @@ -17546,31 +17120,15 @@ node-machine-id@1.1.12: resolved "https://registry.yarnpkg.com/node-machine-id/-/node-machine-id-1.1.12.tgz#37904eee1e59b320bb9c5d6c0a59f3b469cb6267" integrity sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ== -node-mock-http@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-mock-http/-/node-mock-http-1.0.0.tgz#4b32cd509c7f46d844e68ea93fb8be405a18a42a" - integrity sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ== +node-mock-http@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/node-mock-http/-/node-mock-http-1.0.3.tgz#4e55e093267a3b910cded7354389ce2d02c89e77" + integrity sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog== node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== - -nodemon@^2.0.21: - version "2.0.22" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" - integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== - dependencies: - chokidar "^3.5.2" - debug "^3.2.7" - ignore-by-default "^1.0.1" - minimatch "^3.1.2" - pstree.remy "^1.1.8" - semver "^5.7.1" - simple-update-notifier "^1.0.7" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" + version "2.0.20" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.20.tgz#e26bb79dbdd1e64a146df389c699014c611cbc27" + integrity sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA== nopt@^6.0.0: version "6.0.0" @@ -17791,9 +17349,9 @@ num2fraction@^1.2.2: integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== nwsapi@^2.2.0, nwsapi@^2.2.16: - version "2.2.20" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef" - integrity sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA== + version "2.2.22" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.22.tgz#109f9530cda6c156d6a713cdf5939e9f0de98b9d" + integrity sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ== nx@16.10.0, "nx@>=16.5.1 < 17": version "16.10.0" @@ -17862,7 +17420,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.13.3: +object-inspect@^1.13.3, object-inspect@^1.13.4: version "1.13.4" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== @@ -17948,7 +17506,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0, object.values@^1.1.6, object.values@^1.2.0, object.values@^1.2.1: +object.values@^1.1.0, object.values@^1.1.6, object.values@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== @@ -17989,10 +17547,10 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -18026,9 +17584,9 @@ open@^8.0.9, open@^8.4.0: is-wsl "^2.2.0" openapi-fetch@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.13.5.tgz#805606860d85b8ba8c2e7cb36ea30b473d8065d9" - integrity sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q== + version "0.13.8" + resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.13.8.tgz#1769da06e30d19f7568cd0e60db8ca61ea83a2fa" + integrity sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ== dependencies: openapi-typescript-helpers "^0.0.15" @@ -18074,11 +17632,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - own-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" @@ -18394,11 +17947,11 @@ parse5@6.0.1, parse5@^6.0.0, parse5@^6.0.1: integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== parse5@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" - integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== dependencies: - entities "^4.5.0" + entities "^6.0.0" parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" @@ -18475,7 +18028,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1, path-scurry@^1.11.1, path-scurry@^1.6.1: +path-scurry@^1.11.1, path-scurry@^1.6.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -18483,6 +18036,14 @@ path-scurry@^1.10.1, path-scurry@^1.11.1, path-scurry@^1.6.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + path-to-regexp@0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" @@ -18510,15 +18071,16 @@ path-type@^4.0.0: integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + version "3.1.3" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.3.tgz#8be674d591d65658113424592a95d1517318dd4b" + integrity sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA== dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" + create-hash "~1.1.3" + create-hmac "^1.1.7" + ripemd160 "=2.0.1" + safe-buffer "^5.2.1" + sha.js "^2.4.11" + to-buffer "^1.2.0" picocolors@^0.2.1: version "0.2.1" @@ -18535,10 +18097,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatc resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" - integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== +picomatch@^4.0.2, picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== pidtree@^0.3.0: version "0.3.1" @@ -18972,11 +18534,11 @@ postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0 source-map "^0.6.1" postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.33: - version "8.5.3" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" - integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: - nanoid "^3.3.8" + nanoid "^3.3.11" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -19018,6 +18580,15 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" +pretty-format@30.0.5, pretty-format@^30.0.0: + version "30.0.5" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.0.5.tgz#e001649d472800396c1209684483e18a4d250360" + integrity sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw== + dependencies: + "@jest/schemas" "30.0.5" + ansi-styles "^5.2.0" + react-is "^18.3.1" + pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -19037,7 +18608,7 @@ pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.0.0, pretty-format@^29.7.0: +pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== @@ -19154,9 +18725,9 @@ property-information@^5.0.0, property-information@^5.3.0: xtend "^4.0.0" property-information@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.0.0.tgz#3508a6d6b0b8eb3ca6eb2c6623b164d2ed2ab112" - integrity sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg== + version "7.1.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== protobufjs@^6.11.2, protobufjs@^6.8.8: version "6.11.4" @@ -19207,11 +18778,6 @@ psl@^1.1.33: dependencies: punycode "^2.3.1" -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - public-encrypt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -19233,9 +18799,9 @@ pump@^2.0.0: once "^1.3.1" pump@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.2.tgz#836f3edd6bc2ee599256c924ffe0d88573ddcbf8" - integrity sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" + integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA== dependencies: end-of-stream "^1.1.0" once "^1.3.1" @@ -19381,57 +18947,57 @@ raw-loader@^4.0.2: schema-utils "^3.0.0" react-aria@^3.34.3: - version "3.39.0" - resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.39.0.tgz#68e01a25365f403cfbc870b7ec29093d8722f0ba" - integrity sha512-zXCjR01WnfW4uW0f294uWrvdfwEMHgDFSwMwMBwRafAvmsQea87X5VTAfDmQOAbPa+iQFcngIyH0Pn5CfXNrjw== + version "3.43.1" + resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.43.1.tgz#dbd7f9ab9e642b1c485e07520e0745a4a0d5a046" + integrity sha512-/PmZGiw+Ya/YtzXmiLW4ALD4SMuDnbwhMaVh33VCduTl8vVujIUzUTIi5g4C+YHLDs/Z4edsN3aQsPMqFfg1xA== dependencies: - "@internationalized/string" "^3.2.6" - "@react-aria/breadcrumbs" "^3.5.23" - "@react-aria/button" "^3.13.0" - "@react-aria/calendar" "^3.8.0" - "@react-aria/checkbox" "^3.15.4" - "@react-aria/color" "^3.0.6" - "@react-aria/combobox" "^3.12.2" - "@react-aria/datepicker" "^3.14.2" - "@react-aria/dialog" "^3.5.24" - "@react-aria/disclosure" "^3.0.4" - "@react-aria/dnd" "^3.9.2" - "@react-aria/focus" "^3.20.2" - "@react-aria/gridlist" "^3.12.0" - "@react-aria/i18n" "^3.12.8" - "@react-aria/interactions" "^3.25.0" - "@react-aria/label" "^3.7.17" - "@react-aria/landmark" "^3.0.2" - "@react-aria/link" "^3.8.0" - "@react-aria/listbox" "^3.14.3" - "@react-aria/menu" "^3.18.2" - "@react-aria/meter" "^3.4.22" - "@react-aria/numberfield" "^3.11.13" - "@react-aria/overlays" "^3.27.0" - "@react-aria/progress" "^3.4.22" - "@react-aria/radio" "^3.11.2" - "@react-aria/searchfield" "^3.8.3" - "@react-aria/select" "^3.15.4" - "@react-aria/selection" "^3.24.0" - "@react-aria/separator" "^3.4.8" - "@react-aria/slider" "^3.7.18" - "@react-aria/ssr" "^3.9.8" - "@react-aria/switch" "^3.7.2" - "@react-aria/table" "^3.17.2" - "@react-aria/tabs" "^3.10.2" - "@react-aria/tag" "^3.5.2" - "@react-aria/textfield" "^3.17.2" - "@react-aria/toast" "^3.0.2" - "@react-aria/tooltip" "^3.8.2" - "@react-aria/tree" "^3.0.2" - "@react-aria/utils" "^3.28.2" - "@react-aria/visually-hidden" "^3.8.22" - "@react-types/shared" "^3.29.0" + "@internationalized/string" "^3.2.7" + "@react-aria/breadcrumbs" "^3.5.28" + "@react-aria/button" "^3.14.1" + "@react-aria/calendar" "^3.9.1" + "@react-aria/checkbox" "^3.16.1" + "@react-aria/color" "^3.1.1" + "@react-aria/combobox" "^3.13.2" + "@react-aria/datepicker" "^3.15.1" + "@react-aria/dialog" "^3.5.30" + "@react-aria/disclosure" "^3.0.8" + "@react-aria/dnd" "^3.11.2" + "@react-aria/focus" "^3.21.1" + "@react-aria/gridlist" "^3.14.0" + "@react-aria/i18n" "^3.12.12" + "@react-aria/interactions" "^3.25.5" + "@react-aria/label" "^3.7.21" + "@react-aria/landmark" "^3.0.6" + "@react-aria/link" "^3.8.5" + "@react-aria/listbox" "^3.14.8" + "@react-aria/menu" "^3.19.2" + "@react-aria/meter" "^3.4.26" + "@react-aria/numberfield" "^3.12.1" + "@react-aria/overlays" "^3.29.1" + "@react-aria/progress" "^3.4.26" + "@react-aria/radio" "^3.12.1" + "@react-aria/searchfield" "^3.8.8" + "@react-aria/select" "^3.16.2" + "@react-aria/selection" "^3.25.1" + "@react-aria/separator" "^3.4.12" + "@react-aria/slider" "^3.8.1" + "@react-aria/ssr" "^3.9.10" + "@react-aria/switch" "^3.7.7" + "@react-aria/table" "^3.17.7" + "@react-aria/tabs" "^3.10.7" + "@react-aria/tag" "^3.7.1" + "@react-aria/textfield" "^3.18.1" + "@react-aria/toast" "^3.0.7" + "@react-aria/tooltip" "^3.8.7" + "@react-aria/tree" "^3.1.3" + "@react-aria/utils" "^3.30.1" + "@react-aria/visually-hidden" "^3.8.27" + "@react-types/shared" "^3.32.0" react-docgen-typescript@^2.1.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c" - integrity sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg== + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz#033428b4a6a639d050ac8baf2a5195c596521713" + integrity sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg== react-docgen@^5.0.0: version "5.4.3" @@ -19449,7 +19015,7 @@ react-docgen@^5.0.0: node-dir "^0.1.10" strip-indent "^3.0.0" -react-dom@^18, react-dom@^18.0.0, react-dom@^18.2.0: +react-dom@^18.0.0, react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -19473,24 +19039,17 @@ react-error-boundary@^3.1.3: dependencies: "@babel/runtime" "^7.12.5" -react-error-boundary@^4.0.13: - version "4.1.2" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289" - integrity sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag== - dependencies: - "@babel/runtime" "^7.12.5" - react-hook-form@^7.14.2: - version "7.55.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.55.0.tgz#df3c80a20a68f6811f49bec3406defaefb6dce80" - integrity sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog== + version "7.62.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.62.0.tgz#2d81e13c2c6b6d636548e440818341ca753218d0" + integrity sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA== react-i18next@^15.4.0: - version "15.4.1" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758" - integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw== + version "15.7.3" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.7.3.tgz#2eba235247dff0cbf9f0338e2ab85e10e127aa54" + integrity sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw== dependencies: - "@babel/runtime" "^7.25.0" + "@babel/runtime" "^7.27.6" html-parse-stringify "^3.0.1" react-inspector@^5.1.0: @@ -19517,10 +19076,10 @@ react-is@^18.0.0, react-is@^18.3.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-is@^19.0.0, react-is@^19.1.0: - version "19.1.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.0.tgz#805bce321546b7e14c084989c77022351bbdd11b" - integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg== +react-is@^19.0.0, react-is@^19.1.1: + version "19.1.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.1.tgz#038ebe313cf18e1fd1235d51c87360eb87f7c36a" + integrity sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA== react-markdown@^9.0.3: version "9.1.0" @@ -19565,30 +19124,20 @@ react-refresh@^0.11.0: integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== react-router-dom@6: - version "6.30.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.0.tgz#a64774104508bff56b1affc2796daa3f7e76b7df" - integrity sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA== + version "6.30.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.1.tgz#da2580c272ddb61325e435478566be9563a4a237" + integrity sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw== dependencies: "@remix-run/router" "1.23.0" - react-router "6.30.0" + react-router "6.30.1" -react-router@6.30.0: - version "6.30.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.0.tgz#9789d775e63bc0df60f39ced77c8c41f1e01ff90" - integrity sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ== +react-router@6.30.1: + version "6.30.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.1.tgz#ecb3b883c9ba6dbf5d319ddbc996747f4ab9f4c3" + integrity sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ== dependencies: "@remix-run/router" "1.23.0" -react-simple-maps@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/react-simple-maps/-/react-simple-maps-3.0.0.tgz#2349d884c9ba37b68695b9f5e1e7d9c2a826c00e" - integrity sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw== - dependencies: - d3-geo "^2.0.2" - d3-selection "^2.0.0" - d3-zoom "^2.0.0" - topojson-client "^3.1.0" - react-smooth@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.4.tgz#a5875f8bb61963ca61b819cedc569dc2453894b4" @@ -19599,44 +19148,36 @@ react-smooth@^4.0.4: react-transition-group "^4.4.5" react-stately@^3.32.2: - version "3.37.0" - resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.37.0.tgz#9bd09ecd1c7b11461ec60e17a7c670c17a64962e" - integrity sha512-fm2LRM3XN5lJD48+WQKWvESx54kAIHw0JztCRHMsFmTDgYWX/VASuXKON7LECv227stSEadrxGa8LhPkcelljw== + version "3.41.0" + resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.41.0.tgz#e8239f520cf2cbaa037c0fd9ddf274a9a6bcb3bd" + integrity sha512-Fe8PaZPm9Ue9kDXVa8KaOz6gzbmZPuzftxeVQwKVX3u/kyFhbRkr/LeAFvgP7a+EeX+Bjmdht/9ixDsBXj4qbQ== dependencies: - "@react-stately/calendar" "^3.8.0" - "@react-stately/checkbox" "^3.6.13" - "@react-stately/collections" "^3.12.3" - "@react-stately/color" "^3.8.4" - "@react-stately/combobox" "^3.10.4" - "@react-stately/data" "^3.12.3" - "@react-stately/datepicker" "^3.14.0" - "@react-stately/disclosure" "^3.0.3" - "@react-stately/dnd" "^3.5.3" - "@react-stately/form" "^3.1.3" - "@react-stately/list" "^3.12.1" - "@react-stately/menu" "^3.9.3" - "@react-stately/numberfield" "^3.9.11" - "@react-stately/overlays" "^3.6.15" - "@react-stately/radio" "^3.10.12" - "@react-stately/searchfield" "^3.5.11" - "@react-stately/select" "^3.6.12" - "@react-stately/selection" "^3.20.1" - "@react-stately/slider" "^3.6.3" - "@react-stately/table" "^3.14.1" - "@react-stately/tabs" "^3.8.1" - "@react-stately/toast" "^3.1.0" - "@react-stately/toggle" "^3.8.3" - "@react-stately/tooltip" "^3.5.3" - "@react-stately/tree" "^3.8.9" - "@react-types/shared" "^3.29.0" - -react-tooltip@^5.28.1: - version "5.28.1" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.28.1.tgz#5b25c7c53ce008b7ad0685e9f516101d80925cbc" - integrity sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ== - dependencies: - "@floating-ui/dom" "^1.6.1" - classnames "^2.3.0" + "@react-stately/calendar" "^3.8.4" + "@react-stately/checkbox" "^3.7.1" + "@react-stately/collections" "^3.12.7" + "@react-stately/color" "^3.9.1" + "@react-stately/combobox" "^3.11.1" + "@react-stately/data" "^3.14.0" + "@react-stately/datepicker" "^3.15.1" + "@react-stately/disclosure" "^3.0.7" + "@react-stately/dnd" "^3.7.0" + "@react-stately/form" "^3.2.1" + "@react-stately/list" "^3.13.0" + "@react-stately/menu" "^3.9.7" + "@react-stately/numberfield" "^3.10.1" + "@react-stately/overlays" "^3.6.19" + "@react-stately/radio" "^3.11.1" + "@react-stately/searchfield" "^3.5.15" + "@react-stately/select" "^3.7.1" + "@react-stately/selection" "^3.20.5" + "@react-stately/slider" "^3.7.1" + "@react-stately/table" "^3.15.0" + "@react-stately/tabs" "^3.8.5" + "@react-stately/toast" "^3.1.2" + "@react-stately/toggle" "^3.9.1" + "@react-stately/tooltip" "^3.5.7" + "@react-stately/tree" "^3.9.2" + "@react-types/shared" "^3.32.0" react-transition-group@^4.4.5: version "4.4.5" @@ -19657,7 +19198,7 @@ react-world-flags@^1.6.0: svgo "^3.0.2" world-countries "^5.0.0" -react@^18, react@^18.0.0, react@^18.2.0: +react@^18.0.0, react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -19815,9 +19356,9 @@ recharts-scale@^0.4.4: decimal.js-light "^2.4.1" recharts@^2.1.13: - version "2.15.2" - resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.2.tgz#12adcda964b4054fba517512eb94fdff1463b0c1" - integrity sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw== + version "2.15.4" + resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.15.4.tgz#0ed3e66c0843bcf2d9f9a172caf97b1d05127a5f" + integrity sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw== dependencies: clsx "^2.0.0" eventemitter3 "^4.0.1" @@ -19865,10 +19406,10 @@ reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: get-proto "^1.0.1" which-builtin-type "^1.2.1" -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== dependencies: regenerate "^1.4.2" @@ -19882,18 +19423,6 @@ regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.7: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -19902,7 +19431,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.3: +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== @@ -19920,16 +19449,16 @@ regexpp@^3.1.0: integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== regexpu-core@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" - integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + version "6.3.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.3.0.tgz#ebdc3ec610146d01fd64871333d34b0200e00145" + integrity sha512-ulzJYRb0qgR4t8eTgHeL7nnKL/4ul2yjnuTBEDIpYG7cSs8CcADE1q18RFFChXLP8WwRgPrHThGbYplvASdujw== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" + regenerate-unicode-properties "^10.2.2" regjsgen "^0.8.0" regjsparser "^0.12.0" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" + unicode-match-property-value-ecmascript "^2.2.1" regjsgen@^0.8.0: version "0.8.0" @@ -20106,11 +19635,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reselect@^4.1.8: - version "4.1.8" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" - integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -20143,7 +19667,7 @@ resolve.exports@^1.1.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.1.tgz#05cfd5b3edf641571fd46fa608b610dda9ead999" integrity sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ== -resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.3.2, resolve@^1.9.0: +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.10, resolve@^1.22.4, resolve@^1.3.2, resolve@^1.9.0: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -20215,6 +19739,14 @@ rimraf@^4.4.1: dependencies: glob "^9.2.0" +ripemd160@=2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w== + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -20314,12 +19846,7 @@ safe-array-concat@^1.1.2, safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0, safe-buffer@~5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -20440,10 +19967,10 @@ schema-utils@^3.0.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" - integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== +schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.2.tgz#0c10878bf4a73fd2b1dfd14b9462b26788c806ae" + integrity sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -20463,7 +19990,7 @@ selfsigned@^2.1.1: "@types/node-forge" "^1.3.0" node-forge "^1" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -20476,20 +20003,15 @@ semver@7.5.3: lru-cache "^6.0.0" semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.0, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@^7.7.1: - version "7.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" - integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== semver@^6.0.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -20531,15 +20053,15 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.2: randombytes "^2.1.0" serve-favicon@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0" - integrity sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA== + version "2.5.1" + resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.1.tgz#b6482e81801707d5ed40fa547066e7e44fee47cc" + integrity sha512-JndLBslCLA/ebr7rS3d+/EKkzTsTi1jI2T9l+vHfAaGJ7A7NhtDpSZ0lx81HCNWnnE0yHncG+SSnVf9IMxOwXQ== dependencies: etag "~1.8.1" - fresh "0.5.2" - ms "2.1.1" + fresh "~0.5.2" + ms "~2.1.3" parseurl "~1.3.2" - safe-buffer "5.1.1" + safe-buffer "~5.2.1" serve-index@^1.9.1: version "1.9.1" @@ -20626,12 +20148,13 @@ setprototypeof@1.2.0: integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + version "2.4.12" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.0" shallow-clone@^3.0.0: version "3.0.1" @@ -20669,36 +20192,6 @@ sharp@^0.33.1: "@img/sharp-win32-ia32" "0.33.5" "@img/sharp-win32-x64" "0.33.5" -sharp@^0.34.1: - version "0.34.1" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.1.tgz#e5922894b0cc7ddf159eeabc6d5668e4e8b11d61" - integrity sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg== - dependencies: - color "^4.2.3" - detect-libc "^2.0.3" - semver "^7.7.1" - optionalDependencies: - "@img/sharp-darwin-arm64" "0.34.1" - "@img/sharp-darwin-x64" "0.34.1" - "@img/sharp-libvips-darwin-arm64" "1.1.0" - "@img/sharp-libvips-darwin-x64" "1.1.0" - "@img/sharp-libvips-linux-arm" "1.1.0" - "@img/sharp-libvips-linux-arm64" "1.1.0" - "@img/sharp-libvips-linux-ppc64" "1.1.0" - "@img/sharp-libvips-linux-s390x" "1.1.0" - "@img/sharp-libvips-linux-x64" "1.1.0" - "@img/sharp-libvips-linuxmusl-arm64" "1.1.0" - "@img/sharp-libvips-linuxmusl-x64" "1.1.0" - "@img/sharp-linux-arm" "0.34.1" - "@img/sharp-linux-arm64" "0.34.1" - "@img/sharp-linux-s390x" "0.34.1" - "@img/sharp-linux-x64" "0.34.1" - "@img/sharp-linuxmusl-arm64" "0.34.1" - "@img/sharp-linuxmusl-x64" "0.34.1" - "@img/sharp-wasm32" "0.34.1" - "@img/sharp-win32-ia32" "0.34.1" - "@img/sharp-win32-x64" "0.34.1" - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -20723,10 +20216,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1, shell-quote@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.2.tgz#d2d83e057959d53ec261311e9e9b8f51dcb2934a" - integrity sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA== +shell-quote@^1.6.1, shell-quote@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== shiki@^0.10.1: version "0.10.1" @@ -20805,13 +20298,6 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -simple-update-notifier@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" - integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== - dependencies: - semver "~7.0.0" - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -20895,11 +20381,11 @@ socks-proxy-agent@^7.0.0: socks "^2.6.2" socks@^2.6.2: - version "2.8.4" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.4.tgz#07109755cdd4da03269bda4725baa061ab56d5cc" - integrity sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ== + version "2.8.7" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" + integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== dependencies: - ip-address "^9.0.5" + ip-address "^10.0.1" smart-buffer "^4.2.0" sonic-boom@^2.2.1: @@ -20961,9 +20447,9 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.7.3, source-map@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + version "0.7.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.6.tgz#a3658ab87e5b6429c8a1f3ba0083d4c61ca3ef02" + integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== sourcemap-codec@^1.4.8: version "1.4.8" @@ -21002,9 +20488,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.21" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3" - integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== + version "3.0.22" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz#abf5a08a6f5d7279559b669f47f0a43e8f3464ef" + integrity sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ== spdy-transport@^3.0.0: version "3.0.0" @@ -21055,11 +20541,6 @@ split@^1.0.1: dependencies: through "2" -sprintf-js@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -21103,7 +20584,7 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stack-utils@^2.0.3: +stack-utils@^2.0.3, stack-utils@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== @@ -21138,7 +20619,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stop-iteration-iterator@^1.0.0: +stop-iteration-iterator@^1.0.0, stop-iteration-iterator@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== @@ -21305,7 +20786,7 @@ string.prototype.trim@^1.2.10: es-object-atoms "^1.0.0" has-property-descriptors "^1.0.2" -string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9: +string.prototype.trimend@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== @@ -21368,9 +20849,9 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: ansi-regex "^5.0.1" strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== dependencies: ansi-regex "^6.0.1" @@ -21451,11 +20932,11 @@ style-loader@^3.3.1: integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== style-to-js@^1.0.0: - version "1.1.16" - resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.16.tgz#e6bd6cd29e250bcf8fa5e6591d07ced7575dbe7a" - integrity sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw== + version "1.1.17" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.17.tgz#488b1558a8c1fd05352943f088cc3ce376813d83" + integrity sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA== dependencies: - style-to-object "1.0.8" + style-to-object "1.0.9" style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" @@ -21464,10 +20945,10 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -style-to-object@1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292" - integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g== +style-to-object@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.9.tgz#35c65b713f4a6dba22d3d0c61435f965423653f0" + integrity sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw== dependencies: inline-style-parser "0.2.4" @@ -21478,13 +20959,6 @@ styled-jsx@5.1.1: dependencies: client-only "0.0.1" -styled-jsx@5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" - integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== - dependencies: - client-only "0.0.1" - stylehacks@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" @@ -21498,7 +20972,7 @@ stylis@4.2.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -21618,9 +21092,9 @@ tapable@^1.0.0, tapable@^1.1.3: integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + version "2.2.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.3.tgz#4b67b635b2d97578a06a2713d2f04800c237e99b" + integrity sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg== tar-stream@~2.2.0: version "2.2.0" @@ -21735,12 +21209,12 @@ terser@^4.1.2, terser@^4.6.3: source-map-support "~0.5.12" terser@^5.10.0, terser@^5.3.4, terser@^5.31.1: - version "5.39.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.0.tgz#0e82033ed57b3ddf1f96708d123cca717d86ca3a" - integrity sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw== + version "5.44.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.0.tgz#ebefb8e5b8579d93111bfdfc39d2cf63879f4a82" + integrity sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w== dependencies: "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" + acorn "^8.15.0" commander "^2.20.0" source-map-support "~0.5.20" @@ -21832,13 +21306,13 @@ tiny-warning@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tinyglobby@^0.2.12: - version "0.2.12" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" - integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== +tinyglobby@^0.2.13: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== dependencies: - fdir "^6.4.3" - picomatch "^4.0.2" + fdir "^6.5.0" + picomatch "^4.0.3" tinyrainbow@^1.2.0: version "1.2.0" @@ -21857,17 +21331,10 @@ tldts@^6.1.32: dependencies: tldts-core "^6.1.86" -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - tmp@~0.2.1: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== tmpl@1.0.5: version "1.0.5" @@ -21879,6 +21346,15 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== +to-buffer@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.1.tgz#2ce650cdb262e9112a18e65dc29dcb513c8155e0" + integrity sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ== + dependencies: + isarray "^2.0.5" + safe-buffer "^5.2.1" + typed-array-buffer "^1.0.3" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -21921,23 +21397,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -topojson-client@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99" - integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw== - dependencies: - commander "2" - toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -touch@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" - integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== - tough-cookie@^4.0.0: version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" @@ -21963,9 +21427,9 @@ tr46@^2.1.0: punycode "^2.1.1" tr46@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.0.tgz#4a077922360ae807e172075ce5beb79b36e4a101" - integrity sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw== + version "5.1.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca" + integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw== dependencies: punycode "^2.3.1" @@ -22009,12 +21473,7 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -ts-api-utils@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" - integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== - -ts-api-utils@^2.0.1: +ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== @@ -22039,9 +21498,9 @@ ts-jest@^27.0.5: yargs-parser "20.x" ts-loader@^9.4.2: - version "9.5.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.2.tgz#1f3d7f4bb709b487aaa260e8f19b301635d08020" - integrity sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw== + version "9.5.4" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.4.tgz#44b571165c10fb5a90744aa5b7e119233c4f4585" + integrity sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" @@ -22058,25 +21517,6 @@ ts-mocha@^10.0.0: optionalDependencies: tsconfig-paths "^3.5.0" -ts-node@10: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - ts-node@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" @@ -22301,16 +21741,16 @@ typeforce@^1.11.5: integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== "typescript@>=3 < 6", typescript@^5: - version "5.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" - integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== typescript@^4.6.2, typescript@^4.8.4: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== -ufo@^1.5.4: +ufo@^1.5.4, ufo@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== @@ -22335,21 +21775,16 @@ uncrypto@^0.1.3: resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - undici-types@~6.21.0: version "6.21.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== +undici-types@~7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" + integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -22376,10 +21811,10 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" - integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" @@ -22579,27 +22014,32 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -unrs-resolver@^1.3.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.5.0.tgz#d0a608f08321d8e90ba8eb10a3240e7995997275" - integrity sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA== +unrs-resolver@^1.6.2: + version "1.11.1" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" + integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== + dependencies: + napi-postinstall "^0.3.0" optionalDependencies: - "@unrs/resolver-binding-darwin-arm64" "1.5.0" - "@unrs/resolver-binding-darwin-x64" "1.5.0" - "@unrs/resolver-binding-freebsd-x64" "1.5.0" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.5.0" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.5.0" - "@unrs/resolver-binding-linux-arm64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-arm64-musl" "1.5.0" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-riscv64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-s390x-gnu" "1.5.0" - "@unrs/resolver-binding-linux-x64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-x64-musl" "1.5.0" - "@unrs/resolver-binding-wasm32-wasi" "1.5.0" - "@unrs/resolver-binding-win32-arm64-msvc" "1.5.0" - "@unrs/resolver-binding-win32-ia32-msvc" "1.5.0" - "@unrs/resolver-binding-win32-x64-msvc" "1.5.0" + "@unrs/resolver-binding-android-arm-eabi" "1.11.1" + "@unrs/resolver-binding-android-arm64" "1.11.1" + "@unrs/resolver-binding-darwin-arm64" "1.11.1" + "@unrs/resolver-binding-darwin-x64" "1.11.1" + "@unrs/resolver-binding-freebsd-x64" "1.11.1" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.11.1" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.11.1" + "@unrs/resolver-binding-linux-arm64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-arm64-musl" "1.11.1" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-riscv64-musl" "1.11.1" + "@unrs/resolver-binding-linux-s390x-gnu" "1.11.1" + "@unrs/resolver-binding-linux-x64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-x64-musl" "1.11.1" + "@unrs/resolver-binding-wasm32-wasi" "1.11.1" + "@unrs/resolver-binding-win32-arm64-msvc" "1.11.1" + "@unrs/resolver-binding-win32-ia32-msvc" "1.11.1" + "@unrs/resolver-binding-win32-x64-msvc" "1.11.1" unset-value@^1.0.0: version "1.0.0" @@ -22610,18 +22050,18 @@ unset-value@^1.0.0: isobject "^3.0.0" unstorage@^1.9.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.15.0.tgz#d1f23cba0901c5317d15a751a299e50fbb637674" - integrity sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg== + version "1.17.1" + resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.17.1.tgz#611519b799d6d9dbecb34364a1f8919d39732e81" + integrity sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ== dependencies: anymatch "^3.1.3" chokidar "^4.0.3" - destr "^2.0.3" - h3 "^1.15.0" + destr "^2.0.5" + h3 "^1.15.4" lru-cache "^10.4.3" - node-fetch-native "^1.6.6" + node-fetch-native "^1.6.7" ofetch "^1.4.1" - ufo "^1.5.4" + ufo "^1.6.1" untildify@^2.0.0: version "2.1.0" @@ -22640,7 +22080,7 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.1.1: +update-browserslist-db@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== @@ -22749,6 +22189,11 @@ uuid-browser@^3.1.0: resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410" integrity sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -22764,11 +22209,6 @@ uuid@^9.0.0, uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - v8-compile-cache@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -22843,9 +22283,9 @@ vfile-message@^2.0.0: unist-util-stringify-position "^2.0.0" vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + version "4.0.3" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== dependencies: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" @@ -22955,9 +22395,9 @@ watchpack@^1.7.4: watchpack-chokidar2 "^2.0.1" watchpack@^2.2.0, watchpack@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" - integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + version "2.4.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947" + integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -23090,9 +22530,9 @@ webpack-dev-server@^4.5.0: ws "^8.13.0" webpack-favicons@^1.3.8: - version "1.5.4" - resolved "https://registry.yarnpkg.com/webpack-favicons/-/webpack-favicons-1.5.4.tgz#4eca74cf184c30b0bbc450d815e76f5df00189a6" - integrity sha512-NbA7LByE68/eWoJuxitfs1/kD4VUN5wKUExjd3cLtlQDFYOUDBGo2ZuZYjEsKm7eIOPLdZKX6w8dLBXHA0Vu9w== + version "1.5.43" + resolved "https://registry.yarnpkg.com/webpack-favicons/-/webpack-favicons-1.5.43.tgz#fd80dff225b6b087e2060cfcbd9b3f485e6245ae" + integrity sha512-prAavTPgWzn8c7uc9s6XQEqywdyg5wa1jzmGbd3nn6fE1G2OXMl14PcprYB2xa/+b/Kplndd48oLbt8Nc6MwQw== dependencies: favicons "7.2.0" @@ -23135,10 +22575,10 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-sources@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" + integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== webpack-virtual-modules@^0.2.2: version "0.2.2" @@ -23182,19 +22622,21 @@ webpack@4: webpack-sources "^1.4.1" "webpack@>=4.43.0 <6.0.0", webpack@^5.75.0, webpack@^5.9.0: - version "5.99.5" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.5.tgz#86e3b3a5a03377ea5da271c929934003f5ac5dd8" - integrity sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg== + version "5.101.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346" + integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A== dependencies: "@types/eslint-scope" "^3.7.7" - "@types/estree" "^1.0.6" + "@types/estree" "^1.0.8" + "@types/json-schema" "^7.0.15" "@webassemblyjs/ast" "^1.14.1" "@webassemblyjs/wasm-edit" "^1.14.1" "@webassemblyjs/wasm-parser" "^1.14.1" - acorn "^8.14.0" + acorn "^8.15.0" + acorn-import-phases "^1.0.3" browserslist "^4.24.0" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.1" + enhanced-resolve "^5.17.3" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" @@ -23204,11 +22646,11 @@ webpack@4: loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^4.3.0" + schema-utils "^4.3.2" tapable "^2.1.1" terser-webpack-plugin "^5.3.11" watchpack "^2.4.1" - webpack-sources "^3.2.3" + webpack-sources "^3.3.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -23313,7 +22755,7 @@ which-collection@^1.0.1, which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.13, which-typed-array@^1.1.16, which-typed-array@^1.1.18: +which-typed-array@^1.1.13, which-typed-array@^1.1.16, which-typed-array@^1.1.19: version "1.1.19" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== @@ -23502,9 +22944,9 @@ ws@^7, ws@^7.4.6: integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.13.0, ws@^8.18.0, ws@^8.2.3: - version "8.18.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" - integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== x-default-browser@^0.4.0: version "0.4.0" @@ -23630,11 +23072,6 @@ yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" @@ -23659,14 +23096,14 @@ yup@^0.32.9: toposort "^2.0.2" zod@^3.24.1: - version "3.24.2" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3" - integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ== + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== zustand@^4.5.5: - version "4.5.6" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.6.tgz#6857d52af44874a79fb3408c9473f78367255c96" - integrity sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ== + version "4.5.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55" + integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw== dependencies: use-sync-external-store "^1.2.2"

+

+ Epoch Reward Calculator +

+ +
+ + Current epoch reward budget + {' '} + (NYM): + + setA(Number(e.target.value))} + /> + + + setB(Number(e.target.value))} + /> + + + setC(Number(e.target.value))} + /> + + + setD(Number(e.target.value))} + /> + + + setE(Number(e.target.value))} + /> +
+ +

+ Node epoch rewards (if active): +
+ {result} +

+