diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a29e91849..976fe5a7ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,16 +3,19 @@ name: CI Pipeline for the Backend and Frontend on: pull_request: types: [opened, review_requested, synchronize] + push: + branches: + - master jobs: backend: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: - node: ["20", "21"] + node: ["22.14.0"] flavor: ["dev", "prod"] fail-fast: false - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }} steps: @@ -66,7 +69,7 @@ jobs: cache: name: "Cache assets for builds" - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 @@ -157,13 +160,13 @@ jobs: frontend: needs: cache - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" strategy: matrix: - node: ["20", "21"] + node: ["22.14.0"] flavor: ["dev", "prod"] fail-fast: false - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }} steps: @@ -245,23 +248,13 @@ jobs: VERBOSE: 1 e2e: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: "ubuntu-latest" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" + runs-on: ubuntu-latest needs: frontend strategy: fail-fast: false matrix: - module: ["mempool", "liquid"] - include: - - module: "mempool" - spec: | - cypress/e2e/mainnet/*.spec.ts - cypress/e2e/signet/*.spec.ts - cypress/e2e/testnet4/*.spec.ts - - module: "liquid" - spec: | - cypress/e2e/liquid/liquid.spec.ts - cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + module: ["mempool", "liquid", "testnet4"] name: E2E tests for ${{ matrix.module }} steps: @@ -273,7 +266,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 cache: "npm" cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json @@ -310,19 +303,23 @@ jobs: - name: Unzip assets before building (src/resources) run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video - + + # mempool - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'mempool' }} uses: cypress-io/github-action@v5 with: tag: ${{ github.event_name }} working-directory: ${{ matrix.module }}/frontend build: npm run config:defaults:${{ matrix.module }} - start: npm run start:local-staging + start: npm run start:parameterized wait-on: "http://localhost:4200" wait-on-timeout: 120 record: true parallel: true - spec: ${{ matrix.spec }} + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts group: Tests on Chrome (${{ matrix.module }}) browser: "chrome" ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" @@ -332,9 +329,58 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + # liquid + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'liquid' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # testnet + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'testnet4' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:mempool + start: npm run start:parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/testnet4/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + CYPRESS_REROUTE_TESTNET: true + COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} validate_docker_json: - if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')" - runs-on: "ubuntu-latest" + if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'" + runs-on: ubuntu-latest name: Validate generated backend Docker JSON steps: @@ -359,4 +405,4 @@ jobs: - name: Validate JSON syntax run: | cat mempool-config.json | jq - working-directory: docker/docker/backend + working-directory: docker/docker/backend \ No newline at end of file diff --git a/.github/workflows/docker_update_latest_tag.yml b/.github/workflows/docker_update_latest_tag.yml new file mode 100644 index 0000000000..5d21697d5b --- /dev/null +++ b/.github/workflows/docker_update_latest_tag.yml @@ -0,0 +1,181 @@ +name: Docker - Update latest tag + +on: + workflow_dispatch: + inputs: + tag: + description: 'The Docker image tag to pull' + required: true + type: string + +jobs: + retag-and-push: + strategy: + matrix: + service: + - frontend + - backend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + id: buildx + with: + install: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Get source image manifest and SHAs + id: source-manifest + run: | + set -e + echo "Fetching source manifest..." + MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }}) + if [ -z "$MANIFEST" ]; then + echo "No manifest found. Assuming single-arch image." + exit 1 + fi + + echo "Original source manifest:" + echo "$MANIFEST" | jq . + + AMD64_SHA=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest') + ARM64_SHA=$(echo "$MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest') + + if [ -z "$AMD64_SHA" ] || [ -z "$ARM64_SHA" ]; then + echo "Source image is not multi-arch (missing amd64 or arm64)" + exit 1 + fi + + echo "Source amd64 manifest digest: $AMD64_SHA" + echo "Source arm64 manifest digest: $ARM64_SHA" + + echo "amd64_sha=$AMD64_SHA" >> $GITHUB_OUTPUT + echo "arm64_sha=$ARM64_SHA" >> $GITHUB_OUTPUT + + - name: Pull and retag architecture-specific images + run: | + set -e + + docker buildx inspect --bootstrap + + # Remove any existing local images to avoid cache interference + echo "Removing existing local images if they exist..." + docker image rm ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }} || true + + # Pull amd64 image by digest + echo "Pulling amd64 image by digest..." + docker pull --platform linux/amd64 ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} + PULLED_AMD64_MANIFEST_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + PULLED_AMD64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} --format '{{.Id}}') + echo "Pulled amd64 manifest digest: $PULLED_AMD64_MANIFEST_DIGEST" + echo "Pulled amd64 image ID (sha256): $PULLED_AMD64_IMAGE_ID" + + # Pull arm64 image by digest + echo "Pulling arm64 image by digest..." + docker pull --platform linux/arm64 ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} + PULLED_ARM64_MANIFEST_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + PULLED_ARM64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} --format '{{.Id}}') + echo "Pulled arm64 manifest digest: $PULLED_ARM64_MANIFEST_DIGEST" + echo "Pulled arm64 image ID (sha256): $PULLED_ARM64_IMAGE_ID" + + # Tag the images + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 + + # Verify tagged images + TAGGED_AMD64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 --format '{{.Id}}') + TAGGED_ARM64_IMAGE_ID=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 --format '{{.Id}}') + echo "Tagged amd64 image ID (sha256): $TAGGED_AMD64_IMAGE_ID" + echo "Tagged arm64 image ID (sha256): $TAGGED_ARM64_IMAGE_ID" + + - name: Push architecture-specific images + run: | + set -e + + echo "Pushing amd64 image..." + docker push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 + PUSHED_AMD64_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + echo "Pushed amd64 manifest digest (local): $PUSHED_AMD64_DIGEST" + + # Fetch manifest from registry after push + echo "Fetching pushed amd64 manifest from registry..." + PUSHED_AMD64_REGISTRY_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64) + PUSHED_AMD64_REGISTRY_DIGEST=$(echo "$PUSHED_AMD64_REGISTRY_MANIFEST" | jq -r '.config.digest') + echo "Pushed amd64 manifest digest (registry): $PUSHED_AMD64_REGISTRY_DIGEST" + + echo "Pushing arm64 image..." + docker push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 + PUSHED_ARM64_DIGEST=$(docker inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 --format '{{index .RepoDigests 0}}' | cut -d@ -f2) + echo "Pushed arm64 manifest digest (local): $PUSHED_ARM64_DIGEST" + + # Fetch manifest from registry after push + echo "Fetching pushed arm64 manifest from registry..." + PUSHED_ARM64_REGISTRY_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64) + PUSHED_ARM64_REGISTRY_DIGEST=$(echo "$PUSHED_ARM64_REGISTRY_MANIFEST" | jq -r '.config.digest') + echo "Pushed arm64 manifest digest (registry): $PUSHED_ARM64_REGISTRY_DIGEST" + + - name: Create and push multi-arch manifest with original digests + run: | + set -e + + echo "Creating multi-arch manifest with original digests..." + docker manifest create ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest \ + ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.amd64_sha }} \ + ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}@${{ steps.source-manifest.outputs.arm64_sha }} + + echo "Pushing multi-arch manifest..." + docker manifest push ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest + + - name: Clean up intermediate tags + if: success() + run: | + docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-amd64 || true + docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest-arm64 || true + docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:${{ github.event.inputs.tag }} || true + + - name: Verify final manifest + run: | + set -e + echo "Fetching final generated manifest..." + FINAL_MANIFEST=$(docker manifest inspect ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest) + echo "Generated final manifest:" + echo "$FINAL_MANIFEST" | jq . + + FINAL_AMD64_SHA=$(echo "$FINAL_MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest') + FINAL_ARM64_SHA=$(echo "$FINAL_MANIFEST" | jq -r '.manifests[] | select(.platform.architecture=="arm64" and .platform.os=="linux") | .digest') + + echo "Final amd64 manifest digest: $FINAL_AMD64_SHA" + echo "Final arm64 manifest digest: $FINAL_ARM64_SHA" + + # Compare all digests + echo "Comparing digests..." + echo "Source amd64 digest: ${{ steps.source-manifest.outputs.amd64_sha }}" + echo "Pulled amd64 manifest digest: $PULLED_AMD64_MANIFEST_DIGEST" + echo "Pushed amd64 manifest digest (local): $PUSHED_AMD64_DIGEST" + echo "Pushed amd64 manifest digest (registry): $PUSHED_AMD64_REGISTRY_DIGEST" + echo "Final amd64 digest: $FINAL_AMD64_SHA" + echo "Source arm64 digest: ${{ steps.source-manifest.outputs.arm64_sha }}" + echo "Pulled arm64 manifest digest: $PULLED_ARM64_MANIFEST_DIGEST" + echo "Pushed arm64 manifest digest (local): $PUSHED_ARM64_DIGEST" + echo "Pushed arm64 manifest digest (registry): $PUSHED_ARM64_REGISTRY_DIGEST" + echo "Final arm64 digest: $FINAL_ARM64_SHA" + + if [ "$FINAL_AMD64_SHA" != "${{ steps.source-manifest.outputs.amd64_sha }}" ] || [ "$FINAL_ARM64_SHA" != "${{ steps.source-manifest.outputs.arm64_sha }}" ]; then + echo "Error: Final manifest SHAs do not match source SHAs" + exit 1 + fi + + echo "Successfully created multi-arch ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest from ${{ github.event.inputs.tag }}" diff --git a/.github/workflows/e2e_parameterized.yml b/.github/workflows/e2e_parameterized.yml new file mode 100644 index 0000000000..8b07ffe82e --- /dev/null +++ b/.github/workflows/e2e_parameterized.yml @@ -0,0 +1,273 @@ +name: 'Parameterized e2e tests' + +on: + workflow_dispatch: + inputs: + ref: + description: 'Branch name or Pull Request number (e.g., master or 6102)' + required: true + default: 'master' + type: string + mempool_hostname: + description: 'Mempool Hostname' + required: true + default: 'mempool.space' + type: string + liquid_hostname: + description: 'Liquid Hostname' + required: true + default: 'liquid.network' + type: string + +jobs: + cache: + name: "Cache assets for builds" + runs-on: ubuntu-latest + steps: + - name: Determine checkout ref + id: determine-ref + run: | + REF_INPUT="${{ github.event.inputs.ref }}" + if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then + echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT + else + echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT + fi + + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ steps.determine-ref.outputs.ref }} + path: assets + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 22.14.0 + registry-url: "https://registry.npmjs.org" + + - name: Install (Prod dependencies only) + run: npm ci --omit=dev --omit=optional + working-directory: assets/frontend + + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Unzip assets before building (src/resources) + continue-on-error: true + run: unzip -o mining-pool-assets.zip -d assets/frontend/src/resources/mining-pools + + - name: Unzip assets before building (src/resources) + continue-on-error: true + run: unzip -o promo-video-assets.zip -d assets/frontend/src/resources/promo-video + + # - name: Unzip assets before building (dist) + # continue-on-error: true + # run: unzip assets.zip -d assets/frontend/dist/mempool/browser/resources + + - name: Sync-assets + run: npm run sync-assets-dev + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MEMPOOL_CDN: 1 + VERBOSE: 1 + working-directory: assets/frontend + + - name: Zip mining-pool assets + run: zip -jrq mining-pool-assets.zip assets/frontend/src/resources/mining-pools/* + + - name: Zip promo-video assets + run: zip -jrq promo-video-assets.zip assets/frontend/src/resources/promo-video/* + + - name: Upload mining pool assets as artifact + uses: actions/upload-artifact@v4 + with: + name: mining-pool-assets + path: mining-pool-assets.zip + + - name: Upload promo video assets as artifact + uses: actions/upload-artifact@v4 + with: + name: promo-video-assets + path: promo-video-assets.zip + + - name: Save mining pool assets cache + id: cache-mining-pool-save + uses: actions/cache/save@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Save promo video assets cache + id: cache-promo-video-save + uses: actions/cache/save@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + e2e: + runs-on: ubuntu-latest + needs: cache + strategy: + fail-fast: false + matrix: + module: ["mempool", "liquid", "testnet4"] + + name: E2E tests for ${{ matrix.module }} + steps: + - name: Determine checkout ref + id: determine-ref + run: | + REF_INPUT="${{ github.event.inputs.ref }}" + if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then + echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT + else + echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT + fi + + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ steps.determine-ref.outputs.ref }} + path: ${{ matrix.module }} + + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: 22.14.0 + cache: "npm" + cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json + + - name: Restore cached mining pool assets + continue-on-error: true + id: cache-mining-pool-restore + uses: actions/cache/restore@v4 + with: + path: | + mining-pool-assets.zip + key: mining-pool-assets-cache + + - name: Restore cached promo video assets + continue-on-error: true + id: cache-promo-video-restore + uses: actions/cache/restore@v4 + with: + path: | + promo-video-assets.zip + key: promo-video-assets-cache + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: mining-pool-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o mining-pool-assets.zip -d ${{ matrix.module }}/frontend/src/resources/mining-pools + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: promo-video-assets + + - name: Unzip assets before building (src/resources) + run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video + + # mempool + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'mempool' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/mainnet/*.spec.ts + cypress/e2e/signet/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} + LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # liquid + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'liquid' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:${{ matrix.module }} + start: npm run start:parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/liquid/liquid.spec.ts + cypress/e2e/liquidtestnet/liquidtestnet.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} + LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} + + # testnet + - name: Chrome browser tests (${{ matrix.module }}) + if: ${{ matrix.module == 'testnet4' }} + uses: cypress-io/github-action@v5 + with: + tag: ${{ github.event_name }} + working-directory: ${{ matrix.module }}/frontend + build: npm run config:defaults:mempool + start: npm run start:parameterized + wait-on: "http://localhost:4200" + wait-on-timeout: 120 + record: true + parallel: true + spec: | + cypress/e2e/testnet4/*.spec.ts + group: Tests on Chrome (${{ matrix.module }}) + browser: "chrome" + ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}" + env: + COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }} + LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }} + MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }} + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} diff --git a/.github/workflows/get_backend_block_height.yml b/.github/workflows/get_backend_block_height.yml index 52f3b038cd..ae30188e54 100644 --- a/.github/workflows/get_backend_block_height.yml +++ b/.github/workflows/get_backend_block_height.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] jobs: print-backend-sha: - runs-on: 'ubuntu-latest' + runs-on: ubuntu-latest name: Get block height steps: - name: Checkout diff --git a/.github/workflows/get_backend_hash.yml b/.github/workflows/get_backend_hash.yml index 57950dee47..0e31735b66 100644 --- a/.github/workflows/get_backend_hash.yml +++ b/.github/workflows/get_backend_hash.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] jobs: print-backend-sha: - runs-on: 'ubuntu-latest' + runs-on: ubuntu-latest name: Print backend hashes steps: - name: Checkout diff --git a/.github/workflows/get_image_digest.yml b/.github/workflows/get_image_digest.yml index 7414eeb081..18ad39fdee 100644 --- a/.github/workflows/get_image_digest.yml +++ b/.github/workflows/get_image_digest.yml @@ -10,7 +10,7 @@ on: type: string jobs: print-images-sha: - runs-on: 'ubuntu-latest' + runs-on: ubuntu-latest name: Print digest for images steps: - name: Checkout diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 634a27ab94..4a6882b811 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -2,7 +2,7 @@ name: Docker build on tag env: DOCKER_CLI_EXPERIMENTAL: enabled TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$" - DOCKER_BUILDKIT: 0 + DOCKER_BUILDKIT: 1 # Enable BuildKit for better performance COMPOSE_DOCKER_CLI_BUILD: 0 on: @@ -25,13 +25,12 @@ jobs: timeout-minutes: 120 name: Build and push to DockerHub steps: - # Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1 - name: Replace the current swap file shell: bash run: | - sudo swapoff /mnt/swapfile - sudo rm -v /mnt/swapfile - sudo fallocate -l 13G /mnt/swapfile + sudo swapoff /mnt/swapfile || true + sudo rm -f /mnt/swapfile + sudo fallocate -l 16G /mnt/swapfile sudo chmod 600 /mnt/swapfile sudo mkswap /mnt/swapfile sudo swapon /mnt/swapfile @@ -50,7 +49,7 @@ jobs: echo "Directory '/var/lib/docker' not found" exit 1 fi - sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker + sudo mount -t tmpfs -o size=12G tmpfs /var/lib/docker sudo systemctl restart docker sudo df -h | grep docker @@ -75,10 +74,16 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 id: qemu - name: Setup Docker buildx action uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + driver-opts: | + network=host id: buildx - name: Available platforms @@ -89,19 +94,20 @@ jobs: id: cache with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-${{ matrix.service }}- - name: Run Docker buildx for ${{ matrix.service }} against tag run: | docker buildx build \ --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \ --platform linux/amd64,linux/arm64 \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \ --build-context rustgbt=./rust \ --build-context backend=./backend \ - --output "type=registry" ./${{ matrix.service }}/ \ - --build-arg commitHash=$SHORT_SHA + --output "type=registry,push=true" \ + --build-arg commitHash=$SHORT_SHA \ + ./${{ matrix.service }}/ diff --git a/.nvmrc b/.nvmrc index a9b234d515..53d1c14db3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.8.0 +v22 diff --git a/LICENSE b/LICENSE index 1c368c00a6..cdffcd79bb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ The Mempool Open Source Project® -Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders +Copyright (c) 2019-2025 Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free @@ -10,8 +10,8 @@ However, this copyright license does not include an implied right or license to use any trademarks, service marks, logos, or trade names of Mempool Space K.K. or any other contributor to The Mempool Open Source Project. -The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, -Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full +The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, +Mempool Wallet™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, diff --git a/backend/README.md b/backend/README.md index 6ae4ae3e2b..cecc07bc9e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec) #### Build -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer_ _The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._ diff --git a/backend/jest.config.ts b/backend/jest.config.ts index 14f932f986..43246c5186 100644 --- a/backend/jest.config.ts +++ b/backend/jest.config.ts @@ -7,7 +7,7 @@ const config: Config.InitialOptions = { automock: false, collectCoverage: true, collectCoverageFrom: ["./src/**/**.ts"], - coverageProvider: "babel", + coverageProvider: "v8", coverageThreshold: { global: { lines: 1 diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 4650c1e645..c2715153bb 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -27,8 +27,9 @@ "AUTOMATIC_POOLS_UPDATE": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "POOLS_UPDATE_DELAY": 604800, "AUDIT": false, - "RUST_GBT": false, + "RUST_GBT": true, "LIMIT_GBT": false, "CPFP_INDEXING": false, "DISK_CACHE_BLOCK_INTERVAL": 6, @@ -45,7 +46,8 @@ "PASSWORD": "mempool", "TIMEOUT": 60000, "COOKIE": false, - "COOKIE_PATH": "/path/to/bitcoin/.cookie" + "COOKIE_PATH": "/path/to/bitcoin/.cookie", + "DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log" }, "ELECTRUM": { "HOST": "127.0.0.1", @@ -153,6 +155,10 @@ "API": "https://mempool.space/api/v1/services", "ACCELERATIONS": false }, + "STRATUM": { + "ENABLED": false, + "API": "http://localhost:1234" + }, "FIAT_PRICE": { "ENABLED": true, "PAID": false, diff --git a/backend/package-lock.json b/backend/package-lock.json index 66e8d19a25..646f2ecfca 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,23 +1,23 @@ { "name": "mempool-backend", - "version": "3.0.0-rc1", + "version": "3.3-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-backend", - "version": "3.0.0-rc1", + "version": "3.3-dev", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.2", + "axios": "1.10.0", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.11.0", + "mysql2": "~3.14.1", "redis": "^4.7.0", "rust-gbt": "file:./rust-gbt", "socks-proxy-agent": "~7.0.0", @@ -25,8 +25,6 @@ "ws": "~8.18.0" }, "devDependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/core": "^7.25.2", "@types/compression": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.17", @@ -2277,9 +2275,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2454,9 +2452,9 @@ "dev": true }, "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" }, "node_modules/bech32": { "version": "2.0.0", @@ -2488,9 +2486,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2500,7 +2498,7 @@ "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" @@ -2647,6 +2645,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2825,9 +2835,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -2999,6 +3009,19 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3029,9 +3052,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -3046,12 +3069,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -3064,6 +3084,31 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3459,36 +3504,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "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.10", "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", @@ -3601,12 +3646,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "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", @@ -3685,12 +3730,14 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "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" }, "engines": { @@ -3776,15 +3823,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "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" }, "engines": { "node": ">= 0.4" @@ -3802,6 +3854,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3876,11 +3940,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3930,10 +3994,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -3941,10 +4005,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -5998,6 +6065,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -6028,6 +6110,14 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/maxmind": { "version": "4.3.11", "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz", @@ -6050,9 +6140,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6156,16 +6249,16 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mysql2": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", - "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", - "lru-cache": "^8.0.0", + "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" @@ -6185,14 +6278,6 @@ "node": ">=0.10.0" } }, - "node_modules/mysql2/node_modules/lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", - "engines": { - "node": ">=16.14" - } - }, "node_modules/named-placeholders": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", @@ -6266,9 +6351,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6436,9 +6524,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6646,11 +6734,11 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6871,9 +6959,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -6906,6 +6994,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6917,14 +7013,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "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" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -9438,9 +9534,9 @@ "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" }, "axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -9575,9 +9671,9 @@ "dev": true }, "base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" }, "bech32": { "version": "2.0.0", @@ -9603,9 +9699,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -9615,7 +9711,7 @@ "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" @@ -9725,6 +9821,15 @@ "set-function-length": "^1.2.1" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -9849,9 +9954,9 @@ "dev": true }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -9972,6 +10077,16 @@ "esutils": "^2.0.2" } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9996,9 +10111,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "error-ex": { "version": "1.3.2", @@ -10010,18 +10125,34 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -10303,36 +10434,36 @@ } }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "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.10", "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", @@ -10434,12 +10565,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "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", @@ -10494,12 +10625,14 @@ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -10557,15 +10690,20 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "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-package-type": { @@ -10574,6 +10712,15 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -10624,12 +10771,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -10666,15 +10810,18 @@ "es-define-property": "^1.0.0" } }, - "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" - }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } }, "hasown": { "version": "2.0.2", @@ -12197,6 +12344,11 @@ "yallist": "^3.0.2" } }, + "lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -12221,6 +12373,11 @@ "tmpl": "1.0.5" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "maxmind": { "version": "4.3.11", "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz", @@ -12236,9 +12393,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -12311,16 +12468,16 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mysql2": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", - "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", "requires": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", - "lru-cache": "^8.0.0", + "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" @@ -12333,11 +12490,6 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } - }, - "lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" } } }, @@ -12401,9 +12553,9 @@ } }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -12520,9 +12672,9 @@ "dev": true }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -12664,11 +12816,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { @@ -12802,9 +12954,9 @@ "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -12836,6 +12988,11 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12849,14 +13006,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { diff --git a/backend/package.json b/backend/package.json index 959516ac83..8174b439eb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.0.0-rc1", + "version": "3.3-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -39,15 +39,14 @@ "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" }, "dependencies": { - "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.2", + "axios": "1.10.0", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.1", "maxmind": "~4.3.11", - "mysql2": "~3.11.0", + "mysql2": "~3.14.1", "rust-gbt": "file:./rust-gbt", "redis": "^4.7.0", "socks-proxy-agent": "~7.0.0", @@ -55,8 +54,6 @@ "ws": "~8.18.0" }, "devDependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/core": "^7.25.2", "@types/compression": "^1.7.2", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.17", diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index 3796b7f22e..0ca5654a5a 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -28,6 +28,7 @@ "INDEXING_BLOCKS_AMOUNT": 14, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", + "POOLS_UPDATE_DELAY": 604800, "AUDIT": true, "RUST_GBT": false, "LIMIT_GBT": false, @@ -46,7 +47,8 @@ "PASSWORD": "__CORE_RPC_PASSWORD__", "TIMEOUT": 1000, "COOKIE": false, - "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" + "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", + "DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__" }, "ELECTRUM": { "HOST": "__ELECTRUM_HOST__", @@ -149,5 +151,9 @@ "ENABLED": true, "PAID": false, "API_KEY": "__MEMPOOL_CURRENCY_API_KEY__" + }, + "STRATUM": { + "ENABLED": false, + "API": "http://localhost:1234" } } diff --git a/backend/src/__tests__/api/common.ts b/backend/src/__tests__/api/common.ts index 74a7db88f7..14ae3c78bb 100644 --- a/backend/src/__tests__/api/common.ts +++ b/backend/src/__tests__/api/common.ts @@ -1,5 +1,5 @@ import { Common } from '../../api/common'; -import { MempoolTransactionExtended } from '../../mempool.interfaces'; +import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; const randomTransactions = require('./test-data/transactions-random.json'); const replacedTransactions = require('./test-data/transactions-replaced.json'); @@ -10,14 +10,14 @@ describe('Common', () => { describe('RBF', () => { const newTransactions = rbfTransactions.concat(randomTransactions); test('should detect RBF transactions with fast method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); }); test('should detect RBF transactions with scalable method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); diff --git a/backend/src/__tests__/api/fee-api.ts b/backend/src/__tests__/api/fee-api.ts new file mode 100644 index 0000000000..64f80bca94 --- /dev/null +++ b/backend/src/__tests__/api/fee-api.ts @@ -0,0 +1,55 @@ +import feeApi from '../../api/fee-api'; +import { IBitcoinApi } from '../../api/bitcoin/bitcoin-api.interface'; +const feeMempoolBlocks = require('./test-data/fee-mempool-blocks.json'); + + +const subSatMempoolInfo: IBitcoinApi.MempoolInfo = { + mempoolminfee: 0.000001, // 0.1 sat/vbyte + loaded: true, + size: 100, + bytes: 10000, + usage: 10000, + total_fee: 10000, + maxmempool: 10000, + minrelaytxfee: 0.000001, // 0.1 sat/vbyte +}; + +const mempoolInfo: IBitcoinApi.MempoolInfo = { + mempoolminfee: 0.00001, + loaded: true, + size: 100, + bytes: 10000, + usage: 10000, + total_fee: 10000, + maxmempool: 10000, + minrelaytxfee: 0.00001, +}; + +describe('Fee API', () => { + test('should calculate recommended fees properly for sub-sat mempool', () => { + const fee = feeApi.calculateRecommendedFee(feeMempoolBlocks.subsat, subSatMempoolInfo); + expect(fee.fastestFee).toBe(2); + expect(fee.halfHourFee).toBe(1); + expect(fee.hourFee).toBe(1); + expect(fee.economyFee).toBe(1); + expect(fee.minimumFee).toBe(1); + }); + + test('should calculate recommended fees properly for full but low fee mempool', () => { + const fee = feeApi.calculateRecommendedFee(feeMempoolBlocks.lowfee, mempoolInfo); + expect(fee.fastestFee).toBe(2); + expect(fee.halfHourFee).toBe(2); + expect(fee.hourFee).toBe(2); + expect(fee.economyFee).toBe(2); + expect(fee.minimumFee).toBe(1); + }); + + test('should calculate recommended fees properly for empty mempool', () => { + const fee = feeApi.calculateRecommendedFee(feeMempoolBlocks.empty, mempoolInfo); + expect(fee.fastestFee).toBe(1); + expect(fee.halfHourFee).toBe(1); + expect(fee.hourFee).toBe(1); + expect(fee.economyFee).toBe(1); + expect(fee.minimumFee).toBe(1); + }); +}); \ No newline at end of file diff --git a/backend/src/__tests__/api/test-data/fee-mempool-blocks.json b/backend/src/__tests__/api/test-data/fee-mempool-blocks.json new file mode 100644 index 0000000000..f5a2efb200 --- /dev/null +++ b/backend/src/__tests__/api/test-data/fee-mempool-blocks.json @@ -0,0 +1,289 @@ +{ + "subsat": [ + { + "blockSize": 1746106, + "blockVSize": 997953.25, + "nTx": 3750, + "totalFees": 1764766, + "medianFee": 1.0023586640951265, + "feeRange": [ + 0.6082474226804123, + 0.6082474226804123, + 0.6082474226804123, + 0.6082474226804123, + 1.2076788830715532, + 3.166077738515901, + 73.61111111111111 + ] + }, + { + "blockSize": 1809871, + "blockVSize": 997963, + "nTx": 4861, + "totalFees": 588436, + "medianFee": 0.6082474226804123, + "feeRange": [ + 0.5156626506024097, + 0.5649475055559813, + 0.5670103092783505, + 0.6082474226804123, + 0.6082474226804123, + 0.6082474226804123, + 0.6082474226804123 + ] + }, + { + "blockSize": 2917870, + "blockVSize": 997821.25, + "nTx": 1221, + "totalFees": 535389, + "medianFee": 0.5200034509958588, + "feeRange": [ + 0.35051546391752575, + 0.5649475055559813, + 0.5649475055559813, + 0.5649475055559813, + 0.5649475055559813, + 0.5649475055559813, + 0.5649475055559813 + ] + }, + { + "blockSize": 3937966, + "blockVSize": 997875.25, + "nTx": 18, + "totalFees": 506205, + "medianFee": 0.5100003123470801, + "feeRange": [ + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.5099980684971892, + 0.5100003123470801, + 0.5100020185186144, + 0.5100025244589157 + ] + }, + { + "blockSize": 3917639, + "blockVSize": 997810.25, + "nTx": 62, + "totalFees": 503529, + "medianFee": 0.5099975959779666, + "feeRange": [ + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.5099971889756266, + 0.5100002265056967 + ] + }, + { + "blockSize": 3694897, + "blockVSize": 997893.25, + "nTx": 43, + "totalFees": 503307, + "medianFee": 0.5099952023028946, + "feeRange": [ + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.3704911425797351, + 0.5099952023028946, + 0.5099967157103613 + ] + }, + { + "blockSize": 2486759, + "blockVSize": 997857.5, + "nTx": 498, + "totalFees": 386911, + "medianFee": 0.36001120750903104, + "feeRange": [ + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.5000054632270189 + ] + }, + { + "blockSize": 6681226, + "blockVSize": 3385616.5, + "nTx": 10222, + "totalFees": 1197838, + "medianFee": 0.35051546391752575, + "feeRange": [ + 0.21576460085542437, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.35051546391752575, + 0.3599967979827291, + 0.3600004002561639, + 0.3600040026016911, + 0.3600040026016911, + 0.3600040026016911 + ] + } + ], + + "empty": [{ + "blockSize": 10000, + "blockVSize": 10000, + "nTx": 2000, + "totalFees": 1197838, + "medianFee": 1, + "feeRange": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + } + ], + + "lowfee": [ + { + "blockSize": 1746106, + "blockVSize": 997953.25, + "nTx": 3750, + "totalFees": 1764766, + "medianFee": 1.0023586640951265, + "feeRange": [ + 1.6082474226804123, + 1.6082474226804123, + 1.6082474226804123, + 1.6082474226804123, + 1.2076788830715532, + 3.166077738515901, + 73.61111111111111 + ] + }, + { + "blockSize": 1809871, + "blockVSize": 997963, + "nTx": 4861, + "totalFees": 588436, + "medianFee": 1.6082474226804123, + "feeRange": [ + 1.5156626506024097, + 1.5649475055559813, + 1.5670103092783505, + 1.6082474226804123, + 1.6082474226804123, + 1.6082474226804123, + 1.6082474226804123 + ] + }, + { + "blockSize": 2917870, + "blockVSize": 997821.25, + "nTx": 1221, + "totalFees": 535389, + "medianFee": 1.5200034509958588, + "feeRange": [ + 1.35051546391752575, + 1.5649475055559813, + 1.5649475055559813, + 1.5649475055559813, + 1.5649475055559813, + 1.5649475055559813, + 1.5649475055559813 + ] + }, + { + "blockSize": 3937966, + "blockVSize": 997875.25, + "nTx": 18, + "totalFees": 506205, + "medianFee": 1.5100003123470801, + "feeRange": [ + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.5099980684971892, + 1.5100003123470801, + 1.5100020185186144, + 1.5100025244589157 + ] + }, + { + "blockSize": 3917639, + "blockVSize": 997811.25, + "nTx": 62, + "totalFees": 503529, + "medianFee": 1.5099975959779666, + "feeRange": [ + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.5099971889756266, + 1.5100002265056967 + ] + }, + { + "blockSize": 3694897, + "blockVSize": 997893.25, + "nTx": 43, + "totalFees": 503307, + "medianFee": 1.5099952023028946, + "feeRange": [ + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.3704911425797351, + 1.5099952023028946, + 1.5099967157103613 + ] + }, + { + "blockSize": 2486759, + "blockVSize": 997857.5, + "nTx": 498, + "totalFees": 386911, + "medianFee": 1.36001120750903104, + "feeRange": [ + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.5000054632270189 + ] + }, + { + "blockSize": 6681226, + "blockVSize": 3385616.5, + "nTx": 10222, + "totalFees": 1197838, + "medianFee": 1.35051546391752575, + "feeRange": [ + 1.21576460085542437, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.35051546391752575, + 1.3599967979827291, + 1.3600004002561639, + 1.3600040026016911, + 1.3600040026016911, + 1.3600040026016911 + ] + } + ] +} \ No newline at end of file diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 050213143c..e0437941f9 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => { STDOUT_LOG_MIN_PRIORITY: 'debug', POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', + POOLS_UPDATE_DELAY: 604800, AUDIT: false, - RUST_GBT: false, + RUST_GBT: true, LIMIT_GBT: false, CPFP_INDEXING: false, MAX_BLOCKS_BULK_QUERY: 0, @@ -73,7 +74,8 @@ describe('Mempool Backend Config', () => { PASSWORD: 'mempool', TIMEOUT: 60000, COOKIE: false, - COOKIE_PATH: '/bitcoin/.cookie' + COOKIE_PATH: '/bitcoin/.cookie', + DEBUG_LOG_PATH: '', }); expect(config.SECOND_CORE_RPC).toStrictEqual({ @@ -157,6 +159,11 @@ describe('Mempool Backend Config', () => { PAID: false, API_KEY: '', }); + + expect(config.STRATUM).toStrictEqual({ + ENABLED: false, + API: 'http://localhost:1234', + }); }); }); diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index eea96af69c..7b90c516ec 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -2,6 +2,7 @@ import config from '../config'; import logger from '../logger'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import rbfCache from './rbf-cache'; +import transactionUtils from './transaction-utils'; const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners @@ -15,7 +16,8 @@ class Audit { const matches: string[] = []; // present in both mined block and template const added: string[] = []; // present in mined block, not in template const unseen: string[] = []; // present in the mined block, not in our mempool - const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone + let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone + let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const accelerated: string[] = []; // prioritized by the mempool accelerator @@ -53,7 +55,7 @@ class Audit { } else if (mempool[txid]?.lastBoosted != null && (now - (mempool[txid]?.lastBoosted || 0)) <= PROPAGATION_MARGIN) { // tx was recently cpfp'd, miner may not have the latest effective rate fresh.push(txid); - } else { + } else if (mempool[txid].effectiveFeePerVsize >= 1) { // transactions paying < 1 sat/vbyte are never considered censored isCensored[txid] = true; } displacedWeight += mempool[txid]?.weight || 0; @@ -133,23 +135,7 @@ class Audit { totalWeight += tx.weight; } - - // identify "prioritized" transactions - let lastEffectiveRate = 0; - // Iterate over the mined template from bottom to top (excluding the coinbase) - // Transactions should appear in ascending order of mining priority. - for (let i = transactions.length - 1; i > 0; i--) { - const blockTx = transactions[i]; - // If a tx has a lower in-band effective fee rate than the previous tx, - // it must have been prioritized out-of-band (in order to have a higher mining priority) - // so exclude from the analysis. - if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) { - prioritized.push(blockTx.txid); - // accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference - } else if (!isAccelerated[blockTx.txid]) { - lastEffectiveRate = blockTx.effectiveFeePerVsize || 0; - } - } + ({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize')); // transactions missing from near the end of our template are probably not being censored let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index a08f432380..e246f249d4 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,4 +1,4 @@ -import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; +import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { @@ -23,12 +23,14 @@ export interface AbstractBitcoinApi { $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise; $sendRawTransaction(rawTransaction: string): Promise; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise; + $submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise; $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; $getBatchedOutspends(txId: string[]): Promise; $getBatchedOutspendsInternal(txId: string[]): Promise; $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise; $getCoinbaseTx(blockhash: string): Promise; + $getAddressTransactionSummary(address: string): Promise; startHealthChecks(): void; getHealthStatus(): HealthCheckHost[]; diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index 6e8583f6fd..5d8371d27e 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult { }, ['reject-reason']?: string, } + +export interface SubmitPackageResult { + package_msg: string; + "tx-results": { [wtxid: string]: TxResult }; + "replaced-transactions"?: string[]; +} + +export interface TxResult { + txid: string; + "other-wtxid"?: string; + vsize?: number; + fees?: { + base: number; + "effective-feerate"?: number; + "effective-includes"?: string[]; + }; + error?: string; +} diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 3e1fe21084..86c1ffdba0 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -1,11 +1,12 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; -import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; +import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; import blocks from '../blocks'; import mempool from '../mempool'; import { TransactionExtended } from '../../mempool.interfaces'; import transactionUtils from '../transaction-utils'; +import { Common } from '../common'; class BitcoinApi implements AbstractBitcoinApi { private rawMempoolCache: IBitcoinApi.RawMempool | null = null; @@ -196,6 +197,10 @@ class BitcoinApi implements AbstractBitcoinApi { } } + $submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise { + return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined); + } + async $getOutspend(txId: string, vout: number): Promise { const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); return { @@ -251,6 +256,10 @@ class BitcoinApi implements AbstractBitcoinApi { return this.$getRawTransaction(txids[0]); } + async $getAddressTransactionSummary(address: string): Promise { + throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.'); + } + $getEstimatedHashrate(blockHeight: number): Promise { // 120 is the default block span in Core return this.bitcoindClient.getNetworkHashPs(120, blockHeight); @@ -285,7 +294,7 @@ class BitcoinApi implements AbstractBitcoinApi { is_coinbase: !!vin.coinbase, prevout: null, scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '', - scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '', + scriptsig_asm: vin.scriptSig ? transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) : (vin.coinbase ? transactionUtils.convertScriptSigAsm(vin.coinbase) : ''), sequence: vin.sequence, txid: vin.txid || '', vout: vin.vout || 0, @@ -323,6 +332,7 @@ class BitcoinApi implements AbstractBitcoinApi { 'witness_v1_taproot': 'v1_p2tr', 'nonstandard': 'nonstandard', 'multisig': 'multisig', + 'anchor': 'anchor', 'nulldata': 'op_return' }; @@ -351,6 +361,7 @@ class BitcoinApi implements AbstractBitcoinApi { } protected async $addPrevouts(transaction: TransactionExtended): Promise { + let addedPrevouts = false; for (const vin of transaction.vin) { if (vin.prevout) { continue; @@ -358,6 +369,12 @@ class BitcoinApi implements AbstractBitcoinApi { const innerTx = await this.$getRawTransaction(vin.txid, false, false); vin.prevout = innerTx.vout[vin.vout]; transactionUtils.addInnerScriptsToVin(vin); + addedPrevouts = true; + } + if (addedPrevouts) { + // re-calculate transaction flags now that we have full prevout data + transaction.flags = undefined; // clear existing flags to force full classification + transaction.flags = Common.getTransactionFlags(transaction, transaction.status?.block_height ?? blocks.getCurrentBlockHeight()); } return transaction; } diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts index 7933dc17b1..7e1dcea749 100644 --- a/backend/src/api/bitcoin/bitcoin-core.routes.ts +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -1,6 +1,11 @@ import { Application, NextFunction, Request, Response } from 'express'; import logger from '../../logger'; import bitcoinClient from './bitcoin-client'; +import config from '../../config'; + +const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i; +const TXID_REGEX = /^[a-f0-9]{64}$/i; +const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i; /** * Define a set of routes used by the accelerator server @@ -9,26 +14,26 @@ import bitcoinClient from './bitcoin-client'; class BitcoinBackendRoutes { private static tag = 'BitcoinBackendRoutes'; - public initRoutes(app: Application) { + public initRoutes(app: Application): void { app - .get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) - .post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) - .get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) - .post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) - .post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) - .get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) - .get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) - .get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) - .get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) + .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) ; } /** * Disable caching for bitcoin core routes - * - * @param req - * @param res - * @param next + * + * @param req + * @param res + * @param next */ private disableCache(req: Request, res: Response, next: NextFunction): void { res.setHeader('Pragma', 'no-cache'); @@ -39,16 +44,16 @@ class BitcoinBackendRoutes { /** * Exeption handler to return proper details to the accelerator server - * - * @param e - * @param fnName - * @param res + * + * @param e + * @param fnName + * @param res */ private static handleException(e: any, fnName: string, res: Response): void { if (typeof(e.code) === 'number') { - res.status(400).send(JSON.stringify(e, ['code', 'message'])); - } else { - const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`; + res.status(400).send(JSON.stringify(e, ['code'])); + } else { + const err = `unknown exception in ${fnName}`; logger.err(err, BitcoinBackendRoutes.tag); res.status(500).send(err); } @@ -57,13 +62,13 @@ class BitcoinBackendRoutes { private async $getMempoolEntry(req: Request, res: Response): Promise { const txid = req.query.txid; try { - if (typeof(txid) !== 'string' || txid.length !== 64) { - res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { + res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); return; } const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); if (!mempoolEntry) { - res.status(404).send(`no mempool entry found for txid ${txid}`); + res.status(404).send(); return; } res.status(200).send(mempoolEntry); @@ -75,13 +80,13 @@ class BitcoinBackendRoutes { private async $decodeRawTransaction(req: Request, res: Response): Promise { const rawTx = req.body.rawTx; try { - if (typeof(rawTx) !== 'string') { - res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { + res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); return; } const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); if (!decodedTx) { - res.status(400).send(`unable to decode rawTx ${rawTx}`); + res.status(400).send(`unable to decode rawTx`); return; } res.status(200).send(decodedTx); @@ -94,23 +99,23 @@ class BitcoinBackendRoutes { const txid = req.query.txid; const verbose = req.query.verbose; try { - if (typeof(txid) !== 'string' || txid.length !== 64) { - res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { + res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); return; } if (typeof(verbose) !== 'string') { - res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); + res.status(400).send(`invalid param verbose. must be a string representing an integer`); return; } const verboseNumber = parseInt(verbose, 10); if (typeof(verboseNumber) !== 'number') { - res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); + res.status(400).send(`invalid param verbose. must be a valid integer`); return; } const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); if (!decodedTx) { - res.status(400).send(`unable to get raw transaction for txid ${txid}`); + res.status(400).send(`unable to get raw transaction`); return; } res.status(200).send(decodedTx); @@ -122,13 +127,13 @@ class BitcoinBackendRoutes { private async $sendRawTransaction(req: Request, res: Response): Promise { const rawTx = req.body.rawTx; try { - if (typeof(rawTx) !== 'string') { - res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) { + res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`); return; } const txHex = await bitcoinClient.sendRawTransaction(rawTx); if (!txHex) { - res.status(400).send(`unable to send rawTx ${rawTx}`); + res.status(400).send(`unable to send rawTx`); return; } res.status(200).send(txHex); @@ -140,13 +145,13 @@ class BitcoinBackendRoutes { private async $testMempoolAccept(req: Request, res: Response): Promise { const rawTxs = req.body.rawTxs; try { - if (typeof(rawTxs) !== 'object') { - res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`); + if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) { + res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`); return; } const txHex = await bitcoinClient.testMempoolAccept(rawTxs); if (typeof(txHex) !== 'object' || txHex.length === 0) { - res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`); + res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`); return; } res.status(200).send(txHex); @@ -159,18 +164,18 @@ class BitcoinBackendRoutes { const txid = req.query.txid; const verbose = req.query.verbose; try { - if (typeof(txid) !== 'string' || txid.length !== 64) { - res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) { + res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`); return; } if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { - res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`); + res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`); return; } - + const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); if (!ancestors) { - res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); + res.status(400).send(`unable to get mempool ancestors`); return; } res.status(200).send(ancestors); @@ -183,23 +188,23 @@ class BitcoinBackendRoutes { const blockHash = req.query.hash; const verbosity = req.query.verbosity; try { - if (typeof(blockHash) !== 'string' || blockHash.length !== 64) { - res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`); + if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) { + res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`); return; } if (typeof(verbosity) !== 'string') { - res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`); + res.status(400).send(`invalid param verbosity. must be a string representing an integer`); return; } const verbosityNumber = parseInt(verbosity, 10); if (typeof(verbosityNumber) !== 'number') { - res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`); + res.status(400).send(`invalid param verbosity. must be a valid integer`); return; } const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); if (!block) { - res.status(400).send(`unable to get block for block hash ${blockHash}`); + res.status(400).send(`unable to get block`); return; } res.status(200).send(block); @@ -212,18 +217,18 @@ class BitcoinBackendRoutes { const blockHeight = req.query.height; try { if (typeof(blockHeight) !== 'string') { - res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`); + res.status(400).send(`invalid param blockHeight, must be a string representing an integer`); return; } const blockHeightNumber = parseInt(blockHeight, 10); if (typeof(blockHeightNumber) !== 'number') { - res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`); + res.status(400).send(`invalid param blockHeight. must be a valid integer`); return; } const block = await bitcoinClient.getBlockHash(blockHeightNumber); if (!block) { - res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`); + res.status(400).send(`unable to get block hash`); return; } res.status(200).send(block); @@ -246,4 +251,4 @@ class BitcoinBackendRoutes { } } -export default new BitcoinBackendRoutes \ No newline at end of file +export default new BitcoinBackendRoutes; \ No newline at end of file diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 6225a9c1d2..3cf2923f1f 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -20,6 +20,13 @@ import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; import { calculateMempoolTxCpfp } from '../cpfp'; +import { handleError } from '../../utils/api'; +import poolsUpdater from '../../tasks/pools-updater'; + +const TXID_REGEX = /^[a-f0-9]{64}$/i; +const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i; +const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i; +const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i; class BitcoinRoutes { public initRoutes(app: Application) { @@ -41,12 +48,21 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) + .post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts) + .post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs) + // Temporarily add txs/package endpoint for all backends until esplora supports it + .post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage) + // Internal routes + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash) ; if (config.MEMPOOL.BACKEND !== 'esplora') { @@ -86,7 +102,7 @@ class BitcoinRoutes { res.set('Content-Type', 'application/json'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get init data'); } } @@ -105,19 +121,22 @@ class BitcoinRoutes { const result = mempoolBlocks.getMempoolBlocks(); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get mempool blocks'); } } private getTransactionTimes(req: Request, res: Response) { if (!Array.isArray(req.query.txId)) { - res.status(500).send('Not an array'); + handleError(req, res, 500, 'Not an array'); return; } const txIds: string[] = []; for (const _txId in req.query.txId) { if (typeof req.query.txId[_txId] === 'string') { - txIds.push(req.query.txId[_txId].toString()); + const txid = req.query.txId[_txId].toString(); + if (TXID_REGEX.test(txid)) { + txIds.push(txid); + } } } @@ -128,12 +147,16 @@ class BitcoinRoutes { private async $getBatchedOutspends(req: Request, res: Response): Promise { const txids_csv = req.query.txids; if (!txids_csv || typeof txids_csv !== 'string') { - res.status(500).send('Invalid txids format'); + handleError(req, res, 500, 'Invalid txids format'); return; } const txids = txids_csv.split(','); if (txids.length > 50) { - res.status(400).send('Too many txids requested'); + handleError(req, res, 400, 'Too many txids requested'); + return; + } + if (txids.some((txid) => !TXID_REGEX.test(txid))) { + handleError(req, res, 400, 'Invalid txids format'); return; } @@ -141,13 +164,13 @@ class BitcoinRoutes { const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); res.json(batchedOutspends); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get batched outspends'); } } private async $getCpfpInfo(req: Request, res: Response) { - if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { - res.status(501).send(`Invalid transaction ID.`); + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); return; } @@ -180,7 +203,7 @@ class BitcoinRoutes { try { cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); } catch (e) { - res.status(500).send('failed to get CPFP info'); + handleError(req, res, 500, 'Failed to get CPFP info'); return; } } @@ -201,6 +224,10 @@ class BitcoinRoutes { } private async getTransaction(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); res.json(transaction); @@ -208,12 +235,18 @@ class BitcoinRoutes { let statusCode = 500; if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; + handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); + return; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, 'Failed to get transaction'); } } private async getRawTransaction(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); res.setHeader('content-type', 'text/plain'); @@ -222,8 +255,10 @@ class BitcoinRoutes { let statusCode = 500; if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; + handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); + return; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, 'Failed to get raw transaction'); } } @@ -284,18 +319,22 @@ class BitcoinRoutes { // Not modified // 422 Unprocessable Entity // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 - res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); + handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`); } } catch (e: any) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { - res.status(404).send(e.message); + handleError(req, res, 404, notFoundError); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to process PSBT'); } } } private async getTransactionStatus(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); res.json(transaction.status); @@ -303,22 +342,54 @@ class BitcoinRoutes { let statusCode = 500; if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; + handleError(req, res, statusCode, 'No such mempool or blockchain transaction'); + return; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, 'Failed to get transaction status'); } } private async getStrippedBlockTransactions(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block summary'); + } + } + + private async getStrippedBlockTransaction(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } + if (!TXID_REGEX.test(req.params.txid)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } + try { + const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid); + if (!transaction) { + handleError(req, res, 404, `Transaction not found in summary`); + return; + } + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); + res.json(transaction); + } catch (e) { + handleError(req, res, 500, 'Failed to get transaction from summary'); } } private async getBlock(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const block = await blocks.$getBlock(req.params.hash); @@ -330,51 +401,69 @@ class BitcoinRoutes { } else if (blockAge > 30 * day) { cacheDuration = 10 * day; } else { - cacheDuration = 600 + cacheDuration = 600; } res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); - } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + } catch (e: any) { + handleError(req, res, e?.response?.status === 404 ? 404 : 500, 'Failed to get block'); } } private async getBlockHeader(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); res.setHeader('content-type', 'text/plain'); res.send(blockHeader); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block header'); } } private async getBlockAuditSummary(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); if (auditSummary) { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`audit not available`); + handleError(req, res, 404, `Audit not available`); + return; } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit summary'); } } private async $getBlockTxAuditSummary(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } + if (!TXID_REGEX.test(req.params.txid)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); if (auditSummary) { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`transaction audit not available`); + handleError(req, res, 404, `Transaction audit not available`); + return; } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get transaction audit summary'); } } @@ -388,42 +477,49 @@ class BitcoinRoutes { return await this.getLegacyBlocks(req, res); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks'); } } private async getBlocksByBulk(req: Request, res: Response) { try { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented - return res.status(404).send(`This API is only available for Bitcoin networks`); + handleError(req, res, 404, `This API is only available for Bitcoin networks`); + return; } if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { - return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + return; } if (!Common.indexingEnabled()) { - return res.status(404).send(`Indexing is required for this API`); + handleError(req, res, 404, `Indexing is required for this API`); + return; } const from = parseInt(req.params.from, 10); if (!req.params.from || from < 0) { - return res.status(400).send(`Parameter 'from' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`); + return; } const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); if (to < 0) { - return res.status(400).send(`Parameter 'to' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`); + return; } if (from > to) { - return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); + handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`); + return; } if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { - return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + return; } res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await blocks.$getBlocksBetweenHeight(from, to)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks'); } } @@ -458,11 +554,15 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(returnBlocks); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks'); } } - + private async getBlockTransactions(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); @@ -483,7 +583,7 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block transactions'); } } @@ -492,13 +592,17 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block at height'); } } private async getAddress(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); + return; + } + if (!ADDRESS_REGEX.test(req.params.address)) { + handleError(req, res, 501, `Invalid address`); return; } @@ -507,15 +611,20 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get address'); } } private async getAddressTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); + return; + } + if (!ADDRESS_REGEX.test(req.params.address)) { + handleError(req, res, 501, `Invalid address`); return; } @@ -528,23 +637,27 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get address transactions'); } } private async getAddressTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Address summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.'); return; } } private async getScriptHash(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); + return; + } + if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) { + handleError(req, res, 501, `Invalid scripthash`); return; } @@ -555,15 +668,20 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get script hash'); } } private async getScriptHashTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); + return; + } + if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) { + handleError(req, res, 501, `Invalid scripthash`); return; } @@ -578,26 +696,26 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e.message); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get script hash transactions'); } } private async getScriptHashTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Scripthash summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.'); return; } } private async getAddressPrefix(req: Request, res: Response) { try { - const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); - res.send(blockHash); + const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix); + res.send(addressPrefix); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get address prefix'); } } @@ -624,7 +742,53 @@ class BitcoinRoutes { const rawMempool = await bitcoinApi.$getRawMempool(); res.send(rawMempool); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + + private async getBlockDefinitionHashes(req: Request, res: Response): Promise { + try { + const result = await blocks.$getBlockDefinitionHashes(); + if (!result) { + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; + } + res.setHeader('content-type', 'application/json'); + res.send(result); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + + private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise { + try { + const currentSha = await poolsUpdater.getShaFromDb(); + if (!currentSha) { + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; + } + res.setHeader('content-type', 'text/plain'); + res.send(currentSha); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); + } + } + + private async getBlocksByDefinitionHash(req: Request, res: Response): Promise { + try { + if (typeof(req.params.definitionHash) !== 'string') { + res.status(400).send('Parameter "hash" must be a valid string'); + return; + } + const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string); + if (!blocksHash) { + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; + } + res.setHeader('content-type', 'application/json'); + res.send(blocksHash); + } catch (e) { + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -632,12 +796,13 @@ class BitcoinRoutes { try { const result = blocks.getCurrentBlockHeight(); if (!result) { - return res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; } res.setHeader('content-type', 'text/plain'); res.send(result.toString()); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get height at tip'); } } @@ -647,39 +812,55 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get hash at tip'); } } private async getRawBlock(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const result = await bitcoinApi.$getRawBlock(req.params.hash); res.setHeader('content-type', 'application/octet-stream'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get raw block'); } } private async getTxIdsForBlock(req: Request, res: Response) { + if (!BLOCK_HASH_REGEX.test(req.params.hash)) { + handleError(req, res, 501, `Invalid block hash`); + return; + } try { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get txids for block'); } } private async validateAddress(req: Request, res: Response) { + if (!ADDRESS_REGEX.test(req.params.address)) { + handleError(req, res, 501, `Invalid address`); + return; + } try { const result = await bitcoinClient.validateAddress(req.params.address); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to validate address'); } } private async getRbfHistory(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const replacements = rbfCache.getRbfTree(req.params.txId) || null; const replaces = rbfCache.getReplaces(req.params.txId) || null; @@ -688,7 +869,7 @@ class BitcoinRoutes { replaces }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get rbf history'); } } @@ -697,7 +878,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(false); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get rbf trees'); } } @@ -706,11 +887,15 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(true); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get full rbf replacements'); } } private async getCachedTx(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const result = rbfCache.getTx(req.params.txId); if (result) { @@ -719,16 +904,20 @@ class BitcoinRoutes { res.status(204).send(); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get cached tx'); } } private async getTransactionOutspends(req: Request, res: Response) { + if (!TXID_REGEX.test(req.params.txId)) { + handleError(req, res, 501, `Invalid transaction ID`); + return; + } try { const result = await bitcoinApi.$getOutspends(req.params.txId); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get transaction outspends'); } } @@ -738,10 +927,10 @@ class BitcoinRoutes { if (da) { res.json(da); } else { - res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get difficulty change'); } } @@ -752,8 +941,8 @@ class BitcoinRoutes { const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to send raw transaction'); } } @@ -764,8 +953,8 @@ class BitcoinRoutes { const txIdResult = await bitcoinClient.sendRawTransaction(txHex); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to send raw transaction'); } } @@ -776,12 +965,110 @@ class BitcoinRoutes { const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { - res.setHeader('content-type', 'text/plain'); - res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) - : (e.message || 'Error')); + handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to test transactions'); + } + } + + private async $submitPackage(req: Request, res: Response) { + try { + const rawTxs = Common.getTransactionsFromRequest(req); + const maxfeerate = parseFloat(req.query.maxfeerate as string); + const maxburnamount = parseFloat(req.query.maxburnamount as string); + const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined); + res.send(result); + } catch (e: any) { + handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code }) + : 'Failed to submit package'); } } + private async $getPrevouts(req: Request, res: Response) { + try { + const outpoints = req.body; + if (!Array.isArray(outpoints) || outpoints.some((item) => !/^[a-fA-F0-9]{64}$/.test(item.txid) || typeof item.vout !== 'number')) { + handleError(req, res, 400, 'Invalid outpoints format'); + return; + } + + if (outpoints.length > 100) { + handleError(req, res, 400, 'Too many outpoints requested'); + return; + } + + const result = Array(outpoints.length).fill(null); + const memPool = mempool.getMempool(); + + for (let i = 0; i < outpoints.length; i++) { + const outpoint = outpoints[i]; + let prevout: IEsploraApi.Vout | null = null; + let unconfirmed: boolean | null = null; + + const mempoolTx = memPool[outpoint.txid]; + if (mempoolTx) { + if (outpoint.vout < mempoolTx.vout.length) { + prevout = mempoolTx.vout[outpoint.vout]; + unconfirmed = true; + } + } else { + try { + const rawPrevout = await bitcoinClient.getTxOut(outpoint.txid, outpoint.vout, false); + if (rawPrevout) { + prevout = { + value: Math.round(rawPrevout.value * 100000000), + scriptpubkey: rawPrevout.scriptPubKey.hex, + scriptpubkey_asm: rawPrevout.scriptPubKey.asm ? transactionUtils.convertScriptSigAsm(rawPrevout.scriptPubKey.hex) : '', + scriptpubkey_type: transactionUtils.translateScriptPubKeyType(rawPrevout.scriptPubKey.type), + scriptpubkey_address: rawPrevout.scriptPubKey && rawPrevout.scriptPubKey.address ? rawPrevout.scriptPubKey.address : '', + }; + unconfirmed = false; + } + } catch (e) { + // Ignore bitcoin client errors, just leave prevout as null + } + } + + if (prevout) { + result[i] = { prevout, unconfirmed }; + } + } + + res.json(result); + + } catch (e) { + handleError(req, res, 500, 'Failed to get prevouts'); + } + } + + private getCpfpLocalTxs(req: Request, res: Response) { + try { + const transactions = req.body; + + if (!Array.isArray(transactions) || transactions.some(tx => + !tx || typeof tx !== 'object' || + !/^[a-fA-F0-9]{64}$/.test(tx.txid) || + typeof tx.weight !== 'number' || + typeof tx.sigops !== 'number' || + typeof tx.fee !== 'number' || + !Array.isArray(tx.vin) || + !Array.isArray(tx.vout) + )) { + handleError(req, res, 400, 'Invalid transactions format'); + return; + } + + if (transactions.length > 1) { + handleError(req, res, 400, 'More than one transaction is not supported yet'); + return; + } + + const cpfpInfo = calculateMempoolTxCpfp(transactions[0], mempool.getMempool(), true); + res.json([cpfpInfo]); + + } catch (e) { + handleError(req, res, 500, 'Failed to calculate CPFP info'); + } + } } export default new BitcoinRoutes(); diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts index 6e6860a417..13fb3526df 100644 --- a/backend/src/api/bitcoin/esplora-api.interface.ts +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -179,4 +179,11 @@ export namespace IEsploraApi { burn_count: number; } + export interface AddressTxSummary { + txid: string; + value: number; + height: number; + time: number; + tx_position?: number; + } } diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index b4ae35da9f..950bb18b42 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -1,12 +1,13 @@ import config from '../../config'; -import axios, { AxiosResponse, isAxiosError } from 'axios'; +import axios, { isAxiosError } from 'axios'; import http from 'http'; +import https from 'https'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; import { Common } from '../common'; -import { TestMempoolAcceptResult } from './bitcoin-api.interface'; - +import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface'; +import os from 'os'; interface FailoverHost { host: string, rtts: number[], @@ -20,6 +21,14 @@ interface FailoverHost { preferred?: boolean, checked: boolean, lastChecked?: number, + publicDomain: string, + hashes: { + frontend?: string, + hybrid?: string, + backend?: string, + electrs?: string, + lastUpdated: number, + } } class FailoverRouter { @@ -29,14 +38,21 @@ class FailoverRouter { maxHeight: number = 0; hosts: FailoverHost[]; multihost: boolean; - pollInterval: number = 60000; + gitHashInterval: number = 60000; // 1 minute + pollInterval: number = 60000; // 1 minute pollTimer: NodeJS.Timeout | null = null; pollConnection = axios.create(); + localHostname: string = 'localhost'; requestConnection = axios.create({ httpAgent: new http.Agent({ keepAlive: true }) }); constructor() { + try { + this.localHostname = os.hostname(); + } catch (e) { + logger.warn('Failed to set local hostname, using "localhost"'); + } // setup list of hosts this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { return { @@ -45,6 +61,10 @@ class FailoverRouter { rtts: [], rtt: Infinity, failures: 0, + publicDomain: 'https://' + this.extractPublicDomain(domain), + hashes: { + lastUpdated: 0, + }, }; }); this.activeHost = { @@ -55,6 +75,10 @@ class FailoverRouter { socket: !!config.ESPLORA.UNIX_SOCKET_PATH, preferred: true, checked: false, + publicDomain: `http://${this.localHostname}`, + hashes: { + lastUpdated: 0, + }, }; this.fallbackHost = this.activeHost; this.hosts.unshift(this.activeHost); @@ -89,7 +113,7 @@ class FailoverRouter { for (const host of this.hosts) { try { const result = await (host.socket - ? this.pollConnection.get('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) + ? this.pollConnection.get('http://api/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) : this.pollConnection.get(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT }) ); if (result) { @@ -106,6 +130,25 @@ class FailoverRouter { host.outOfSync = false; } host.unreachable = false; + + // update esplora git hash using the x-powered-by header from the height check + const poweredBy = result.headers['x-powered-by']; + if (poweredBy) { + const match = poweredBy.match(/([a-fA-F0-9]{5,40})/); + if (match && match[1]?.length) { + host.hashes.electrs = match[1]; + } + } + + // Check front and backend git hashes less often + if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) { + await Promise.all([ + this.$updateFrontendGitHash(host), + this.$updateBackendGitHash(host), + config.MEMPOOL.OFFICIAL ? this.$updateHybridGitHash(host) : Promise.resolve(), + ]); + host.hashes.lastUpdated = Date.now(); + } } else { host.outOfSync = true; host.unreachable = true; @@ -202,12 +245,94 @@ class FailoverRouter { } } + // methods for retrieving git hashes by host + private async $updateFrontendGitHash(host: FailoverHost): Promise { + try { + const url = `${host.publicDomain}/resources/config.js`; + const response = await this.pollConnection.get(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); + const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/); + if (match && match[1]?.length) { + host.hashes.frontend = match[1]; + } + const hybridMatch = response.data.match(/GIT_COMMIT_HASH_MEMPOOL_SPACE\s*=\s*['"](.*?)['"]/); + if (hybridMatch && hybridMatch[1]?.length) { + host.hashes.hybrid = hybridMatch[1]; + } + } catch (e) { + // failed to get frontend build hash - do nothing + } + } + + private async $updateHybridGitHash(host: FailoverHost): Promise { + try { + const response: string = await new Promise((resolve, reject) => { + const req = https.request({ + hostname: host.publicDomain.replace('https://', '').replace('http://', ''), + port: 443, + path: '/en-US/resources/config.js', + method: 'GET', + headers: { + 'Host': 'mempool.space' + }, + timeout: config.ESPLORA.FALLBACK_TIMEOUT, + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode === 200) { + resolve(data); + } else { + reject(new Error(`Failed to get hybrid git hash: ${res.statusCode}`)); + } + }); + }); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); + const match = response.match(/GIT_COMMIT_HASH_MEMPOOL_SPACE\s*=\s*['"](.*?)['"]/); + if (match && match[1]?.length) { + host.hashes.hybrid = match[1]; + } + } catch (e) { + // failed to get frontend build hash - do nothing + } + } + + private async $updateBackendGitHash(host: FailoverHost): Promise { + try { + const url = `${host.publicDomain}/api/v1/backend-info`; + const response = await this.pollConnection.get(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT }); + if (response.data?.gitCommit) { + host.hashes.backend = response.data.gitCommit; + } + } catch (e) { + // failed to get backend build hash - do nothing + } + } + + // returns the public mempool domain corresponding to an esplora server url + // (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server) + private extractPublicDomain(url: string): string { + // force the url to start with a valid protocol + const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`; + // parse as URL and extract the hostname + try { + const parsed = new URL(urlWithProtocol); + return parsed.hostname; + } catch (e) { + // fallback to the original url + return url; + } + } + private async $query(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise { let axiosConfig; let url; if (host.socket) { axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType }; - url = path; + url = 'http://api' + path; } else { axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType }; url = host.host + path; @@ -305,7 +430,7 @@ class ElectrsApi implements AbstractBitcoinApi { } $getAddress(address: string): Promise { - throw new Error('Method getAddress not implemented.'); + return this.failoverRouter.$get('/address/' + address); } $getAddressTransactions(address: string, txId?: string): Promise { @@ -332,6 +457,10 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } + $submitPackage(rawTransactions: string[]): Promise { + throw new Error('Method not implemented.'); + } + $getOutspend(txId: string, vout: number): Promise { return this.failoverRouter.$get('/tx/' + txId + '/outspend/' + vout); } @@ -357,6 +486,10 @@ class ElectrsApi implements AbstractBitcoinApi { return this.failoverRouter.$get('/tx/' + txid); } + async $getAddressTransactionSummary(address: string): Promise { + return this.failoverRouter.$get('/address/' + address + '/txs/summary'); + } + public startHealthChecks(): void { this.failoverRouter.startHealthChecks(); } @@ -373,6 +506,7 @@ class ElectrsApi implements AbstractBitcoinApi { unreachable: !!host.unreachable, checked: !!host.checked, lastChecked: host.lastChecked || 0, + hashes: host.hashes, })); } else { return []; diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index a5b8af0e2c..aa1fa9751e 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -33,7 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository'; import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import mempool from './mempool'; import CpfpRepository from '../repositories/CpfpRepository'; -import accelerationApi from './services/acceleration'; +import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; +import database from '../database'; class Blocks { private blocks: BlockExtended[] = []; @@ -219,10 +220,10 @@ class Blocks { }; } - public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { + public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary { return { id: hash, - transactions: Common.classifyTransactions(transactions), + transactions: Common.classifyTransactions(transactions, height), }; } @@ -243,7 +244,7 @@ class Blocks { */ private async $getBlockExtended(block: IEsploraApi.Block, transactions: TransactionExtended[]): Promise { const coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); - + const blk: Partial = Object.assign({}, block); const extras: Partial = {}; @@ -295,7 +296,7 @@ class Blocks { extras.medianFeeAmt = extras.feePercentiles[3]; } } - + extras.virtualSize = block.weight / 4.0; if (coinbaseTx?.vout.length > 0) { extras.coinbaseAddress = coinbaseTx.vout[0].scriptpubkey_address ?? null; @@ -342,7 +343,12 @@ class Blocks { id: pool.uniqueId, name: pool.name, slug: pool.slug, + minerNames: null, }; + + if (extras.pool.name === 'OCEAN') { + extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); + } } extras.matchRate = null; @@ -406,8 +412,16 @@ class Blocks { } try { + const blockchainInfo = await bitcoinClient.getBlockchainInfo(); + const currentBlockHeight = blockchainInfo.blocks; + let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight); + if (indexingBlockAmount <= -1) { + indexingBlockAmount = currentBlockHeight + 1; + } + const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1); + // Get all indexed block hash - const indexedBlocks = await blocksRepository.$getIndexedBlocks(); + const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex); const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId(); const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop @@ -616,7 +630,7 @@ class Blocks { // add CPFP const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); // classify - const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { const cpfpClusters = await CpfpRepository.$getClustersAt(height); @@ -653,7 +667,7 @@ class Blocks { } const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); // classify - const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; for (const tx of classifiedTxs) { classifiedTxMap[tx.txid] = tx; @@ -912,7 +926,7 @@ class Blocks { } const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); - const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); + const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); if (Common.indexingEnabled()) { @@ -1169,7 +1183,7 @@ class Blocks { transactions: cpfpSummary.transactions.map(tx => { let flags: number = 0; try { - flags = Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx, height); } catch (e) { logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); } @@ -1188,7 +1202,7 @@ class Blocks { } else { if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); - summary = this.summarizeBlockTransactions(hash, txs); + summary = this.summarizeBlockTransactions(hash, height || 0, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1210,6 +1224,11 @@ class Blocks { return summary.transactions; } + public async $getSingleTxFromSummary(hash: string, txid: string): Promise { + const txs = await this.$getStrippedBlockTransactions(hash); + return txs.find(tx => tx.txid === txid) || null; + } + /** * Get 15 blocks * @@ -1324,7 +1343,7 @@ class Blocks { let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); - summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); + summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1372,7 +1391,7 @@ class Blocks { } public async $getBlockAuditSummary(hash: string): Promise { - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) { return BlocksAuditsRepository.$getBlockAudit(hash); } else { return null; @@ -1380,7 +1399,7 @@ class Blocks { } public async $getBlockTxAuditSummary(hash: string, txid: string): Promise { - if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { + if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) { return BlocksAuditsRepository.$getBlockTxAudit(hash, txid); } else { return null; @@ -1443,6 +1462,36 @@ class Blocks { // not a fatal error, we'll try again next time the indexer runs } } + + public async $getBlockDefinitionHashes(): Promise { + try { + const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`); + if (rows && Array.isArray(rows)) { + return rows.map(r => r.definition_hash); + } else { + logger.debug(`Unable to retrieve list of blocks.definition_hash from db (no result)`); + return null; + } + } catch (e) { + logger.debug(`Unable to retrieve list of blocks.definition_hash from db (exception: ${e})`); + return null; + } + } + + public async $getBlocksByDefinitionHash(definitionHash: string): Promise { + try { + const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]); + if (rows && Array.isArray(rows)) { + return rows.map(r => r.hash); + } else { + logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (no result)`); + return null; + } + } catch (e) { + logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (exception: ${e})`); + return null; + } + } } export default new Blocks(); diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 13fc86147e..0e29ac44b2 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -8,9 +8,9 @@ import transactionUtils from './transaction-utils'; import { isPoint } from '../utils/secp256k1'; import logger from '../logger'; import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; +import { IEsploraApi } from './bitcoin/esplora-api.interface'; // Bitcoin Core default policy settings -const TX_MAX_STANDARD_VERSION = 2; const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); @@ -80,8 +80,8 @@ export class Common { return arr; } - static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { - const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; + static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { + const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; // For small N, a naive nested loop is extremely fast, but it doesn't scale if (added.length < 1000 && deleted.length < 50 && !forceScalable) { @@ -96,7 +96,7 @@ export class Common { addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); }); if (foundMatches?.length) { - matches[addedTx.txid] = [...new Set(foundMatches)]; + matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; } }); } else { @@ -124,7 +124,7 @@ export class Common { foundMatches.add(deletedTx); } if (foundMatches.size) { - matches[addedTx.txid] = [...foundMatches]; + matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx }; } } } @@ -139,17 +139,17 @@ export class Common { const replaced: Set = new Set(); for (let i = 0; i < tx.vin.length; i++) { const vin = tx.vin[i]; - const match = spendMap.get(`${vin.txid}:${vin.vout}`); + const key = `${vin.txid}:${vin.vout}`; + const match = spendMap.get(key); if (match && match.txid !== tx.txid) { replaced.add(match); // remove this tx from the spendMap // prevents the same tx being replaced more than once for (const replacedVin of match.vin) { - const key = `${replacedVin.txid}:${replacedVin.vout}`; - spendMap.delete(key); + const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; + spendMap.delete(replacedKey); } } - const key = `${vin.txid}:${vin.vout}`; spendMap.delete(key); } if (replaced.size) { @@ -200,10 +200,13 @@ export class Common { * * returns true early if any standardness rule is violated, otherwise false * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) + * + * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks. + * For now, just pull out individual rules into versioned functions where necessary. */ - static isNonStandard(tx: TransactionExtended): boolean { + static isNonStandard(tx: TransactionExtended, height?: number): boolean { // version - if (tx.version > TX_MAX_STANDARD_VERSION) { + if (this.isNonStandardVersion(tx, height)) { return true; } @@ -250,8 +253,35 @@ export class Common { } } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { return true; + } else if (vin.prevout?.scriptpubkey_type === 'anchor' && this.isNonStandardAnchor(vin, height)) { + return true; } - // TODO: bad-witness-nonstandard + // bad-witness-nonstandard + if (vin.prevout?.scriptpubkey_type === 'v1_p2tr' && vin.witness?.length) { + const hasAnnex = vin.witness.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50'); + // annex is non-standard + if (hasAnnex) { + return true; + } + if (vin.witness.length > (hasAnnex ? 2 : 1)) { + // script path spend + const controlBlock = vin.witness[vin.witness.length - (hasAnnex ? 2 : 1)]; + // control block is required + if (!controlBlock.length) { + return false; + } else { + // Leaf version must be 0xc0 (aka Tapscript, see BIP 342) + if ((parseInt(controlBlock.slice(0, 2), 16) & 0xfe) !== 0xc0) { + return false; + } + } + // remaining witness items (except for the script) must be within MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit + if (vin.witness.slice(0, vin.witness.length - (hasAnnex ? 3 : 2)).some(v => v.length > 160)) { + return false; + } + } + } + // TODO: other bad-witness-nonstandard cases } // output validation @@ -299,7 +329,7 @@ export class Common { } if (vout.value < (dustSize * DUST_RELAY_TX_FEE)) { // under minimum output size - return true; + return !Common.isStandardEphemeralDust(tx, height); } } } @@ -335,6 +365,70 @@ export class Common { return false; } + // Individual versioned standardness rules + + static V3_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, + }; + static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean { + let TX_MAX_STANDARD_VERSION = 3; + if ( + height != null + && this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + ) { + // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + TX_MAX_STANDARD_VERSION = 2; + } + + if (tx.version > TX_MAX_STANDARD_VERSION) { + return true; + } + return false; + } + + static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, + }; + static isNonStandardAnchor(vin: IEsploraApi.Vin, height?: number): boolean { + if ( + height != null + && this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && vin.prevout?.scriptpubkey === '51024e73' + ) { + // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + return true; + } + return false; + } + + // Ephemeral dust is a new concept that allows a single dust output in a transaction, provided the transaction is zero fee + static EPHEMERAL_DUST_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 90_500, + 'testnet': 4_550_000, + 'signet': 260_000, + '': 905_000, + }; + static isStandardEphemeralDust(tx: TransactionExtended, height?: number): boolean { + if ( + tx.fee === 0 + && (height == null || ( + this.EPHEMERAL_DUST_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height >= this.EPHEMERAL_DUST_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + )) + ) { + return true; + } + return false; + } + static getNonWitnessSize(tx: TransactionExtended): number { let weight = tx.weight; let hasWitness = false; @@ -415,7 +509,52 @@ export class Common { return flags; } - static getTransactionFlags(tx: TransactionExtended): number { + static inputIsMaybeInscription(vin: IEsploraApi.Vin): boolean { + // check if this is actually a taproot input + let isTaproot = false; + let isNotTaproot = false; + + // taproot inputs have no scriptsig (otherwise probably wrapped segwit) + if (vin.scriptsig?.length) { + isNotTaproot = true; + } + + // p2wpkh consist of a DER-encoded signature followed by a compressed pubkey + if (!isNotTaproot + && this.isDERSig(vin.witness[0]) + && (vin.witness[1].startsWith('02') || vin.witness[1].startsWith('03')) + && vin.witness[1].length === 66) { + isNotTaproot = true; + } + + // p2wsh could be almost anything, but ends with a script which + // probably doesn't match a valid taproot control block length (32 + 33m) + if (!isNotTaproot + && vin.witness.length >= 2 + ) { + const hasAnnex = vin.witness[vin.witness.length - 1].startsWith('50'); + const controlBlock = vin.witness[vin.witness.length - (hasAnnex ? 2 : 1)]; + if ((controlBlock.length - 66) % 64 === 0) { + isNotTaproot = true; + } + } + + // signed taproot inscriptions should have 3 non-annex witness items: + // 1) schnorr signature + // 2) inscription script + // 3) control block (length 33 + 32m) + if (!isTaproot + && vin.witness.length >= 3 + && vin.witness[0].length === 128 || vin.witness[0].length === 130 + && (vin.witness[2].length - 66) % 64 === 0 + ) { + isTaproot = true; + } + + return isTaproot || !isNotTaproot; + } + + static getTransactionFlags(tx: TransactionExtended, height?: number): number { let flags = tx.flags ? BigInt(tx.flags) : 0n; // Update variable flags (CPFP, RBF) @@ -466,12 +605,17 @@ export class Common { flags |= TransactionFlags.p2tr; if (vin.witness?.length) { flags = Common.isInscription(vin, flags); + const hasAnnex = vin.witness.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50'); + if (hasAnnex) { + flags |= TransactionFlags.annex; + } } } break; } } else { // no prevouts, optimistically check witness-bearing inputs - if (vin.witness?.length >= 2) { + if (vin.witness?.length >= 2 && Common.inputIsMaybeInscription(vin)) { + // try to parse the witness as a taproot inscription try { flags = Common.isInscription(vin, flags); } catch { @@ -548,7 +692,7 @@ export class Common { if (hasFakePubkey) { flags |= TransactionFlags.fake_pubkey; } - + // fast but bad heuristic to detect possible coinjoins // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse) const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; @@ -564,17 +708,17 @@ export class Common { flags |= TransactionFlags.batch_payout; } - if (this.isNonStandard(tx)) { + if (this.isNonStandard(tx, height)) { flags |= TransactionFlags.nonstandard; } return Number(flags); } - static classifyTransaction(tx: TransactionExtended): TransactionClassified { + static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified { let flags = 0; try { - flags = Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx, height); } catch (e) { logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); } @@ -585,8 +729,8 @@ export class Common { }; } - static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { - return txs.map(Common.classifyTransaction); + static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] { + return txs.map(tx => Common.classifyTransaction(tx, height)); } static stripTransaction(tx: TransactionExtended): TransactionStripped { @@ -675,6 +819,13 @@ export class Common { ); } + static auditIndexingEnabled(): boolean { + return ( + Common.indexingEnabled() && + config.MEMPOOL.AUDIT === true + ); + } + static gogglesIndexingEnabled(): boolean { return ( Common.blocksSummariesIndexingEnabled() && @@ -809,14 +960,16 @@ export class Common { } } - static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats { + static calcEffectiveFeeStatistics(transactions: { weight: number, fee?: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats { const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate); + const totalWeight = transactions.reduce((acc, tx) => acc + tx.weight, 0); - let weightCount = 0; + // include any unused space + let weightCount = config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight; let medianFee = 0; let medianWeight = 0; - // calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions + // calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of the conceptual block const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800; const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth); const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth); diff --git a/backend/src/api/cpfp.ts b/backend/src/api/cpfp.ts index 9da11328b0..953664fcc2 100644 --- a/backend/src/api/cpfp.ts +++ b/backend/src/api/cpfp.ts @@ -167,8 +167,10 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran /** * Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for * that transaction (and all others in the same cluster) + * If the passed transaction is not guaranteed to be in the mempool, set localTx to true: this will + * prevent updating the CPFP data of other transactions in the cluster */ -export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo { +export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }, localTx: boolean = false): CpfpInfo { if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) { tx.cpfpDirty = false; return { @@ -198,17 +200,26 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: totalFee += tx.fees.base; } const effectiveFeePerVsize = totalFee / totalVsize; - for (const tx of cluster.values()) { - mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; - mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); - mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); - mempool[tx.txid].bestDescendant = null; - mempool[tx.txid].cpfpChecked = true; - mempool[tx.txid].cpfpDirty = true; - mempool[tx.txid].cpfpUpdated = Date.now(); - } - tx = mempool[tx.txid]; + if (localTx) { + tx.effectiveFeePerVsize = effectiveFeePerVsize; + tx.ancestors = Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base })); + tx.descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !cluster.get(tx.txid)?.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); + tx.bestDescendant = null; + } else { + for (const tx of cluster.values()) { + mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize; + mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); + mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base })); + mempool[tx.txid].bestDescendant = null; + mempool[tx.txid].cpfpChecked = true; + mempool[tx.txid].cpfpDirty = true; + mempool[tx.txid].cpfpUpdated = Date.now(); + } + + tx = mempool[tx.txid]; + + } return { ancestors: tx.ancestors || [], diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 6ddca76976..973dab7e19 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 81; + private static currentVersion = 101; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -700,6 +700,473 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); await this.updateToSchemaVersion(81); } + + if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') { + await this.$fixBadV1AuditBlocks(); + await this.updateToSchemaVersion(82); + } + + if (databaseSchemaVersion < 83 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); + await this.updateToSchemaVersion(83); + } + + // add new pools indexes + if (databaseSchemaVersion < 84 && isBitcoin === true) { + await this.$executeQuery(` + ALTER TABLE \`pools\` + ADD INDEX \`slug\` (\`slug\`), + ADD INDEX \`unique_id\` (\`unique_id\`) + `); + await this.updateToSchemaVersion(84); + } + + // lightning channels indexes + if (databaseSchemaVersion < 85 && isBitcoin === true) { + await this.$executeQuery(` + ALTER TABLE \`channels\` + ADD INDEX \`created\` (\`created\`), + ADD INDEX \`capacity\` (\`capacity\`), + ADD INDEX \`closing_reason\` (\`closing_reason\`), + ADD INDEX \`closing_resolved\` (\`closing_resolved\`) + `); + await this.updateToSchemaVersion(85); + } + + // lightning nodes indexes + if (databaseSchemaVersion < 86 && isBitcoin === true) { + await this.$executeQuery(` + ALTER TABLE \`nodes\` + ADD INDEX \`status\` (\`status\`), + ADD INDEX \`channels\` (\`channels\`), + ADD INDEX \`country_id\` (\`country_id\`), + ADD INDEX \`as_number\` (\`as_number\`), + ADD INDEX \`first_seen\` (\`first_seen\`) + `); + await this.updateToSchemaVersion(86); + } + + // lightning node sockets indexes + if (databaseSchemaVersion < 87 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)'); + await this.updateToSchemaVersion(87); + } + + // lightning stats indexes + if (databaseSchemaVersion < 88 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)'); + await this.updateToSchemaVersion(88); + } + + // geo names indexes + if (databaseSchemaVersion < 89 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)'); + await this.updateToSchemaVersion(89); + } + + // hashrates indexes + if (databaseSchemaVersion < 90 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)'); + await this.updateToSchemaVersion(90); + } + + // block audits indexes + if (databaseSchemaVersion < 91 && isBitcoin === true) { + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)'); + await this.updateToSchemaVersion(91); + } + + // elements_pegs indexes + if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') { + await this.$executeQuery(` + ALTER TABLE \`elements_pegs\` + ADD INDEX \`block\` (\`block\`), + ADD INDEX \`datetime\` (\`datetime\`), + ADD INDEX \`amount\` (\`amount\`), + ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`), + ADD INDEX \`bitcointxid\` (\`bitcointxid\`) + `); + await this.updateToSchemaVersion(92); + } + + // federation_txos indexes + if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') { + await this.$executeQuery(` + ALTER TABLE \`federation_txos\` + ADD INDEX \`unspent\` (\`unspent\`), + ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`), + ADD INDEX \`blocktime\` (\`blocktime\`), + ADD INDEX \`emergencyKey\` (\`emergencyKey\`), + ADD INDEX \`expiredAt\` (\`expiredAt\`) + `); + await this.updateToSchemaVersion(93); + } + + // Unify database schema for all mempool netwoks + // versions above 94 should not use network-specific flags + if (databaseSchemaVersion < 94) { + + if (!isBitcoin) { + // Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!) + // Version 5 + await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"'); + + // Version 6 + await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`'); + await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL'); + await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)'); + await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""'); + await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL'); + + // Version 7 + await this.$executeQuery('DROP table IF EXISTS hashrates;'); + await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates')); + + // Version 8 + await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"'); + + // Version 9 + await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)'); + await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)'); + + // Version 10 + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)'); + + // Version 11 + await this.$executeQuery(`ALTER TABLE blocks + ADD avg_fee INT UNSIGNED NULL, + ADD avg_fee_rate INT UNSIGNED NULL + `); + await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 12 + await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 13 + await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 14 + await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`'); + await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"'); + + // Version 17 + await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL'); + + // Version 18 + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);'); + + // Version 20 + await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries')); + + // Version 22 + await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`'); + await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments')); + + // Version 24 + await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`'); + await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits')); + + // Version 25 + await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats')); + await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes')); + await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels')); + await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); + + // Version 26 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"'); + + // Version 27 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"'); + + // Version 28 + await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`); + + // Version 29 + await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names')); + await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL'); + + // Version 30 + await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL'); + + // Version 31 + await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE'); + await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`'); + await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices')); + + // Version 32 + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"'); + + // Version 33 + await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL'); + + // Version 34 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"'); + + // Version 35 + await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);'); + + // Version 36 + await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"'); + + // Version 37 + await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets')); + + // Version 38 + await this.$executeQuery(`TRUNCATE lightning_stats`); + await this.$executeQuery(`TRUNCATE node_stats`); + await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL'); + await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL'); + await this.updateToSchemaVersion(38); + + // Version 39 + await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`'); + await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)'); + + // Version 40 + await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);'); + + // Version 41 + await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1'); + + // Version 42 + await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0'); + + // Version 43 + await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records')); + + // Version 44 + await this.$executeQuery('UPDATE blocks_summaries SET template = NULL'); + + // Version 45 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"'); + + // Version 48 + await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"'); + + // Version 57 + await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`); + + // Version 60 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"'); + + // Version 61 + if (! await this.$checkIfTableExists('blocks_templates')) { + await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"'); + } + await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"'); + await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)'); + await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template'); + + // Version 62 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL'); + + // Version 63 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"'); + + // Version 64 + await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL'); + + // Version 65 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"'); + + // Version 67 + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)'); + await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)'); + + // Version 76 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"'); + + // Version 81 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)'); + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); + + // Version 83 + await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL'); + + // Version 84 + await this.$executeQuery(` + ALTER TABLE \`pools\` + ADD INDEX \`slug\` (\`slug\`), + ADD INDEX \`unique_id\` (\`unique_id\`) + `); + + // Version 85 + await this.$executeQuery(` + ALTER TABLE \`channels\` + ADD INDEX \`created\` (\`created\`), + ADD INDEX \`capacity\` (\`capacity\`), + ADD INDEX \`closing_reason\` (\`closing_reason\`), + ADD INDEX \`closing_resolved\` (\`closing_resolved\`) + `); + + // Version 86 + await this.$executeQuery(` + ALTER TABLE \`nodes\` + ADD INDEX \`status\` (\`status\`), + ADD INDEX \`channels\` (\`channels\`), + ADD INDEX \`country_id\` (\`country_id\`), + ADD INDEX \`as_number\` (\`as_number\`), + ADD INDEX \`first_seen\` (\`first_seen\`) + `); + + // Version 87 + await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)'); + await this.updateToSchemaVersion(87); + + // Version 88 + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)'); + + // Version 89 + await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)'); + + // Version 90 + await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)'); + + // Version 91 + await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)'); + } + + if (config.MEMPOOL.NETWORK !== 'liquid') { + // Apply all the liquid specific migrations to all other networks + // Version 68 + await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);'); + await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses')); + await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos')); + + // Version 71 + await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0'); + + // Version 92 + await this.$executeQuery(` + ALTER TABLE \`elements_pegs\` + ADD INDEX \`block\` (\`block\`), + ADD INDEX \`datetime\` (\`datetime\`), + ADD INDEX \`amount\` (\`amount\`), + ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`), + ADD INDEX \`bitcointxid\` (\`bitcointxid\`) + `); + + // Version 93 + await this.$executeQuery(` + ALTER TABLE \`federation_txos\` + ADD INDEX \`unspent\` (\`unspent\`), + ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`), + ADD INDEX \`blocktime\` (\`blocktime\`), + ADD INDEX \`emergencyKey\` (\`emergencyKey\`), + ADD INDEX \`expiredAt\` (\`expiredAt\`) + `); + } + + if (config.MEMPOOL.NETWORK !== 'mainnet') { + // Apply all the mainnet specific migrations to all other networks + // Version 69 + await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations')); + + // Version 70 + await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;'); + + // Version 77 + await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL'); + } + await this.updateToSchemaVersion(94); + } + + // blocks pools-v2.json hash + if (databaseSchemaVersion < 95) { + let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254'; // https://github.com/mempool/mining-pools/commit/f737d86571d190cf1a1a3cf5fd86b33ba9624254 + const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`); + if (poolJsonShaDb?.length > 0) { + poolJsonSha = poolJsonShaDb[0].string; + } + await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`); + await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)'); + await this.updateToSchemaVersion(95); + } + + if (databaseSchemaVersion < 96) { + await this.$executeQuery(`ALTER TABLE blocks_audits MODIFY time timestamp NOT NULL DEFAULT 0`); + await this.updateToSchemaVersion(96); + } + + // Make definition_hash nullable + if (databaseSchemaVersion < 97) { + let poolJsonSha = '895cf0903e771beb647d0c1356bb4b8f4f123af7'; // https://github.com/mempool/mining-pools/commit/895cf0903e771beb647d0c1356bb4b8f4f123af7 + const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`); + if (poolJsonShaDb?.length > 0) { + poolJsonSha = poolJsonShaDb[0].string; + } + await this.$executeQuery(`ALTER TABLE blocks MODIFY COLUMN definition_hash varchar(255) NULL DEFAULT "${poolJsonSha}"`); + await this.updateToSchemaVersion(97); + } + + // reindex mainnet Goggles flags for mined block templates above height 896070 + // (since the first annex transaction at height 896071) + // (safe to make this conditional on the network since it doesn't change the database schema) + if (databaseSchemaVersion < 98 && config.MEMPOOL.NETWORK === 'mainnet') { + await this.$executeQuery('UPDATE blocks_summaries SET version = 0 WHERE height >= 896070;'); + await this.updateToSchemaVersion(98); + } + + // Add vsize_0 to statistics table + if (databaseSchemaVersion < 99) { + await this.$executeQuery('ALTER TABLE statistics ADD COLUMN vsize_0 int(11) NOT NULL DEFAULT 0'); + await this.updateToSchemaVersion(99); + } + + // Add "block indexed at version" index_version column to the blocks table + // to be used for lazy migrations & reindexing tasks + if (databaseSchemaVersion < 100) { + await this.$executeQuery('ALTER TABLE `blocks` ADD index_version INT NOT NULL DEFAULT 0'); + await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `index_version` (`index_version`)'); + } } /** @@ -832,6 +1299,10 @@ class DatabaseMigration { queries.push(`DELETE FROM state WHERE name = 'last_weekly_hashrates_indexing'`); } + if (version < 101) { + queries.push(`DELETE FROM prices WHERE USD = -1`); + } + return queries; } @@ -1314,6 +1785,28 @@ class DatabaseMigration { logger.warn(`Failed to migrate cpfp transaction data`); } } + + private async $fixBadV1AuditBlocks(): Promise { + const badBlocks = [ + '000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc', + '000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960', + '000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7', + '00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286', + '0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb', + ]; + + for (const hash of badBlocks) { + try { + await this.$executeQuery(` + UPDATE blocks_audits + SET prioritized_txs = '[]' + WHERE hash = '${hash}' + `, true); + } catch (e) { + continue; + } + } + } } export default new DatabaseMigration(); diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 202f8f4cba..f2a1f2390a 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -257,6 +257,7 @@ class DiskCache { trees: rbfData.rbf.trees, expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } } catch (e) { diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 391bf628e8..031aeea176 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -1,6 +1,9 @@ import config from '../../config'; import { Application, Request, Response } from 'express'; import channelsApi from './channels.api'; +import { handleError } from '../../utils/api'; + +const TXID_REGEX = /^[a-f0-9]{64}$/i; class ChannelsRoutes { constructor() { } @@ -22,7 +25,7 @@ class ChannelsRoutes { const channels = await channelsApi.$searchChannelsById(req.params.search); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to search channels by id'); } } @@ -38,7 +41,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channel); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channel'); } } @@ -53,11 +56,11 @@ class ChannelsRoutes { const status: string = typeof req.query.status === 'string' ? req.query.status : ''; if (index < -1) { - res.status(400).send('Invalid index'); + handleError(req, res, 400, 'Invalid index'); return; } if (['open', 'active', 'closed'].includes(status) === false) { - res.status(400).send('Invalid status'); + handleError(req, res, 400, 'Invalid status'); return; } @@ -69,20 +72,23 @@ class ChannelsRoutes { res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channels for node'); } } private async $getChannelsByTransactionIds(req: Request, res: Response): Promise { try { if (!Array.isArray(req.query.txId)) { - res.status(400).send('Not an array'); + handleError(req, res, 400, 'Not an array'); return; } const txIds: string[] = []; for (const _txId in req.query.txId) { if (typeof req.query.txId[_txId] === 'string') { - txIds.push(req.query.txId[_txId].toString()); + const txid = req.query.txId[_txId].toString(); + if (TXID_REGEX.test(txid)) { + txIds.push(txid); + } } } const channels = await channelsApi.$getChannelsByTransactionId(txIds); @@ -107,7 +113,7 @@ class ChannelsRoutes { res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channels by transaction ids'); } } @@ -119,7 +125,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get penalty closed channels'); } } @@ -132,7 +138,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get channel geodata'); } } diff --git a/backend/src/api/explorer/general.routes.ts b/backend/src/api/explorer/general.routes.ts index 07620e84ab..f974c98105 100644 --- a/backend/src/api/explorer/general.routes.ts +++ b/backend/src/api/explorer/general.routes.ts @@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import channelsApi from './channels.api'; import statisticsApi from './statistics.api'; +import { handleError } from '../../utils/api'; + class GeneralLightningRoutes { constructor() { } @@ -27,7 +29,7 @@ class GeneralLightningRoutes { channels: channels, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to search for nodes and channels'); } } @@ -41,7 +43,7 @@ class GeneralLightningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get lightning statistics'); } } @@ -50,7 +52,7 @@ class GeneralLightningRoutes { const statistics = await statisticsApi.$getLatestStatistics(); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get lightning statistics'); } } } diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 9d63738455..811292b4b8 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import DB from '../../database'; import { INodesRanking } from '../../mempool.interfaces'; +import { handleError } from '../../utils/api'; class NodesRoutes { constructor() { } @@ -31,7 +32,7 @@ class NodesRoutes { const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to search for node'); } } @@ -181,13 +182,13 @@ class NodesRoutes { } } catch (e) {} } - + res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get node group'); } } @@ -195,7 +196,7 @@ class NodesRoutes { try { const node = await nodesApi.$getNode(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -203,7 +204,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get node'); } } @@ -215,7 +216,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical node stats'); } } @@ -223,7 +224,7 @@ class NodesRoutes { try { const node = await nodesApi.$getFeeHistogram(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -231,7 +232,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get fee histogram'); } } @@ -247,7 +248,7 @@ class NodesRoutes { topByChannels: topChannelsNodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes ranking'); } } @@ -259,7 +260,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get top nodes by capacity'); } } @@ -271,7 +272,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get top nodes by channels'); } } @@ -283,7 +284,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get oldest nodes'); } } @@ -295,7 +296,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get ISP ranking'); } } @@ -307,7 +308,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(worldNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get world nodes'); } } @@ -322,7 +323,7 @@ class NodesRoutes { ); if (country.length === 0) { - res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`); return; } @@ -335,7 +336,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes per country'); } } @@ -349,7 +350,7 @@ class NodesRoutes { ); if (isp.length === 0) { - res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`); return; } @@ -362,7 +363,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes per ISP'); } } @@ -374,7 +375,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get nodes per country'); } } } diff --git a/backend/src/api/fee-api.ts b/backend/src/api/fee-api.ts index 24fd25a4bd..8efb8f605b 100644 --- a/backend/src/api/fee-api.ts +++ b/backend/src/api/fee-api.ts @@ -1,4 +1,5 @@ import { MempoolBlock } from '../mempool.interfaces'; +import { IBitcoinApi } from './bitcoin/bitcoin-api.interface'; import config from '../config'; import mempool from './mempool'; import projectedBlocks from './mempool-blocks'; @@ -22,6 +23,11 @@ class FeeApi { public getRecommendedFee(): RecommendedFees { const pBlocks = projectedBlocks.getMempoolBlocks(); const mPool = mempool.getMempoolInfo(); + + return this.calculateRecommendedFee(pBlocks, mPool); + } + + public calculateRecommendedFee(pBlocks: MempoolBlock[], mPool: IBitcoinApi.MempoolInfo): RecommendedFees { const minimumFee = this.roundUpToNearest(mPool.mempoolminfee * 100000, this.minimumIncrement); const defaultMinFee = Math.max(minimumFee, this.defaultFee); @@ -64,7 +70,7 @@ class FeeApi { private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number { const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee; - if (pBlock.blockVSize <= 500000) { + if (pBlock.blockVSize <= 500000 || pBlock.medianFee < 1) { return this.defaultFee; } if (pBlock.blockVSize <= 950000 && !nextBlock) { diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index 9ea61ca31c..4976d1694e 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import elementsParser from './elements-parser'; import icons from './icons'; +import { handleError } from '../../utils/api'; +import PricesRepository from '../../repositories/PricesRepository'; class LiquidRoutes { public initRoutes(app: Application) { @@ -30,6 +32,7 @@ class LiquidRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/emergency-spent', this.$getEmergencySpentUtxos) .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos/emergency-spent/stats', this.$getEmergencySpentUtxosStats) .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/status', this.$getFederationAuditStatus) + .get(config.MEMPOOL.API_URL_PREFIX + 'historical-price', this.$getHistoricalPrice) ; } } @@ -42,7 +45,7 @@ class LiquidRoutes { res.setHeader('content-length', result.length); res.send(result); } else { - res.status(404).send('Asset icon not found'); + handleError(req, res, 404, 'Asset icon not found'); } } @@ -51,7 +54,7 @@ class LiquidRoutes { if (result) { res.json(result); } else { - res.status(404).send('Asset icons not found'); + handleError(req, res, 404, 'Asset icons not found'); } } @@ -82,7 +85,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(pegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs by month'); } } @@ -94,7 +97,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(reserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get reserves by month'); } } @@ -106,7 +109,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentSupply); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs'); } } @@ -118,7 +121,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentReserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get reserves'); } } @@ -130,7 +133,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(auditStatus); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation audit status'); } } @@ -142,7 +145,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation addresses'); } } @@ -154,7 +157,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation addresses'); } } @@ -166,7 +169,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation utxos'); } } @@ -178,7 +181,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(expiredUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get expired utxos'); } } @@ -190,7 +193,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get federation utxos number'); } } @@ -202,7 +205,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get emergency spent utxos'); } } @@ -214,7 +217,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get emergency spent utxos stats'); } } @@ -226,7 +229,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(recentPegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs list'); } } @@ -238,7 +241,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsVolume); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs volume daily'); } } @@ -250,7 +253,35 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsCount); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pegs count'); + } + } + + private async $getHistoricalPrice(req: Request, res: Response): Promise { + try { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); + if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { + handleError(req, res, 400, 'Prices are not available on testnets.'); + return; + } + const timestamp = parseInt(req.query.timestamp as string, 10) || 0; + const currency = req.query.currency as string; + + let response; + if (timestamp && currency) { + response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); + } else if (timestamp) { + response = await PricesRepository.$getNearestHistoricalPrice(timestamp); + } else if (currency) { + response = await PricesRepository.$getHistoricalPrices(currency); + } else { + response = await PricesRepository.$getHistoricalPrices(); + } + res.status(200).send(response); + } catch (e) { + handleError(req, res, 500, 'Failed to get historical prices'); } } diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 5d9dcf8f48..ba4ce2ed0f 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -369,7 +369,7 @@ class MempoolBlocks { const lastBlockIndex = blocks.length - 1; let hasBlockStack = blocks.length >= 8; let stackWeight; - let feeStatsCalculator: OnlineFeeStatsCalculator | void; + let feeStatsCalculator: OnlineFeeStatsCalculator | null = null; if (hasBlockStack) { if (blockWeights && blockWeights[7] !== null) { stackWeight = blockWeights[7]; @@ -380,28 +380,36 @@ class MempoolBlocks { feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); } + const ancestors: Ancestor[] = []; + const descendants: Ancestor[] = []; + let ancestor: MempoolTransactionExtended; for (const cluster of clusters) { for (const memberTxid of cluster) { const mempoolTx = mempool[memberTxid]; if (mempoolTx) { - const ancestors: Ancestor[] = []; - const descendants: Ancestor[] = []; + // ugly micro-optimization to avoid allocating new arrays + ancestors.length = 0; + descendants.length = 0; let matched = false; cluster.forEach(txid => { + ancestor = mempool[txid]; if (txid === memberTxid) { matched = true; } else { - if (!mempool[txid]) { + if (!ancestor) { console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); + return; } const relative = { txid: txid, - fee: mempool[txid].fee, - weight: (mempool[txid].adjustedVsize * 4), + fee: ancestor.fee, + weight: (ancestor.adjustedVsize * 4), }; if (matched) { descendants.push(relative); - mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0); + if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) { + mempoolTx.lastBoosted = ancestor.firstSeen; + } } else { ancestors.push(relative); } @@ -410,7 +418,20 @@ class MempoolBlocks { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { mempoolTx.cpfpDirty = true; } - Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); + // ugly micro-optimization to avoid allocating new arrays or objects + if (mempoolTx.ancestors) { + mempoolTx.ancestors.length = 0; + } else { + mempoolTx.ancestors = []; + } + if (mempoolTx.descendants) { + mempoolTx.descendants.length = 0; + } else { + mempoolTx.descendants = []; + } + mempoolTx.ancestors.push(...ancestors); + mempoolTx.descendants.push(...descendants); + mempoolTx.cpfpChecked = true; } } } @@ -420,7 +441,10 @@ class MempoolBlocks { const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; // update this thread's mempool with the results let mempoolTx: MempoolTransactionExtended; - const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => { + let acceleration: Acceleration; + const mempoolBlocks: MempoolBlockWithTransactions[] = []; + for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { + const block = blocks[blockIndex]; let totalSize = 0; let totalVsize = 0; let totalWeight = 0; @@ -436,8 +460,9 @@ class MempoolBlocks { } } - for (const txid of block) { - if (txid) { + for (let i = 0; i < block.length; i++) { + const txid = block[i]; + if (txid in mempool) { mempoolTx = mempool[txid]; // save position in projected blocks mempoolTx.position = { @@ -445,30 +470,40 @@ class MempoolBlocks { vsize: totalVsize + (mempoolTx.vsize / 2), }; - const acceleration = accelerations[txid]; - if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { - if (!mempoolTx.acceleration) { - mempoolTx.cpfpDirty = true; - } - mempoolTx.acceleration = true; - mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; - mempoolTx.acceleratedAt = acceleration?.added; - mempoolTx.feeDelta = acceleration?.feeDelta; - for (const ancestor of mempoolTx.ancestors || []) { - if (!mempool[ancestor.txid].acceleration) { - mempool[ancestor.txid].cpfpDirty = true; + if (txid in accelerations) { + acceleration = accelerations[txid]; + if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { + if (!mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + } + mempoolTx.acceleration = true; + mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; + mempoolTx.acceleratedAt = acceleration?.added; + mempoolTx.feeDelta = acceleration?.feeDelta; + for (const ancestor of mempoolTx.ancestors || []) { + if (!(ancestor.txid in mempool)) { + continue; + } + if (!mempool[ancestor.txid].acceleration) { + mempool[ancestor.txid].cpfpDirty = true; + } + mempool[ancestor.txid].acceleration = true; + mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; + mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; + mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; + isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; + } + } else { + if (mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - mempool[ancestor.txid].acceleration = true; - mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; - mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; - mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; - isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; } } else { if (mempoolTx.acceleration) { mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - delete mempoolTx.acceleration; } // online calculation of stack-of-blocks fee stats @@ -486,7 +521,7 @@ class MempoolBlocks { } } } - return this.dataToMempoolBlocks( + mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( block, transactions, totalSize, @@ -494,7 +529,7 @@ class MempoolBlocks { totalFees, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, ); - }); + }; if (saveResults) { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); @@ -656,7 +691,7 @@ class MempoolBlocks { [pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; } = {}; // prepare a list of accelerations in ascending order (we'll pop items off the end of the list) - const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => { + const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => { let vsize = mempoolCache[acc.txid].vsize; for (const ancestor of mempoolCache[acc.txid].ancestors || []) { vsize += (ancestor.weight / 4); diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 1f55179fb2..87e7f10cd2 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import rbfCache from './rbf-cache'; import { Acceleration } from './services/acceleration'; +import accelerationApi from './services/acceleration'; import redisCache from './redis-cache'; import blocks from './blocks'; @@ -19,12 +20,13 @@ class Mempool { private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {}; private spendMap = new Map(); + private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 }; private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined; private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; private accelerations: { [txId: string]: Acceleration } = {}; private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; @@ -74,12 +76,12 @@ class Mempool { } public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void { + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void { this.mempoolChangedCallback = fn; } public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise): void { this.$asyncMempoolChangedCallback = fn; } @@ -206,7 +208,7 @@ class Mempool { return txTimes; } - public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise { + public async $updateMempool(transactions: string[], accelerations: Record | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise { logger.debug(`Updating mempool...`); // warn if this run stalls the main loop for more than 2 minutes @@ -353,7 +355,7 @@ class Mempool { const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); - const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : []; + const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : []; if (accelerationDelta.length) { hasChange = true; } @@ -362,12 +364,15 @@ class Mempool { const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; - if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { - this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); + this.recentlyDeleted.unshift(deletedTransactions); + this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates + + if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) { + this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta); } - if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) { + if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) { this.updateTimerProgress(timer, 'running async mempool callback'); - await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates); + await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates); this.updateTimerProgress(timer, 'completed async mempool callback'); } @@ -395,58 +400,11 @@ class Mempool { return this.accelerations; } - public $updateAccelerations(newAccelerations: Acceleration[]): string[] { + public updateAccelerations(newAccelerationMap: Record): string[] { try { - const changed: string[] = []; - - const newAccelerationMap: { [txid: string]: Acceleration } = {}; - for (const acceleration of newAccelerations) { - // skip transactions we don't know about - if (!this.mempoolCache[acceleration.txid]) { - continue; - } - newAccelerationMap[acceleration.txid] = acceleration; - if (this.accelerations[acceleration.txid] == null) { - // new acceleration - changed.push(acceleration.txid); - } else { - if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) { - // feeDelta changed - changed.push(acceleration.txid); - } else if (this.accelerations[acceleration.txid].pools?.length) { - let poolsChanged = false; - const pools = new Set(); - this.accelerations[acceleration.txid].pools.forEach(pool => { - pools.add(pool); - }); - acceleration.pools.forEach(pool => { - if (!pools.has(pool)) { - poolsChanged = true; - } else { - pools.delete(pool); - } - }); - if (pools.size > 0) { - poolsChanged = true; - } - if (poolsChanged) { - // pools changed - changed.push(acceleration.txid); - } - } - } - } - - for (const oldTxid of Object.keys(this.accelerations)) { - if (!newAccelerationMap[oldTxid]) { - // removed - changed.push(oldTxid); - } - } - + const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap); this.accelerations = newAccelerationMap; - - return changed; + return accelerationDelta; } catch (e: any) { logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e)); return []; @@ -541,16 +499,7 @@ class Mempool { } } - public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void { - for (const rbfTransaction in rbfTransactions) { - if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) { - // Store replaced transactions - rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]); - } - } - } - - public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { + public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { for (const rbfTransaction in rbfTransactions) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { // Store replaced transactions diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 8f8bbac828..ede047eed7 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -10,6 +10,7 @@ import mining from "./mining"; import PricesRepository from '../../repositories/PricesRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository'; import accelerationApi from '../services/acceleration'; +import { handleError } from '../../utils/api'; class MiningRoutes { public initRoutes(app: Application) { @@ -53,12 +54,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Prices are not available on testnets.'); + handleError(req, res, 400, 'Prices are not available on testnets.'); return; } const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const currency = req.query.currency as string; - + let response; if (timestamp && currency) { response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); @@ -71,7 +72,7 @@ class MiningRoutes { } res.status(200).send(response); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical prices'); } } @@ -84,9 +85,9 @@ class MiningRoutes { res.json(stats); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pool'); } } } @@ -103,9 +104,9 @@ class MiningRoutes { res.json(poolBlocks); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get blocks for pool'); } } } @@ -129,7 +130,7 @@ class MiningRoutes { res.json(pools); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pools'); } } @@ -143,7 +144,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(stats); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pools'); } } @@ -157,7 +158,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(hashrates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pools historical hashrate'); } } @@ -172,9 +173,9 @@ class MiningRoutes { res.json(hashrates); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get pool historical hashrate'); } } } @@ -182,7 +183,7 @@ class MiningRoutes { private async $getHistoricalHashrate(req: Request, res: Response) { let currentHashrate = 0, currentDifficulty = 0; try { - currentHashrate = await bitcoinClient.getNetworkHashPs(); + currentHashrate = await bitcoinClient.getNetworkHashPs(1008); currentDifficulty = await bitcoinClient.getDifficulty(); } catch (e) { logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); @@ -203,7 +204,7 @@ class MiningRoutes { currentDifficulty: currentDifficulty, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical hashrate'); } } @@ -217,7 +218,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block fees'); } } @@ -235,7 +236,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block fees'); } } @@ -249,7 +250,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockRewards); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block rewards'); } } @@ -263,7 +264,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFeeRates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block fee rates'); } } @@ -281,7 +282,7 @@ class MiningRoutes { weights: blockWeights }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical block size and weight'); } } @@ -293,7 +294,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical difficulty adjustments'); } } @@ -303,7 +304,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(response); } catch (e) { - res.status(500).end(); + handleError(req, res, 500, 'Failed to get reward stats'); } } @@ -317,7 +318,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get historical blocks health'); } } @@ -326,7 +327,7 @@ class MiningRoutes { const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); if (!audit) { - res.status(204).send(`This block has not been audited.`); + handleError(req, res, 204, `This block has not been audited.`); return; } @@ -335,7 +336,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit'); } } @@ -358,7 +359,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get height from timestamp'); } } @@ -371,7 +372,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit scores'); } } @@ -384,7 +385,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit || 'null'); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get block audit score'); } } @@ -394,12 +395,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get accelerations by pool'); } } @@ -409,13 +410,13 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get accelerations by height'); } } @@ -425,12 +426,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get recent accelerations'); } } @@ -440,12 +441,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationTotals(req.query.pool, req.query.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get acceleration totals'); } } @@ -455,12 +456,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } - res.status(200).send(accelerationApi.accelerations || []); + res.status(200).send(Object.values(accelerationApi.getAccelerations() || {})); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get active accelerations'); } } @@ -472,7 +473,7 @@ class MiningRoutes { accelerationApi.accelerationRequested(req.params.txid); res.status(200).send(); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to request acceleration'); } } } diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index 21ee4b35a6..0e9dcf91a2 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -136,9 +136,13 @@ class Mining { poolsStatistics['blockCount'] = blockCount; const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); + const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d'); + const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w'); try { poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); + poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d); + poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w); } catch (e) { poolsStatistics['lastEstimatedHashrate'] = 0; logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); @@ -252,31 +256,36 @@ class Mining { const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp( null, fromTimestamp / 1000, toTimestamp / 1000); - const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, - blockStats.lastBlockHeight); - - let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000); - const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); - if (totalBlocks > 0) { - pools = pools.map((pool: any) => { - pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; - pool.share = (pool.blockCount / totalBlocks); - return pool; - }); - for (const pool of pools) { - hashrates.push({ - hashrateTimestamp: toTimestamp / 1000, - avgHashrate: pool['hashrate'] , - poolId: pool.poolId, - share: pool['share'], - type: 'weekly', + if (blockStats.blockCount <= 0) { + logger.debug(`No block found between ${fromTimestamp / 1000} and ${toTimestamp / 1000}, skipping hashrate indexing for this period`, logger.tags.mining); + } else { + const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount, + blockStats.lastBlockHeight); + + let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000); + const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0); + if (totalBlocks > 0) { + pools = pools.map((pool: any) => { + pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate; + pool.share = (pool.blockCount / totalBlocks); + return pool; }); - } - newlyIndexed += hashrates.length / Math.max(1, pools.length); - await HashratesRepository.$saveHashrates(hashrates); - hashrates.length = 0; + for (const pool of pools) { + hashrates.push({ + hashrateTimestamp: toTimestamp / 1000, + avgHashrate: pool['hashrate'] , + poolId: pool.poolId, + share: pool['share'], + type: 'weekly', + }); + } + + newlyIndexed += hashrates.length / Math.max(1, pools.length); + await HashratesRepository.$saveHashrates(hashrates); + hashrates.length = 0; + } } const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); diff --git a/backend/src/api/pools-parser.ts b/backend/src/api/pools-parser.ts index 289389d5e2..2895da5a50 100644 --- a/backend/src/api/pools-parser.ts +++ b/backend/src/api/pools-parser.ts @@ -8,6 +8,7 @@ import mining from './mining/mining'; import transactionUtils from './transaction-utils'; import BlocksRepository from '../repositories/BlocksRepository'; import redisCache from './redis-cache'; +import blocks from './blocks'; class PoolsParser { miningPools: any[] = []; @@ -19,15 +20,6 @@ class PoolsParser { 'addresses': '[]', 'slug': 'unknown' }; - private uniqueLogs: string[] = []; - - private uniqueLog(loggerFunction: any, msg: string): void { - if (this.uniqueLogs.includes(msg)) { - return; - } - this.uniqueLogs.push(msg); - loggerFunction(msg); - } public setMiningPools(pools): void { for (const pool of pools) { @@ -51,6 +43,8 @@ class PoolsParser { await this.$insertUnknownPool(); let reindexUnknown = false; + let clearCache = false; + for (const pool of this.miningPools) { if (!pool.id) { @@ -87,17 +81,20 @@ class PoolsParser { logger.debug(`Inserting new mining pool ${pool.name}`); await PoolsRepository.$insertNewMiningPool(pool, slug); reindexUnknown = true; + clearCache = true; } else { if (poolDB.name !== pool.name) { // Pool has been renamed const newSlug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase(); logger.warn(`Renaming ${poolDB.name} mining pool to ${pool.name}. Slug has been updated. Maybe you want to make a redirection from 'https://mempool.space/mining/pool/${poolDB.slug}' to 'https://mempool.space/mining/pool/${newSlug}`); await PoolsRepository.$renameMiningPool(poolDB.id, newSlug, pool.name); + clearCache = true; } if (poolDB.link !== pool.link) { // Pool link has changed logger.debug(`Updating link for ${pool.name} mining pool`); await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link); + clearCache = true; } if (JSON.stringify(pool.addresses) !== poolDB.addresses || JSON.stringify(pool.regexes) !== poolDB.regexes) { @@ -105,6 +102,7 @@ class PoolsParser { logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`); await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes); reindexUnknown = true; + clearCache = true; await this.$reindexBlocksForPool(poolDB.id); } } @@ -120,6 +118,19 @@ class PoolsParser { } await this.$reindexBlocksForPool(unknownPool.id); } + + // refresh the in-memory block cache with the reindexed data + if (clearCache) { + for (const block of blocks.getBlocks()) { + const reindexedBlock = await blocks.$indexBlock(block.height); + if (reindexedBlock.id === block.id) { + block.extras.pool = reindexedBlock.extras.pool; + } + } + // update persistent cache with the reindexed data + diskCache.$saveCacheToDisk(); + redisCache.$updateBlocks(blocks.getBlocks()); + } } public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined { diff --git a/backend/src/api/prices/prices.routes.ts b/backend/src/api/prices/prices.routes.ts index b46331b73e..e395fb44b8 100644 --- a/backend/src/api/prices/prices.routes.ts +++ b/backend/src/api/prices/prices.routes.ts @@ -1,10 +1,15 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import pricesUpdater from '../../tasks/price-updater'; +import logger from '../../logger'; +import PricesRepository from '../../repositories/PricesRepository'; class PricesRoutes { public initRoutes(app: Application): void { - app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this)); + app + .get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this)) + .get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this)) + ; } private $getCurrentPrices(req: Request, res: Response): void { @@ -14,6 +19,23 @@ class PricesRoutes { res.json(pricesUpdater.getLatestPrices()); } + + private async $getAllPrices(req: Request, res: Response): Promise { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString()); + + try { + const usdPriceHistory = await PricesRepository.$getPricesTimesAndId(); + const responseData = usdPriceHistory.map(p => { + return { time: p.time, USD: p.USD }; + }); + res.status(200).json(responseData); + } catch (e: any) { + logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`); + res.status(403).send(); + } + } } export default new PricesRoutes(); diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index a087abbe00..df6e10c774 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -44,6 +44,22 @@ interface CacheEvent { value?: any, } +/** + * Singleton for tracking RBF trees + * + * Maintains a set of RBF trees, where each tree represents a sequence of + * consecutive RBF replacements. + * + * Trees are identified by the txid of the root transaction. + * + * To maintain consistency, the following invariants must be upheld: + * - Symmetry: replacedBy(A) = B <=> A in replaces(B) + * - Unique id: treeMap(treeMap(X)) = treeMap(X) + * - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B) + * - Existence: X in treeMap => treeMap(X) in rbfTrees + * - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap + */ + class RbfCache { private replacedBy: Map = new Map(); private replaces: Map = new Map(); @@ -61,6 +77,10 @@ class RbfCache { setInterval(this.cleanup.bind(this), 1000 * 60 * 10); } + /** + * Low level cache operations + */ + private addTx(txid: string, tx: MempoolTransactionExtended): void { this.txs.set(txid, tx); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); @@ -92,8 +112,18 @@ class RbfCache { this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); } + /** + * Basic data structure operations + * must uphold tree invariants + */ + + public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { - if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { + if ( !newTxExtended + || !replaced?.length + || this.txs.has(newTxExtended.txid) + || !(replaced.some(tx => !this.replacedBy.has(tx.txid))) + ) { return; } @@ -114,6 +144,10 @@ class RbfCache { if (!replacedTx.rbf) { txFullRbf = true; } + if (this.replacedBy.has(replacedTx.txid)) { + // should never happen + continue; + } this.replacedBy.set(replacedTx.txid, newTx.txid); if (this.treeMap.has(replacedTx.txid)) { const treeId = this.treeMap.get(replacedTx.txid); @@ -140,18 +174,47 @@ class RbfCache { } } newTx.fullRbf = txFullRbf; - const treeId = replacedTrees[0].tx.txid; const newTree = { tx: newTx, time: newTime, fullRbf: treeFullRbf, replaces: replacedTrees }; - this.addTree(treeId, newTree); - this.updateTreeMap(treeId, newTree); + this.addTree(newTree.tx.txid, newTree); + this.updateTreeMap(newTree.tx.txid, newTree); this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); } + public mined(txid): void { + if (!this.txs.has(txid)) { + return; + } + const treeId = this.treeMap.get(txid); + if (treeId && this.rbfTrees.has(treeId)) { + const tree = this.rbfTrees.get(treeId); + if (tree) { + this.setTreeMined(tree, txid); + tree.mined = true; + this.dirtyTrees.add(treeId); + this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); + } + } + this.evict(txid); + } + + // flag a transaction as removed from the mempool + public evict(txid: string, fast: boolean = false): void { + this.evictionCount++; + if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { + const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours + this.addExpiration(txid, expiryTime); + } + } + + /** + * Read-only public interface + */ + public has(txId: string): boolean { return this.txs.has(txId); } @@ -232,32 +295,6 @@ class RbfCache { return changes; } - public mined(txid): void { - if (!this.txs.has(txid)) { - return; - } - const treeId = this.treeMap.get(txid); - if (treeId && this.rbfTrees.has(treeId)) { - const tree = this.rbfTrees.get(treeId); - if (tree) { - this.setTreeMined(tree, txid); - tree.mined = true; - this.dirtyTrees.add(treeId); - this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); - } - } - this.evict(txid); - } - - // flag a transaction as removed from the mempool - public evict(txid: string, fast: boolean = false): void { - this.evictionCount++; - if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { - const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours - this.addExpiration(txid, expiryTime); - } - } - // is the transaction involved in a full rbf replacement? public isFullRbf(txid: string): boolean { const treeId = this.treeMap.get(txid); @@ -271,6 +308,10 @@ class RbfCache { return tree?.fullRbf; } + /** + * Cache maintenance & utility functions + */ + private cleanup(): void { const now = Date.now(); for (const txid of this.expiring.keys()) { @@ -299,10 +340,6 @@ class RbfCache { for (const tx of (replaces || [])) { // recursively remove prior versions from the cache this.replacedBy.delete(tx); - // if this is the id of a tree, remove that too - if (this.treeMap.get(tx) === tx) { - this.removeTree(tx); - } this.remove(tx); } } @@ -370,14 +407,21 @@ class RbfCache { }; } - public async load({ txs, trees, expiring, mempool }): Promise { + public async load({ txs, trees, expiring, mempool, spendMap }): Promise { try { txs.forEach(txEntry => { this.txs.set(txEntry.value.txid, txEntry.value); }); this.staleCount = 0; - for (const deflatedTree of trees) { - await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) { + const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + if (tree) { + this.addTree(tree.tx.txid, tree); + this.updateTreeMap(tree.tx.txid, tree); + if (tree.mined) { + this.evict(tree.tx.txid); + } + } } expiring.forEach(expiringEntry => { if (this.txs.has(expiringEntry.key)) { @@ -385,6 +429,31 @@ class RbfCache { } }); this.staleCount = 0; + + // connect cached trees to current mempool transactions + const conflicts: Record }> = {}; + for (const tree of this.rbfTrees.values()) { + const tx = this.getTx(tree.tx.txid); + if (!tx || tree.mined) { + continue; + } + for (const vin of tx.vin) { + const conflict = spendMap.get(`${vin.txid}:${vin.vout}`); + if (conflict && conflict.txid !== tx.txid) { + if (!conflicts[conflict.txid]) { + conflicts[conflict.txid] = { + replacedBy: conflict, + replaces: new Set(), + }; + } + conflicts[conflict.txid].replaces.add(tx); + } + } + } + for (const { replacedBy, replaces } of Object.values(conflicts)) { + this.add([...replaces.values()], replacedBy); + } + await this.checkTrees(); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); this.cleanup(); @@ -426,6 +495,12 @@ class RbfCache { return; } + // if this tx is already in the cache, return early + if (this.treeMap.has(txid)) { + this.removeTree(deflated.key); + return; + } + // recursively reconstruct child trees for (const childId of treeInfo.replaces) { const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); @@ -457,10 +532,6 @@ class RbfCache { fullRbf: treeInfo.fullRbf, replaces, }; - this.treeMap.set(txid, root); - if (root === txid) { - this.addTree(root, tree); - } return tree; } @@ -511,6 +582,7 @@ class RbfCache { processTxs(txs); } + // evict missing transactions for (const txid of txids) { if (!found[txid]) { this.evict(txid, false); diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index cbfa2f18b5..1caade15b2 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -365,6 +365,7 @@ class RedisCache { trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } diff --git a/backend/src/api/services/acceleration.ts b/backend/src/api/services/acceleration.ts index 88289382b9..053da6e82d 100644 --- a/backend/src/api/services/acceleration.ts +++ b/backend/src/api/services/acceleration.ts @@ -1,7 +1,10 @@ +import { WebSocket } from 'ws'; import config from '../../config'; import logger from '../../logger'; import { BlockExtended } from '../../mempool.interfaces'; import axios from 'axios'; +import mempool from '../mempool'; +import websocketHandler from '../websocket-handler'; type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; @@ -37,14 +40,23 @@ export interface AccelerationHistory { }; class AccelerationApi { + private ws: WebSocket | null = null; + private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS; + private startedWebsocketLoop: boolean = false; + private websocketConnected: boolean = false; private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS; private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); - private _accelerations: Acceleration[] | null = null; + private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/'; + private _accelerations: Record = {}; private lastPoll = 0; + private lastPing = Date.now(); + private lastPong = Date.now(); private forcePoll = false; private myAccelerations: Record = {}; - public get accelerations(): Acceleration[] | null { + public constructor() {} + + public getAccelerations(): Record { return this._accelerations; } @@ -72,11 +84,18 @@ class AccelerationApi { } } - public async $updateAccelerations(): Promise { + public async $updateAccelerations(): Promise | null> { + if (this.useWebsocket && this.websocketConnected) { + return this._accelerations; + } if (!this.onDemandPollingEnabled) { const accelerations = await this.$fetchAccelerations(); if (accelerations) { - this._accelerations = accelerations; + const latestAccelerations = {}; + for (const acc of accelerations) { + latestAccelerations[acc.txid] = acc; + } + this._accelerations = latestAccelerations; return this._accelerations; } } else { @@ -85,7 +104,7 @@ class AccelerationApi { return null; } - private async $updateAccelerationsOnDemand(): Promise { + private async $updateAccelerationsOnDemand(): Promise | null> { const shouldUpdate = this.forcePoll || this.countMyAccelerationsWithStatus('requested') > 0 || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); @@ -120,7 +139,11 @@ class AccelerationApi { } } - this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]; + const latestAccelerations = {}; + for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) { + latestAccelerations[acc.txid] = acc; + } + this._accelerations = latestAccelerations; return this._accelerations; } @@ -152,6 +175,148 @@ class AccelerationApi { } return anyAccelerated; } + + // get a list of accelerations that have changed between two sets of accelerations + public getAccelerationDelta(oldAccelerationMap: Record, newAccelerationMap: Record): string[] { + const changed: string[] = []; + const mempoolCache = mempool.getMempool(); + + for (const acceleration of Object.values(newAccelerationMap)) { + // skip transactions we don't know about + if (!mempoolCache[acceleration.txid]) { + continue; + } + if (oldAccelerationMap[acceleration.txid] == null) { + // new acceleration + changed.push(acceleration.txid); + } else { + if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) { + // feeDelta changed + changed.push(acceleration.txid); + } else if (oldAccelerationMap[acceleration.txid].pools?.length) { + let poolsChanged = false; + const pools = new Set(); + oldAccelerationMap[acceleration.txid].pools.forEach(pool => { + pools.add(pool); + }); + acceleration.pools.forEach(pool => { + if (!pools.has(pool)) { + poolsChanged = true; + } else { + pools.delete(pool); + } + }); + if (pools.size > 0) { + poolsChanged = true; + } + if (poolsChanged) { + // pools changed + changed.push(acceleration.txid); + } + } + } + } + + for (const oldTxid of Object.keys(oldAccelerationMap)) { + if (!newAccelerationMap[oldTxid]) { + // removed + changed.push(oldTxid); + } + } + + return changed; + } + + private handleWebsocketMessage(msg: any): void { + if (msg?.accelerations !== null) { + const latestAccelerations = {}; + for (const acc of msg?.accelerations || []) { + latestAccelerations[acc.txid] = acc; + } + this._accelerations = latestAccelerations; + websocketHandler.handleAccelerationsChanged(this._accelerations); + } + } + + public async connectWebsocket(): Promise { + if (this.startedWebsocketLoop) { + return; + } + while (this.useWebsocket) { + this.startedWebsocketLoop = true; + if (!this.ws) { + this.ws = new WebSocket(this.websocketPath); + this.lastPing = 0; + + this.ws.on('open', () => { + logger.info(`Acceleration websocket opened to ${this.websocketPath}`); + this.websocketConnected = true; + this.ws?.send(JSON.stringify({ + 'watch-accelerations': true + })); + }); + + this.ws.on('error', (error) => { + let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`; + if (error['errors']) { + errMsg += ' - ' + error['errors'].join(' - '); + } + logger.err(errMsg); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('close', () => { + logger.info('Acceleration websocket closed'); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('message', (data, isBinary) => { + try { + const msg = (isBinary ? data : data.toString()) as string; + const parsedMsg = msg?.length ? JSON.parse(msg) : null; + this.handleWebsocketMessage(parsedMsg); + } catch (e) { + logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e)); + } + }); + + this.ws.on('ping', () => { + logger.debug('received ping from acceleration websocket server'); + }); + + this.ws.on('pong', () => { + logger.debug('received pong from acceleration websocket server'); + this.lastPong = Date.now(); + }); + } else if (this.websocketConnected) { + if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) { + logger.warn('No pong received within 10 seconds, terminating connection'); + try { + this.ws?.terminate(); + } catch (e) { + logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e)); + } finally { + this.ws = null; + this.websocketConnected = false; + this.lastPing = 0; + } + } else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) { + logger.debug('sending ping to acceleration websocket server'); + if (this.ws?.readyState === WebSocket.OPEN) { + try { + this.ws?.ping(); + this.lastPing = Date.now(); + } catch (e) { + logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e)); + } + } + } + } + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } } export default new AccelerationApi(); \ No newline at end of file diff --git a/backend/src/api/services/services-routes.ts b/backend/src/api/services/services-routes.ts new file mode 100644 index 0000000000..422219d829 --- /dev/null +++ b/backend/src/api/services/services-routes.ts @@ -0,0 +1,41 @@ +import { Application, Request, Response } from 'express'; +import config from '../../config'; +import WalletApi from './wallets'; +import { handleError } from '../../utils/api'; + +class ServicesRoutes { + public initRoutes(app: Application): void { + app + .get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet) + .get(config.MEMPOOL.API_URL_PREFIX + 'treasuries', this.$getTreasuries) + ; + } + + private async $getWallet(req: Request, res: Response): Promise { + try { + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString()); + const walletId = req.params.walletId; + const wallet = await WalletApi.getWallet(walletId); + if (wallet === null) { + res.status(404).send('No such wallet'); + } else { + res.status(200).send(wallet); + } + } catch (e) { + handleError(req, res, 500, 'Failed to get wallet'); + } + } + + private async $getTreasuries(req: Request, res: Response): Promise { + try { + const treasuries = await WalletApi.getTreasuries(); + res.status(200).send(treasuries); + } catch (e) { + handleError(req, res, 500, 'Failed to get treasuries'); + } + } +} + +export default new ServicesRoutes(); diff --git a/backend/src/api/services/stratum.ts b/backend/src/api/services/stratum.ts new file mode 100644 index 0000000000..a8ee64106e --- /dev/null +++ b/backend/src/api/services/stratum.ts @@ -0,0 +1,105 @@ +import { WebSocket } from 'ws'; +import logger from '../../logger'; +import config from '../../config'; +import websocketHandler from '../websocket-handler'; + +export interface StratumJob { + pool: number; + height: number; + coinbase: string; + scriptsig: string; + reward: number; + jobId: string; + extraNonce: string; + extraNonce2Size: number; + prevHash: string; + coinbase1: string; + coinbase2: string; + merkleBranches: string[]; + version: string; + bits: string; + time: string; + timestamp: number; + cleanJobs: boolean; + received: number; +} + +function isStratumJob(obj: any): obj is StratumJob { + return obj + && typeof obj === 'object' + && 'pool' in obj + && 'prevHash' in obj + && 'height' in obj + && 'received' in obj + && 'version' in obj + && 'timestamp' in obj + && 'bits' in obj + && 'merkleBranches' in obj + && 'cleanJobs' in obj; +} + +class StratumApi { + private ws: WebSocket | null = null; + private runWebsocketLoop: boolean = false; + private startedWebsocketLoop: boolean = false; + private websocketConnected: boolean = false; + private jobs: Record = {}; + + public constructor() {} + + public getJobs(): Record { + return this.jobs; + } + + private handleWebsocketMessage(msg: any): void { + if (isStratumJob(msg)) { + this.jobs[msg.pool] = msg; + websocketHandler.handleNewStratumJob(this.jobs[msg.pool]); + } + } + + public async connectWebsocket(): Promise { + if (!config.STRATUM.ENABLED) { + return; + } + this.runWebsocketLoop = true; + if (this.startedWebsocketLoop) { + return; + } + while (this.runWebsocketLoop) { + this.startedWebsocketLoop = true; + if (!this.ws) { + this.ws = new WebSocket(`${config.STRATUM.API}`); + this.websocketConnected = true; + + this.ws.on('open', () => { + logger.info('Stratum websocket opened'); + }); + + this.ws.on('error', (error) => { + logger.err('Stratum websocket error: ' + error); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('close', () => { + logger.info('Stratum websocket closed'); + this.ws = null; + this.websocketConnected = false; + }); + + this.ws.on('message', (data, isBinary) => { + try { + const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string); + this.handleWebsocketMessage(parsedMsg); + } catch (e) { + logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e)); + } + }); + } + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } +} + +export default new StratumApi(); \ No newline at end of file diff --git a/backend/src/api/services/wallets.ts b/backend/src/api/services/wallets.ts new file mode 100644 index 0000000000..b7f81af923 --- /dev/null +++ b/backend/src/api/services/wallets.ts @@ -0,0 +1,308 @@ +import config from '../../config'; +import logger from '../../logger'; +import { IEsploraApi } from '../bitcoin/esplora-api.interface'; +import bitcoinApi from '../bitcoin/bitcoin-api-factory'; +import axios from 'axios'; +import { TransactionExtended } from '../../mempool.interfaces'; +import { promises as fsPromises } from 'fs'; + +interface WalletAddress { + address: string; + active: boolean; + stats: { + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + }; + transactions: IEsploraApi.AddressTxSummary[]; + lastSync: number; +} + +interface Wallet { + name: string; + addresses: Record; + lastPoll: number; +} + +interface Treasury { + id: number, + name: string, + wallet: string, + enterprise: string, +} + +const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes + +class WalletApi { + private treasuries: Treasury[] = []; + private wallets: Record = {}; + private syncing = false; + private lastSync = 0; + private isSaving = false; + private cacheSchemaVersion = 1; + + private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-wallets-cache.json'; + private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/wallets-cache.json'; + + constructor() { + this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => { + acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; + return acc; + }, {} as Record) : {}; + + // Load cache on startup + if (config.WALLETS.ENABLED) { + this.$loadCache(); + } + } + + private async $loadCache(): Promise { + try { + const cacheData = await fsPromises.readFile(WalletApi.FILE_NAME, 'utf8'); + if (!cacheData) { + return; + } + + const data = JSON.parse(cacheData); + + if (data.cacheSchemaVersion !== this.cacheSchemaVersion) { + logger.notice('Wallets cache contains an outdated schema version. Clearing it.'); + return this.$wipeCache(); + } + + this.wallets = data.wallets; + this.treasuries = data.treasuries || []; + + // Reset lastSync time to force transaction history refresh + for (const wallet of Object.values(this.wallets)) { + wallet.lastPoll = 0; + for (const address of Object.values(wallet.addresses)) { + address.lastSync = 0; + } + } + logger.info('Restored wallets data from disk cache'); + } catch (e) { + logger.warn('Failed to parse wallets cache. Skipping. Reason: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $saveCache(): Promise { + if (this.isSaving || !config.WALLETS.ENABLED) { + return; + } + + try { + this.isSaving = true; + logger.debug('Writing wallets data to disk cache...'); + + const cacheData = { + cacheSchemaVersion: this.cacheSchemaVersion, + wallets: this.wallets, + treasuries: this.treasuries, + }; + + await fsPromises.writeFile( + WalletApi.TMP_FILE_NAME, + JSON.stringify(cacheData), + { flag: 'w' } + ); + + await fsPromises.rename(WalletApi.TMP_FILE_NAME, WalletApi.FILE_NAME); + + logger.debug('Wallets data saved to disk cache'); + } catch (e) { + logger.warn('Error writing to wallets cache file: ' + (e instanceof Error ? e.message : e)); + } finally { + this.isSaving = false; + } + } + + private async $wipeCache(): Promise { + try { + await fsPromises.unlink(WalletApi.FILE_NAME); + } catch (e: any) { + if (e?.code !== 'ENOENT') { + logger.err(`Cannot wipe wallets cache file ${WalletApi.FILE_NAME}. Exception ${JSON.stringify(e)}`); + } + } + } + + public getWallet(wallet: string): Record | null { + if (wallet in this.wallets) { + return this.wallets?.[wallet]?.addresses || {}; + } else { + return null; + } + } + + public getWallets(): string[] { + return Object.keys(this.wallets); + } + + public getTreasuries(): Treasury[] { + return this.treasuries?.filter(treasury => !!this.wallets[treasury.wallet]) || []; + } + + // resync wallet addresses from the services backend + async $syncWallets(): Promise { + if (!config.WALLETS.ENABLED || this.syncing) { + return; + } + + this.syncing = true; + + if (config.WALLETS.AUTO && (Date.now() - this.lastSync) > POLL_FREQUENCY) { + try { + // update list of active wallets + this.lastSync = Date.now(); + const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets`); + const walletList: string[] = response.data; + if (walletList) { + // create a quick lookup dictionary of active wallets + const newWallets: Record = Object.fromEntries( + walletList.map(wallet => [wallet, true]) + ); + for (const wallet of walletList) { + // don't overwrite existing wallets + if (!this.wallets[wallet]) { + this.wallets[wallet] = { name: wallet, addresses: {}, lastPoll: 0 }; + } + } + // remove wallets that are no longer active + for (const wallet of Object.keys(this.wallets)) { + if (!newWallets[wallet]) { + delete this.wallets[wallet]; + } + } + } + + // update list of treasuries + const treasuriesResponse = await axios.get(config.MEMPOOL_SERVICES.API + `/treasuries`); + console.log(treasuriesResponse.data); + this.treasuries = treasuriesResponse.data || []; + } catch (e) { + logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`); + } + + try { + // update list of active treasuries + this.lastSync = Date.now(); + const response = await axios.get(config.MEMPOOL_SERVICES.API + `/treasuries`); + const treasuries: Treasury[] = response.data; + if (treasuries) { + this.treasuries = treasuries; + } + } catch (e) { + logger.err(`Error updating active treasuries: ${(e instanceof Error ? e.message : e)}`); + } + } + + for (const walletKey of Object.keys(this.wallets)) { + const wallet = this.wallets[walletKey]; + if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) { + try { + const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`); + const addresses: Record = response.data; + const addressList: WalletAddress[] = Object.values(addresses); + // sync all current addresses + for (const address of addressList) { + await this.$syncWalletAddress(wallet, address); + } + // remove old addresses + for (const address of Object.keys(wallet.addresses)) { + if (!addresses[address]) { + delete wallet.addresses[address]; + } + } + wallet.lastPoll = Date.now(); + logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`); + + // Update cache + await this.$saveCache(); + } catch (e) { + logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`); + } + } + } + + this.syncing = false; + } + + // resync address transactions from esplora + async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise { + // fetch full transaction data if the address is new or still active and hasn't been synced in the last hour + const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000); + if (refreshTransactions) { + try { + const summary = await bitcoinApi.$getAddressTransactionSummary(address.address); + const addressInfo = await bitcoinApi.$getAddress(address.address); + const walletAddress: WalletAddress = { + address: address.address, + active: address.active, + transactions: summary, + stats: addressInfo.chain_stats, + lastSync: Date.now(), + }; + wallet.addresses[address.address] = walletAddress; + } catch (e) { + logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`); + } + } + } + + // check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets + processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record { + const walletTransactions: Record = {}; + for (const walletKey of Object.keys(this.wallets)) { + const wallet = this.wallets[walletKey]; + walletTransactions[walletKey] = []; + for (const tx of blockTxs) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + let anyMatch = false; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet.addresses[address]) { + anyMatch = true; + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet.addresses[address]) { + anyMatch = true; + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // update address stats + wallet.addresses[address].stats.tx_count++; + wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0; + wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0; + wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0; + wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0; + // add tx to summary + const txSummary: IEsploraApi.AddressTxSummary = { + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: block.height, + time: block.timestamp, + }; + wallet.addresses[address].transactions?.push(txSummary); + } + if (anyMatch) { + walletTransactions[walletKey].push(tx); + } + } + } + return walletTransactions; + } +} + +export default new WalletApi(); \ No newline at end of file diff --git a/backend/src/api/statistics/statistics-api.ts b/backend/src/api/statistics/statistics-api.ts index 2d66d69d9a..fa13b60b95 100644 --- a/backend/src/api/statistics/statistics-api.ts +++ b/backend/src/api/statistics/statistics-api.ts @@ -16,6 +16,7 @@ class StatisticsApi { fee_data, total_fee, min_fee, + vsize_0, vsize_1, vsize_2, vsize_3, @@ -55,7 +56,7 @@ class StatisticsApi { vsize_1800, vsize_2000 ) - VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + VALUES (NOW(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)`; const [result]: any = await DB.query(query); return result.insertId; @@ -75,6 +76,7 @@ class StatisticsApi { fee_data, total_fee, min_fee, + vsize_0, vsize_1, vsize_2, vsize_3, @@ -115,7 +117,7 @@ class StatisticsApi { vsize_2000 ) VALUES (${convertToDatetime ? `FROM_UNIXTIME(${statistics.added})` : statistics.added}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const params: (string | number)[] = [ statistics.unconfirmed_transactions, @@ -125,6 +127,7 @@ class StatisticsApi { statistics.fee_data, statistics.total_fee, statistics.min_fee, + statistics.vsize_0, statistics.vsize_1, statistics.vsize_2, statistics.vsize_3, @@ -177,6 +180,7 @@ class StatisticsApi { CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions, CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second, CAST(avg(min_fee) as DOUBLE) as min_fee, + CAST(avg(vsize_0) as DOUBLE) as vsize_0, CAST(avg(vsize_1) as DOUBLE) as vsize_1, CAST(avg(vsize_2) as DOUBLE) as vsize_2, CAST(avg(vsize_3) as DOUBLE) as vsize_3, @@ -227,6 +231,7 @@ class StatisticsApi { CAST(avg(unconfirmed_transactions) as DOUBLE) as unconfirmed_transactions, CAST(avg(vbytes_per_second) as DOUBLE) as vbytes_per_second, CAST(avg(min_fee) as DOUBLE) as min_fee, + vsize_0, vsize_1, vsize_2, vsize_3, @@ -414,6 +419,7 @@ class StatisticsApi { total_fee: s.total_fee, min_fee: s.min_fee, vsizes: [ + s.vsize_0, s.vsize_1, s.vsize_2, s.vsize_3, @@ -459,6 +465,7 @@ class StatisticsApi { public mapOptimizedStatisticToStatistic(statistic: OptimizedStatistic[]): Statistic[] { return statistic.map((s) => { + const completeVsizes = s.vsizes.length === 37 ? [0, ...s.vsizes] : s.vsizes; return { added: s.added, unconfirmed_transactions: s.count, @@ -468,44 +475,45 @@ class StatisticsApi { total_fee: s.total_fee || 0, min_fee: s.min_fee, fee_data: '', - vsize_1: s.vsizes[0], - vsize_2: s.vsizes[1], - vsize_3: s.vsizes[2], - vsize_4: s.vsizes[3], - vsize_5: s.vsizes[4], - vsize_6: s.vsizes[5], - vsize_8: s.vsizes[6], - vsize_10: s.vsizes[7], - vsize_12: s.vsizes[8], - vsize_15: s.vsizes[9], - vsize_20: s.vsizes[10], - vsize_30: s.vsizes[11], - vsize_40: s.vsizes[12], - vsize_50: s.vsizes[13], - vsize_60: s.vsizes[14], - vsize_70: s.vsizes[15], - vsize_80: s.vsizes[16], - vsize_90: s.vsizes[17], - vsize_100: s.vsizes[18], - vsize_125: s.vsizes[19], - vsize_150: s.vsizes[20], - vsize_175: s.vsizes[21], - vsize_200: s.vsizes[22], - vsize_250: s.vsizes[23], - vsize_300: s.vsizes[24], - vsize_350: s.vsizes[25], - vsize_400: s.vsizes[26], - vsize_500: s.vsizes[27], - vsize_600: s.vsizes[28], - vsize_700: s.vsizes[29], - vsize_800: s.vsizes[30], - vsize_900: s.vsizes[31], - vsize_1000: s.vsizes[32], - vsize_1200: s.vsizes[33], - vsize_1400: s.vsizes[34], - vsize_1600: s.vsizes[35], - vsize_1800: s.vsizes[36], - vsize_2000: s.vsizes[37], + vsize_0: completeVsizes[0], + vsize_1: completeVsizes[1], + vsize_2: completeVsizes[2], + vsize_3: completeVsizes[3], + vsize_4: completeVsizes[4], + vsize_5: completeVsizes[5], + vsize_6: completeVsizes[6], + vsize_8: completeVsizes[7], + vsize_10: completeVsizes[8], + vsize_12: completeVsizes[9], + vsize_15: completeVsizes[10], + vsize_20: completeVsizes[11], + vsize_30: completeVsizes[12], + vsize_40: completeVsizes[13], + vsize_50: completeVsizes[14], + vsize_60: completeVsizes[15], + vsize_70: completeVsizes[16], + vsize_80: completeVsizes[17], + vsize_90: completeVsizes[18], + vsize_100: completeVsizes[19], + vsize_125: completeVsizes[20], + vsize_150: completeVsizes[21], + vsize_175: completeVsizes[22], + vsize_200: completeVsizes[23], + vsize_250: completeVsizes[24], + vsize_300: completeVsizes[25], + vsize_350: completeVsizes[26], + vsize_400: completeVsizes[27], + vsize_500: completeVsizes[28], + vsize_600: completeVsizes[29], + vsize_700: completeVsizes[30], + vsize_800: completeVsizes[31], + vsize_900: completeVsizes[32], + vsize_1000: completeVsizes[33], + vsize_1200: completeVsizes[34], + vsize_1400: completeVsizes[35], + vsize_1600: completeVsizes[36], + vsize_1800: completeVsizes[37], + vsize_2000: completeVsizes[38], } }); } diff --git a/backend/src/api/statistics/statistics.routes.ts b/backend/src/api/statistics/statistics.routes.ts index 31db5198c9..ec05bf0322 100644 --- a/backend/src/api/statistics/statistics.routes.ts +++ b/backend/src/api/statistics/statistics.routes.ts @@ -1,7 +1,7 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import statisticsApi from './statistics-api'; - +import { handleError } from '../../utils/api'; class StatisticsRoutes { public initRoutes(app: Application) { app @@ -65,7 +65,7 @@ class StatisticsRoutes { } res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, 'Failed to get statistics'); } } } diff --git a/backend/src/api/statistics/statistics.ts b/backend/src/api/statistics/statistics.ts index 2926a4b173..62dff3d664 100644 --- a/backend/src/api/statistics/statistics.ts +++ b/backend/src/api/statistics/statistics.ts @@ -73,7 +73,7 @@ class Statistics { const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4; const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr); - const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, + const logFees = [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; const weightVsizeFees: { [feePerWU: number]: number } = {}; @@ -109,6 +109,7 @@ class Statistics { total_fee: totalFee, fee_data: '', min_fee: minFee, + vsize_0: weightVsizeFees['0'] || 0, vsize_1: weightVsizeFees['1'] || 0, vsize_2: weightVsizeFees['2'] || 0, vsize_3: weightVsizeFees['3'] || 0, diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index b3077b935e..519527d5ce 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -121,6 +121,7 @@ class TransactionUtils { const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor const feePerVbytes = (transaction.fee || 0) / fractionalVsize; const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize; + const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes; const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, { order: this.txidToOrdering(transaction.txid), vsize, @@ -128,7 +129,7 @@ class TransactionUtils { sigops, feePerVsize: feePerVbytes, adjustedFeePerVsize: adjustedFeePerVsize, - effectiveFeePerVsize: adjustedFeePerVsize, + effectiveFeePerVsize: effectiveFeePerVsize, }); if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) { transactionExtended.firstSeen = Math.round((Date.now() / 1000)); @@ -338,6 +339,110 @@ class TransactionUtils { const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; return witness[positionOfScript]; } + + // calculate the most parsimonious set of prioritizations given a list of block transactions + // (i.e. the most likely prioritizations and deprioritizations) + public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } { + // find the longest increasing subsequence of transactions + // (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms) + // should be O(n log n) + const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase) + if (X.length < 2) { + return { prioritized: [], deprioritized: [] }; + } + const N = X.length; + const P: number[] = new Array(N); + const M: number[] = new Array(N + 1); + M[0] = -1; // undefined so can be set to any value + + let L = 0; + for (let i = 0; i < N; i++) { + // Binary search for the smallest positive l ≤ L + // such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize + let lo = 1; + let hi = L + 1; + while (lo < hi) { + const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi + if (X[M[mid]].rate > X[i].rate) { + hi = mid; + } else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize + lo = mid + 1; + } + } + + // After searching, lo == hi is 1 greater than the + // length of the longest prefix of X[i] + const newL = lo; + + // The predecessor of X[i] is the last index of + // the subsequence of length newL-1 + P[i] = M[newL - 1]; + M[newL] = i; + + if (newL > L) { + // If we found a subsequence longer than any we've + // found yet, update L + L = newL; + } + } + + // Reconstruct the longest increasing subsequence + // It consists of the values of X at the L indices: + // ..., P[P[M[L]]], P[M[L]], M[L] + const LIS: any[] = new Array(L); + let k = M[L]; + for (let j = L - 1; j >= 0; j--) { + LIS[j] = X[k]; + k = P[k]; + } + + const lisMap = new Map(); + LIS.forEach((tx, index) => lisMap.set(tx.txid, index)); + + const prioritized: string[] = []; + const deprioritized: string[] = []; + + let lastRate = X[0].rate; + + for (const tx of X) { + if (lisMap.has(tx.txid)) { + lastRate = tx.rate; + } else { + if (Math.abs(tx.rate - lastRate) < 0.1) { + // skip if the rate is almost the same as the previous transaction + } else if (tx.rate <= lastRate) { + prioritized.push(tx.txid); + } else { + deprioritized.push(tx.txid); + } + } + } + + return { prioritized, deprioritized }; + } + + // Copied from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/api/bitcoin/bitcoin-api.ts#L324 + public translateScriptPubKeyType(outputType: string): string { + const map = { + 'pubkey': 'p2pk', + 'pubkeyhash': 'p2pkh', + 'scripthash': 'p2sh', + 'witness_v0_keyhash': 'v0_p2wpkh', + 'witness_v0_scripthash': 'v0_p2wsh', + 'witness_v1_taproot': 'v1_p2tr', + 'nonstandard': 'nonstandard', + 'multisig': 'multisig', + 'anchor': 'anchor', + 'nulldata': 'op_return' + }; + + if (map[outputType]) { + return map[outputType]; + } else { + return 'unknown'; + } + } + } export default new TransactionUtils(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 79a783f882..09e56630ae 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -16,16 +16,19 @@ import transactionUtils from './transaction-utils'; import rbfCache, { ReplacementInfo } from './rbf-cache'; import difficultyAdjustment from './difficulty-adjustment'; import feeApi from './fee-api'; +import BlocksRepository from '../repositories/BlocksRepository'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import Audit from './audit'; import priceUpdater from '../tasks/price-updater'; import { ApiPrice } from '../repositories/PricesRepository'; +import { Acceleration } from './services/acceleration'; import accelerationApi from './services/acceleration'; import mempool from './mempool'; import statistics from './statistics/statistics'; import accelerationRepository from '../repositories/AccelerationRepository'; import bitcoinApi from './bitcoin/bitcoin-api-factory'; +import walletApi from './services/wallets'; interface AddressTransactions { mempool: MempoolTransactionExtended[], @@ -34,6 +37,8 @@ interface AddressTransactions { } import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import { calculateMempoolTxCpfp } from './cpfp'; +import { getRecentFirstSeen } from '../utils/file-read'; +import stratumApi, { StratumJob } from './services/stratum'; // valid 'want' subscriptions const wantable = [ @@ -57,6 +62,8 @@ class WebsocketHandler { private lastRbfSummary: ReplacementInfo[] | null = null; private mempoolSequence: number = 0; + private accelerations: Record = {}; + constructor() { } addWebsocketServer(wss: WebSocket.Server) { @@ -305,6 +312,14 @@ class WebsocketHandler { } } + if (parsedMessage && parsedMessage['track-wallet']) { + if (parsedMessage['track-wallet'] === 'stop') { + client['track-wallet'] = null; + } else { + client['track-wallet'] = parsedMessage['track-wallet']; + } + } + if (parsedMessage && parsedMessage['track-asset']) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { client['track-asset'] = parsedMessage['track-asset']; @@ -389,6 +404,16 @@ class WebsocketHandler { delete client['track-mempool']; } + if (parsedMessage && parsedMessage['track-stratum'] != null) { + if (parsedMessage['track-stratum']) { + const sub = parsedMessage['track-stratum']; + client['track-stratum'] = sub; + response['stratumJobs'] = this.socketData['stratumJobs']; + } else { + client['track-stratum'] = false; + } + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } @@ -484,6 +509,42 @@ class WebsocketHandler { } } + handleAccelerationsChanged(accelerations: Record): void { + if (!this.webSocketServers.length) { + throw new Error('No WebSocket.Server has been set'); + } + + const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations); + this.accelerations = accelerations; + + if (!websocketAccelerationDelta.length) { + return; + } + + // pre-compute acceleration delta + const accelerationUpdate = { + added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), + removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]), + }; + + try { + const response = JSON.stringify({ + accelerations: accelerationUpdate, + }); + + for (const server of this.webSocketServers) { + server.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) { + return; + } + client.send(response); + }); + } + } catch (e) { + logger.debug(`Error sending acceleration update to websocket clients: ${e}`); + } + } + handleReorg(): void { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -520,8 +581,17 @@ class WebsocketHandler { } } + /** + * + * @param newMempool + * @param mempoolSize + * @param newTransactions array of transactions added this mempool update. + * @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first. + * @param accelerationDelta + * @param candidates + */ async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates): Promise { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -529,6 +599,8 @@ class WebsocketHandler { this.printLogs(); + const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : []; + const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); let added = newTransactions; let removed = deletedTransactions; @@ -547,9 +619,9 @@ class WebsocketHandler { const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mempoolInfo = memPool.getMempoolInfo(); const vBytesPerSecond = memPool.getVBytesPerSecond(); - const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); + const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat()); const da = difficultyAdjustment.getDifficultyAdjustment(); - const accelerations = memPool.getAccelerations(); + const accelerations = accelerationApi.getAccelerations(); memPool.handleRbfTransactions(rbfTransactions); const rbfChanges = rbfCache.getRbfChanges(); let rbfReplacements; @@ -578,7 +650,7 @@ class WebsocketHandler { const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; for (const tx of newTransactions) { if (rbfTransactions[tx.txid]) { - for (const replaced of rbfTransactions[tx.txid]) { + for (const replaced of rbfTransactions[tx.txid].replaced) { replacedTransactions.push({ replaced: replaced.txid, by: tx }); } } @@ -657,10 +729,13 @@ class WebsocketHandler { const addressCache = this.makeAddressCache(newTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions); + const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations); + this.accelerations = accelerations; + // pre-compute acceleration delta const accelerationUpdate = { - added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), - removed: accelerationDelta.filter(txid => !accelerations[txid]), + added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), + removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]), }; // TODO - Fix indentation after PR is merged @@ -936,18 +1011,22 @@ class WebsocketHandler { const blockTransactions = structuredClone(transactions); this.printLogs(); - await statistics.runStatistics(); + if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { + await statistics.runStatistics(); + } const _memPool = memPool.getMempool(); const candidateTxs = await memPool.getMempoolCandidates(); let candidates: GbtCandidates | undefined = (memPool.limitGBT && candidateTxs) ? { txs: candidateTxs, added: [], removed: [] } : undefined; let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool); - const accelerations = Object.values(mempool.getAccelerations()); - await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); + if (config.DATABASE.ENABLED) { + const accelerations = Object.values(mempool.getAccelerations()); + await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); + } const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); - memPool.handleMinedRbfTransactions(rbfTransactions); + memPool.handleRbfTransactions(rbfTransactions); memPool.removeFromSpendMap(transactions); if (config.MEMPOOL.AUDIT && memPool.isInSync()) { @@ -1017,6 +1096,16 @@ class WebsocketHandler { } } + if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) { + const firstSeen = getRecentFirstSeen(block.id); + if (firstSeen) { + if (config.DATABASE.ENABLED) { + BlocksRepository.$saveFirstSeenTime(block.id, firstSeen); + } + block.extras.firstSeen = firstSeen; + } + } + const confirmedTxids: { [txid: string]: boolean } = {}; // Update mempool to remove transactions included in the new block @@ -1091,6 +1180,9 @@ class WebsocketHandler { replaced: replacedTransactions, }; + // check for wallet transactions + const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : []; + const responseCache = { ...this.socketData }; function getCachedResponse(key, data): string { if (!responseCache[key]) { @@ -1295,13 +1387,37 @@ class WebsocketHandler { response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); } + if (client['track-wallet']) { + const trackedWallet = client['track-wallet']; + response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {}); + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } }); } - await statistics.runStatistics(); + if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) { + await statistics.runStatistics(); + } + } + + public handleNewStratumJob(job: StratumJob): void { + this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() }); + + for (const server of this.webSocketServers) { + server.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) { + return; + } + if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) { + client.send(JSON.stringify({ + 'stratumJob': job + })); + } + }); + } } // takes a dictionary of JSON serialized values diff --git a/backend/src/config.ts b/backend/src/config.ts index b0afe7f23c..3fe3db2ee2 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -32,6 +32,7 @@ interface IConfig { AUTOMATIC_POOLS_UPDATE: boolean; POOLS_JSON_URL: string, POOLS_JSON_TREE_URL: string, + POOLS_UPDATE_DELAY: number, AUDIT: boolean; RUST_GBT: boolean; LIMIT_GBT: boolean; @@ -85,6 +86,7 @@ interface IConfig { TIMEOUT: number; COOKIE: boolean; COOKIE_PATH: string; + DEBUG_LOG_PATH: string; }; SECOND_CORE_RPC: { HOST: string; @@ -160,6 +162,15 @@ interface IConfig { PAID: boolean; API_KEY: string; }, + WALLETS: { + ENABLED: boolean; + AUTO: boolean; + WALLETS: string[]; + }, + STRATUM: { + ENABLED: boolean; + API: string; + } } const defaults: IConfig = { @@ -192,8 +203,9 @@ const defaults: IConfig = { 'AUTOMATIC_POOLS_UPDATE': false, 'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', + 'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week 'AUDIT': false, - 'RUST_GBT': false, + 'RUST_GBT': true, 'LIMIT_GBT': false, 'CPFP_INDEXING': false, 'MAX_BLOCKS_BULK_QUERY': 0, @@ -225,7 +237,8 @@ const defaults: IConfig = { 'PASSWORD': 'mempool', 'TIMEOUT': 60000, 'COOKIE': false, - 'COOKIE_PATH': '/bitcoin/.cookie' + 'COOKIE_PATH': '/bitcoin/.cookie', + 'DEBUG_LOG_PATH': '', }, 'SECOND_CORE_RPC': { 'HOST': '127.0.0.1', @@ -320,6 +333,15 @@ const defaults: IConfig = { 'PAID': false, 'API_KEY': '', }, + 'WALLETS': { + 'ENABLED': false, + 'AUTO': false, + 'WALLETS': [], + }, + 'STRATUM': { + 'ENABLED': false, + 'API': 'http://localhost:1234', + } }; class Config implements IConfig { @@ -341,6 +363,8 @@ class Config implements IConfig { MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES']; REDIS: IConfig['REDIS']; FIAT_PRICE: IConfig['FIAT_PRICE']; + WALLETS: IConfig['WALLETS']; + STRATUM: IConfig['STRATUM']; constructor() { const configs = this.merge(configFromFile, defaults); @@ -362,6 +386,8 @@ class Config implements IConfig { this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES; this.REDIS = configs.REDIS; this.FIAT_PRICE = configs.FIAT_PRICE; + this.WALLETS = configs.WALLETS; + this.STRATUM = configs.STRATUM; } merge = (...objects: object[]): IConfig => { diff --git a/backend/src/index.ts b/backend/src/index.ts index 1d83c56a39..b478115e0c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes'; import miningRoutes from './api/mining/mining-routes'; import liquidRoutes from './api/liquid/liquid.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; +import servicesRoutes from './api/services/services-routes'; import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher'; import forensicsService from './tasks/lightning/forensics.service'; import priceUpdater from './tasks/price-updater'; @@ -46,6 +47,8 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client'; import accelerationRoutes from './api/acceleration/acceleration.routes'; import aboutRoutes from './api/about.routes'; import mempoolBlocks from './api/mempool-blocks'; +import walletApi from './api/services/wallets'; +import stratumApi from './api/services/stratum'; class Server { private wss: WebSocket.Server | undefined; @@ -128,6 +131,9 @@ class Server { this.app .use((req: Request, res: Response, next: NextFunction) => { res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'); + res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count,X-Mempool-Auth'); next(); }) .use(express.urlencoded({ extended: true })) @@ -149,8 +155,15 @@ class Server { this.setUpWebsocketHandling(); await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it + if (config.DATABASE.ENABLED === true && config.MEMPOOL.ENABLED && ['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && !poolsUpdater.currentSha) { + logger.err(`Failed to retreive pools-v2.json sha, cannot run block indexing. Please make sure you've set valid urls in your mempool-config.json::MEMPOOL::POOLS_JSON_URL and mempool-config.json::MEMPOOL::POOLS_JSON_TREE_UR, aborting now`); + return process.exit(1); + } + await syncAssets.syncAssets$(); - await mempoolBlocks.updatePools$(); + if (config.DATABASE.ENABLED) { + await mempoolBlocks.updatePools$(); + } if (config.MEMPOOL.ENABLED) { if (config.MEMPOOL.CACHE_ENABLED) { await diskCache.$loadMempoolCache(); @@ -211,6 +224,8 @@ class Server { } }); } + + poolsUpdater.$startService(); } async runMainUpdateLoop(): Promise { @@ -229,13 +244,17 @@ class Server { const newMempool = await bitcoinApi.$getRawMempool(); const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; - const newAccelerations = await accelerationApi.$updateAccelerations(); + const latestAccelerations = await accelerationApi.$updateAccelerations(); const numHandledBlocks = await blocks.$updateBlocks(); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); if (numHandledBlocks === 0) { - await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate); + await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate); } indexer.$run(); + if (config.WALLETS.ENABLED) { + // might take a while, so run in the background + walletApi.$syncWallets(); + } if (config.FIAT_PRICE.ENABLED) { priceUpdater.$run(); } @@ -310,11 +329,18 @@ class Server { priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler)); } loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); + + accelerationApi.connectWebsocket(); + if (config.STRATUM.ENABLED) { + stratumApi.connectWebsocket(); + } } - + setUpHttpApiRoutes(): void { bitcoinRoutes.initRoutes(this.app); - bitcoinCoreRoutes.initRoutes(this.app); + if (config.MEMPOOL.OFFICIAL) { + bitcoinCoreRoutes.initRoutes(this.app); + } pricesRoutes.initRoutes(this.app); if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { statisticsRoutes.initRoutes(this.app); @@ -333,6 +359,9 @@ class Server { if (config.MEMPOOL_SERVICES.ACCELERATIONS) { accelerationRoutes.initRoutes(this.app); } + if (config.WALLETS.ENABLED) { + servicesRoutes.initRoutes(this.app); + } if (!config.MEMPOOL.OFFICIAL) { aboutRoutes.initRoutes(this.app); } diff --git a/backend/src/indexer.ts b/backend/src/indexer.ts index dfd7f13175..d46fb14bc6 100644 --- a/backend/src/indexer.ts +++ b/backend/src/indexer.ts @@ -11,6 +11,7 @@ import auditReplicator from './replication/AuditReplication'; import statisticsReplicator from './replication/StatisticsReplication'; import AccelerationRepository from './repositories/AccelerationRepository'; import BlocksAuditsRepository from './repositories/BlocksAuditsRepository'; +import BlocksRepository from './repositories/BlocksRepository'; export interface CoreIndex { name: string; @@ -194,6 +195,7 @@ class Indexer { await statisticsReplicator.$sync(); await AccelerationRepository.$indexPastAccelerations(); await BlocksAuditsRepository.$migrateAuditsV0toV1(); + await BlocksRepository.$migrateBlocks(); // do not wait for classify blocks to finish blocks.$classifyBlocks(); } catch (e) { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index ccbc94bfa8..0dfce6b1f4 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -274,6 +274,7 @@ export const TransactionFlags = { fake_pubkey: 0b00000010_00000000_00000000_00000000n, inscription: 0b00000100_00000000_00000000_00000000n, fake_scripthash: 0b00001000_00000000_00000000_00000000n, + annex: 0b00010000_00000000_00000000_00000000n, // heuristics coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, consolidation: 0b00000010_00000000_00000000_00000000_00000000n, @@ -299,6 +300,7 @@ export interface BlockExtension { id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` name: string; slug: string; + minerNames: string[] | null; }; avgFee: number; avgFeeRate: number; @@ -319,10 +321,13 @@ export interface BlockExtension { segwitTotalSize: number; segwitTotalWeight: number; header: string; + firstSeen: number | null; utxoSetChange: number; // Requires coinstatsindex, will be set to NULL otherwise utxoSetSize: number | null; totalInputAmt: number | null; + // pools-v2.json git hash + definitionHash: string | undefined; } /** @@ -332,6 +337,7 @@ export interface BlockExtension { export interface BlockExtended extends IEsploraApi.Block { extras: BlockExtension; canonical?: string; + indexVersion?: number; } export interface BlockSummary { @@ -401,6 +407,7 @@ export interface Statistic { fee_data: string; min_fee: number; + vsize_0: number; vsize_1: number; vsize_2: number; vsize_3: number; diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 90100a7674..309cff2cea 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -14,8 +14,11 @@ import chainTips from '../api/chain-tips'; import blocks from '../api/blocks'; import BlocksAuditsRepository from './BlocksAuditsRepository'; import transactionUtils from '../api/transaction-utils'; +import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; +import poolsUpdater from '../tasks/pools-updater'; interface DatabaseBlock { + index_version: number; id: string; height: number; version: number; @@ -56,9 +59,11 @@ interface DatabaseBlock { utxoSetChange: number; utxoSetSize: number; totalInputAmt: number; + firstSeen: number; } const BLOCK_DB_FIELDS = ` + blocks.index_version AS indexVersion, blocks.hash AS id, blocks.height, blocks.version, @@ -98,10 +103,13 @@ const BLOCK_DB_FIELDS = ` blocks.header, blocks.utxoset_change AS utxoSetChange, blocks.utxoset_size AS utxoSetSize, - blocks.total_input_amt AS totalInputAmt + blocks.total_input_amt AS totalInputAmt, + UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen `; class BlocksRepository { + static version = 1; + /** * Save indexed block data in the database */ @@ -111,16 +119,16 @@ class BlocksRepository { try { const query = `INSERT INTO blocks( - height, hash, blockTimestamp, size, - weight, tx_count, coinbase_raw, difficulty, - pool_id, fees, fee_span, median_fee, - reward, version, bits, nonce, - merkle_root, previous_block_hash, avg_fee, avg_fee_rate, - median_timestamp, header, coinbase_address, coinbase_addresses, - coinbase_signature, utxoset_size, utxoset_change, avg_tx_size, - total_inputs, total_outputs, total_input_amt, total_output_amt, - fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight, - median_fee_amt, coinbase_signature_ascii + height, hash, blockTimestamp, size, + weight, tx_count, coinbase_raw, difficulty, + pool_id, fees, fee_span, median_fee, + reward, version, bits, nonce, + merkle_root, previous_block_hash, avg_fee, avg_fee_rate, + median_timestamp, header, coinbase_address, coinbase_addresses, + coinbase_signature, utxoset_size, utxoset_change, avg_tx_size, + total_inputs, total_outputs, total_input_amt, total_output_amt, + fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight, + median_fee_amt, coinbase_signature_ascii, definition_hash, index_version ) VALUE ( ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, @@ -131,7 +139,7 @@ class BlocksRepository { ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, - ?, ? + ?, ?, ?, ? )`; const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id); @@ -178,6 +186,8 @@ class BlocksRepository { block.extras.segwitTotalWeight, block.extras.medianFeeAmt, truncatedCoinbaseSignatureAscii, + poolsUpdater.currentSha, + BlocksRepository.version ]; await DB.query(query, params); @@ -498,7 +508,7 @@ class BlocksRepository { } query += ` ORDER BY height DESC - LIMIT 10`; + LIMIT 100`; try { const [rows]: any[] = await DB.query(query, params); @@ -877,7 +887,9 @@ class BlocksRepository { SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.height FROM blocks LEFT JOIN blocks_prices ON blocks.height = blocks_prices.height + LEFT JOIN prices ON blocks_prices.price_id = prices.id WHERE blocks_prices.height IS NULL + OR prices.id IS NULL ORDER BY blocks.height `); return rows; @@ -897,6 +909,7 @@ class BlocksRepository { query += ` (${price.height}, ${price.priceId}),`; } query = query.slice(0, -1); + query += ` ON DUPLICATE KEY UPDATE price_id = VALUES(price_id)`; await DB.query(query); } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart @@ -1010,9 +1023,9 @@ class BlocksRepository { public async $savePool(id: string, poolId: number): Promise { try { await DB.query(` - UPDATE blocks SET pool_id = ? + UPDATE blocks SET pool_id = ?, definition_hash = ? WHERE hash = ?`, - [poolId, id] + [poolId, poolsUpdater.currentSha, id] ); } catch (e) { logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e)); @@ -1020,6 +1033,24 @@ class BlocksRepository { } } + /** + * Save block first seen time + * + * @param id + */ + public async $saveFirstSeenTime(id: string, firstSeen: number): Promise { + try { + await DB.query(` + UPDATE blocks SET first_seen = FROM_UNIXTIME(?) + WHERE hash = ?`, + [firstSeen, id] + ); + } catch (e) { + logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } + /** * Convert a mysql row block into a BlockExtended. Note that you * must provide the correct field into dbBlk object param @@ -1044,7 +1075,7 @@ class BlocksRepository { blk.weight = dbBlk.weight; blk.previousblockhash = dbBlk.previousblockhash; blk.mediantime = dbBlk.mediantime; - + blk.indexVersion = dbBlk.index_version; // BlockExtension extras.totalFees = dbBlk.totalFees; extras.medianFee = dbBlk.medianFee; @@ -1054,6 +1085,7 @@ class BlocksRepository { id: dbBlk.poolId, name: dbBlk.poolName, slug: dbBlk.poolSlug, + minerNames: null, }; extras.avgFee = dbBlk.avgFee; extras.avgFeeRate = dbBlk.avgFeeRate; @@ -1076,6 +1108,7 @@ class BlocksRepository { extras.utxoSetSize = dbBlk.utxoSetSize; extras.totalInputAmt = dbBlk.totalInputAmt; extras.virtualSize = dbBlk.weight / 4.0; + extras.firstSeen = dbBlk.firstSeen; // Re-org can happen after indexing so we need to always get the // latest state from core @@ -1106,7 +1139,7 @@ class BlocksRepository { let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); - summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); + summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1123,9 +1156,81 @@ class BlocksRepository { } } + if (extras.pool.name === 'OCEAN') { + extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); + } + blk.extras = extras; return blk; } + + // Execute reindexing tasks & lazy schema migrations + public async $migrateBlocks(): Promise { + let blocksMigrated = 0; + blocksMigrated = await this.$migrateBlocksToV1(); + if (blocksMigrated > 0) { + // return early, run the next migration on the next indexing loop + return blocksMigrated; + } + return 0; + } + + // migration to fix median fee bug + private async $migrateBlocksToV1(): Promise { + let blocksMigrated = 0; + try { + // median fee bug only affects mmre-than-half but less-than-completely full blocks + const minWeight = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2 - (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800); + const maxWeight = config.MEMPOOL.BLOCK_WEIGHT_UNITS - (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 400); + const [rows]: any[] = await DB.query(` + SELECT height, hash, index_version + FROM blocks + WHERE index_version < 1 + AND weight >= ? + AND weight <= ? + ORDER BY height DESC + `, [minWeight, maxWeight]); + const blocksToMigrate = rows.length; + + let timer = Date.now() / 1000; + const startedAt = Date.now() / 1000; + + for (const row of rows) { + // fetch block summary + const transactions = await blocks.$getStrippedBlockTransactions(row.hash); + // recalculate effective fee statistics using latest methodology + const feeStats = Common.calcEffectiveFeeStatistics(transactions.map(tx => ({ + weight: tx.vsize * 4, + effectiveFeePerVsize: tx.rate, + txid: tx.txid, + acceleration: tx.acc, + }))); + + // update block db + await DB.query(` + UPDATE blocks SET index_version = 1, median_fee = ?, fee_span = ? + WHERE hash = ?`, + [feeStats.medianFee, JSON.stringify(feeStats.feeRange), row.hash] + ); + + const elapsedSeconds = (Date.now() / 1000) - timer; + if (elapsedSeconds > 5) { + const runningFor = (Date.now() / 1000) - startedAt; + const blockPerSeconds = blocksMigrated / elapsedSeconds; + const progress = Math.round(blocksMigrated / blocksToMigrate * 10000) / 100; + logger.debug(`Migrating blocks to version 1 | ~${blockPerSeconds.toFixed(2)} blocks/sec | height: ${row.height} | total: ${blocksMigrated}/${blocksToMigrate} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`); + timer = Date.now() / 1000; + } + + blocksMigrated++; + } + logger.notice(`Migrating blocks to version 1 completed: migrated ${blocksMigrated} blocks`); + } catch (e) { + logger.err(`Migrating blocks to version 1 failed. Trying again later. Reason: ${(e instanceof Error ? e.message : e)}`); + throw e; + } + return blocksMigrated; + } } export default new BlocksRepository(); diff --git a/backend/src/repositories/HashratesRepository.ts b/backend/src/repositories/HashratesRepository.ts index ec44afebe7..93aa2d53fb 100644 --- a/backend/src/repositories/HashratesRepository.ts +++ b/backend/src/repositories/HashratesRepository.ts @@ -93,7 +93,7 @@ class HashratesRepository { const [rows]: any[] = await DB.query(query); return rows.map(row => row.timestamp); } catch (e) { - logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot retrieve indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); throw e; } } diff --git a/backend/src/repositories/PricesRepository.ts b/backend/src/repositories/PricesRepository.ts index 13392f0cf4..e12027a746 100644 --- a/backend/src/repositories/PricesRepository.ts +++ b/backend/src/repositories/PricesRepository.ts @@ -296,6 +296,7 @@ class PricesRepository { id, USD FROM prices + WHERE USD >= 0 ORDER BY time `); return times as {time: number, id: number, USD: number}[]; @@ -305,6 +306,7 @@ class PricesRepository { const [rates] = await DB.query(` SELECT ${ApiPriceFields} FROM prices + WHERE USD >= 0 ORDER BY time DESC LIMIT 1` ); @@ -321,6 +323,7 @@ class PricesRepository { SELECT ${ApiPriceFields} FROM prices WHERE UNIX_TIMESTAMP(time) < ? + AND USD >= 0 ORDER BY time DESC LIMIT 1`, [timestamp] @@ -332,6 +335,7 @@ class PricesRepository { const [latestPrices] = await DB.query(` SELECT ${ApiPriceFields} FROM prices + WHERE USD >= 0 ORDER BY time DESC LIMIT 1 `); @@ -429,6 +433,7 @@ class PricesRepository { const [rates] = await DB.query(` SELECT ${ApiPriceFields} FROM prices + WHERE USD >= 0 ORDER BY time DESC `); if (!Array.isArray(rates)) { diff --git a/backend/src/rpc-api/commands.ts b/backend/src/rpc-api/commands.ts index 85675230b5..89ab9cfe6a 100644 --- a/backend/src/rpc-api/commands.ts +++ b/backend/src/rpc-api/commands.ts @@ -83,6 +83,7 @@ module.exports = { signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ stop: 'stop', submitBlock: 'submitblock', // bitcoind v0.7.0+ + submitPackage: 'submitpackage', validateAddress: 'validateaddress', verifyChain: 'verifychain', // bitcoind v0.9.0+ verifyMessage: 'verifymessage', diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index ada2a89b21..d0b92f42e9 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -79,6 +79,10 @@ class FundingTxFetcher { } const parts = channelId.split('x'); + if (parts.length < 3) { + logger.debug(`Channel ID ${channelId} does not seem valid, should contains at least 3 parts separated by 'x'`, logger.tags.ln); + return null; + } const blockHeight = parts[0]; const txIdx = parts[1]; const outputIdx = parts[2]; @@ -99,6 +103,10 @@ class FundingTxFetcher { } const txid = block.tx[txIdx]; + if (!txid) { + logger.debug(`Cannot cache ${channelId} funding tx. TX index ${txIdx} does not exist in block ${block.hash ?? block.id}`, logger.tags.ln); + return null; + } const rawTx = await bitcoinClient.getRawTransaction(txid); const tx = await bitcoinClient.decodeRawTransaction(rawTx); diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index a3a3265c62..6b0520dfc7 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info'; import logger from '../logger'; import { SocksProxyAgent } from 'socks-proxy-agent'; import * as https from 'https'; +import { Common } from '../api/common'; /** * Maintain the most recent version of pools-v2.json */ class PoolsUpdater { + tag = 'PoolsUpdater'; + lastRun: number = 0; currentSha: string | null = null; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; + public async $startService(): Promise { + while ('Bitcoin is still alive') { + try { + await this.updatePoolsJson(); + } catch (e: any) { + logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag); + } + await Common.sleep$(10000); + } + } + public async updatePoolsJson(): Promise { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.MEMPOOL.ENABLED === false @@ -23,27 +37,24 @@ class PoolsUpdater { return; } - const oneWeek = 604800; - const oneDay = 86400; - const now = new Date().getTime() / 1000; - if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart + if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart return; } this.lastRun = now; try { + if (config.DATABASE.ENABLED === true) { + this.currentSha = await this.getShaFromDb(); + } + const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github if (githubSha === null) { return; } - if (config.DATABASE.ENABLED === true) { - this.currentSha = await this.getShaFromDb(); - } - - logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); + logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag); if (this.currentSha !== null && this.currentSha === githubSha) { return; } @@ -53,16 +64,16 @@ class PoolsUpdater { config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled !process.env.npm_config_update_pools // We're not manually updating mining pool ) { - logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`); - logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`); + logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag); + logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag); return; } const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; if (this.currentSha === null) { - logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); + logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag); } else { - logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); + logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag); } const poolsJson = await this.query(this.poolsUrl); if (poolsJson === undefined) { @@ -71,24 +82,25 @@ class PoolsUpdater { poolsParser.setMiningPools(poolsJson); if (config.DATABASE.ENABLED === false) { // Don't run db operations - logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`); + logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag); return; } try { await DB.query('START TRANSACTION;'); - await poolsParser.migratePoolsJson(); await this.updateDBSha(githubSha); + await poolsParser.migratePoolsJson(); await DB.query('COMMIT;'); } catch (e) { - logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); await DB.query('ROLLBACK;'); } - logger.info(`Mining pools-v2.json (${githubSha}) import completed`); + logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag); } catch (e) { - this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week - logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + // fast-forward lastRun to 10 minutes before the next scheduled update + this.lastRun = now - Math.max(config.MEMPOOL.POOLS_UPDATE_DELAY - 600, 600); + logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); } } @@ -102,7 +114,7 @@ class PoolsUpdater { await DB.query('DELETE FROM state where name="pools_json_sha"'); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); } catch (e) { - logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); } } } @@ -110,12 +122,12 @@ class PoolsUpdater { /** * Fetch our latest pools-v2.json sha from the db */ - private async getShaFromDb(): Promise { + public async getShaFromDb(): Promise { try { const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); return (rows.length > 0 ? rows[0].string : null); } catch (e) { - logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); return null; } } @@ -134,7 +146,7 @@ class PoolsUpdater { } } - logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); + logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag); return null; } @@ -186,7 +198,7 @@ class PoolsUpdater { } return data.data; } catch (e) { - logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag); retry++; } await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); diff --git a/backend/src/tasks/price-updater.ts b/backend/src/tasks/price-updater.ts index 467669a6f6..2cf0fbdb49 100644 --- a/backend/src/tasks/price-updater.ts +++ b/backend/src/tasks/price-updater.ts @@ -271,11 +271,6 @@ class PriceUpdater { } this.latestPrices.time = Math.round(new Date().getTime() / 1000); - logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`); - - if (this.ratesChangedCallback) { - this.ratesChangedCallback(this.latestPrices); - } if (!forceUpdate) { this.cyclePosition++; @@ -283,6 +278,13 @@ class PriceUpdater { if (this.latestPrices.USD === -1) { this.latestPrices = await PricesRepository.$getLatestConversionRates(); + logger.warn(`No BTC price available, falling back to latest known price: ${JSON.stringify(this.latestPrices)}`); + } else { + logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`); + } + + if (this.ratesChangedCallback && this.latestPrices.USD > 0) { + this.ratesChangedCallback(this.latestPrices); } } diff --git a/backend/src/utils/api.ts b/backend/src/utils/api.ts new file mode 100644 index 0000000000..69d746b9f1 --- /dev/null +++ b/backend/src/utils/api.ts @@ -0,0 +1,9 @@ +import { Request, Response } from 'express'; + +export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void { + if (req.accepts('json')) { + res.status(statusCode).json({ error: errorMessage }); + } else { + res.status(statusCode).send(errorMessage); + } +} \ No newline at end of file diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index 3414e82691..f9755fcb41 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb if (!opN) { return; } - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); @@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb if (!opM) { return; } - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); @@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number { } else { return 9; } +} + +/** Extracts miner names from a DATUM coinbase transaction */ +export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null { + let bytes: number[] = []; + for (let c = 0; c < coinbaseRaw.length; c += 2) { + bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16)); + } + + // Skip block height + let tagLengthByte = 1 + bytes[0]; + + let tagsLength = bytes[tagLengthByte]; + if (tagsLength == 0x4c) { + tagLengthByte += 1; + tagsLength = bytes[tagLengthByte]; + } + + const tagStart = tagLengthByte + 1; + const tags = bytes.slice(tagStart, tagStart + tagsLength); + let tagString = String.fromCharCode(...tags); + tagString = tagString.replace('\x00', ''); + + return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, '')); } \ No newline at end of file diff --git a/backend/src/utils/file-read.ts b/backend/src/utils/file-read.ts new file mode 100644 index 0000000000..ddf8660c4c --- /dev/null +++ b/backend/src/utils/file-read.ts @@ -0,0 +1,58 @@ +import * as fs from 'fs'; +import logger from '../logger'; +import config from '../config'; + +function readFile(filePath: string, bufferSize?: number): string[] { + const fileSize = fs.statSync(filePath).size; + const chunkSize = bufferSize || fileSize; + const fileDescriptor = fs.openSync(filePath, 'r'); + const buffer = Buffer.alloc(chunkSize); + + fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize); + fs.closeSync(fileDescriptor); + + const lines = buffer.toString('utf8', 0, chunkSize).split('\n'); + return lines; +} + +function extractDateFromLogLine(line: string): number | undefined { + // Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z" + const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/); + if (!dateMatch) { + return undefined; + } + + const dateStr = dateMatch[0]; + const date = new Date(dateStr); + let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later) + + const timePart = dateStr.split('T')[1]; + const microseconds = timePart.split('.')[1] || ''; + + if (!microseconds) { + return timestamp; + } + + return parseFloat(timestamp + '.' + microseconds); +} + +export function getRecentFirstSeen(hash: string): number | undefined { + const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH; + if (debugLogPath) { + try { + // Read the last few lines of debug.log + const lines = readFile(debugLogPath, 2048); + + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + if (line && line.includes(`Saw new header hash=${hash}`)) { + return extractDateFromLogLine(line); + } + } + } catch (e) { + logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e)); + } + } + + return undefined; +} diff --git a/contributors/adambor.txt b/contributors/adambor.txt new file mode 100644 index 0000000000..14c78ea9c8 --- /dev/null +++ b/contributors/adambor.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 12, 2025. + +Signed: adambor diff --git a/docker/README.md b/docker/README.md index ce1548e910..2658914eb6 100644 --- a/docker/README.md +++ b/docker/README.md @@ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over "AUTOMATIC_POOLS_UPDATE": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "POOLS_UPDATE_DELAY": 604800, "CPFP_INDEXING": false, "MAX_BLOCKS_BULK_QUERY": 0, "DISK_CACHE_BLOCK_INTERVAL": 6, @@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides: MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: "" + MEMPOOL_POOLS_UPDATE_DELAY: "" MEMPOOL_CPFP_INDEXING: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 60d663f206..942a8f9c8b 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -1,20 +1,20 @@ -FROM node:20.15.0-buster-slim AS builder +FROM rust:1.84-bookworm AS builder ARG commitHash ENV MEMPOOL_COMMIT_HASH=${commitHash} WORKDIR /build -COPY . . -RUN apt-get update -RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates +RUN apt-get update && \ + apt-get install -y curl ca-certificates && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs=22.14.0-1nodesource1 build-essential python3 pkg-config && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +COPY . . -# Install Rust via rustup -RUN CPU_ARCH=$(uname -m); if [ "$CPU_ARCH" = "armv7l" ]; then c_rehash; fi -#RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable -#Workaround to run on github actions from https://github.com/rust-lang/rustup/issues/2700#issuecomment-1367488985 -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sed 's#/proc/self/exe#\/bin\/sh#g' | sh -s -- -y --default-toolchain stable -ENV PATH="/root/.cargo/bin:$PATH" +ENV PATH="/usr/local/cargo/bin:$PATH" COPY --from=backend . . COPY --from=rustgbt . ../rust/ @@ -24,7 +24,14 @@ RUN npm install --omit=dev --omit=optional WORKDIR /build RUN npm run package -FROM node:20.15.0-buster-slim +FROM rust:1.84-bookworm AS runtime + +RUN apt-get update && \ + apt-get install -y curl ca-certificates && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs=22.14.0-1nodesource1 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* WORKDIR /backend diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 79cd146448..ee8e329a64 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -36,6 +36,7 @@ "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", + "POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__, "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ }, @@ -46,7 +47,8 @@ "PASSWORD": "__CORE_RPC_PASSWORD__", "TIMEOUT": __CORE_RPC_TIMEOUT__, "COOKIE": __CORE_RPC_COOKIE__, - "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" + "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__", + "DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__" }, "ELECTRUM": { "HOST": "__ELECTRUM_HOST__", @@ -146,6 +148,10 @@ "API": "__MEMPOOL_SERVICES_API__", "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__ }, + "STRATUM": { + "ENABLED": __STRATUM_ENABLED__, + "API": "__STRATUM_API__" + }, "REDIS": { "ENABLED": __REDIS_ENABLED__, "UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__", diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 8033531ef1..ae0bc616f8 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -26,11 +26,12 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1} __MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0} __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} -__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} +__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=true} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} +__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800} __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} -__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false} +__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true} __MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false} __MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false} __MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0} @@ -48,6 +49,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool} __CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000} __CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} __CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} +__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""} # ELECTRUM __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} @@ -147,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]} __MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"} __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false} +# STRATUM +__STRATUM_ENABLED__=${STRATUM_ENABLED:=false} +__STRATUM_API__=${STRATUM_API:="http://localhost:1234"} + # REDIS __REDIS_ENABLED__=${REDIS_ENABLED:=false} __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""} @@ -187,6 +193,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json +sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json @@ -205,6 +212,7 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json +sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json @@ -296,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json +# STRATUM +sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json +sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json + # REDIS sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 8374ebe49b..23c1da3d42 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.15.0-buster-slim AS builder +FROM node:22.14.0-bookworm-slim AS builder ARG commitHash ENV DOCKER_COMMIT_HASH=${commitHash} diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh index 2086188c90..9727adb2df 100644 --- a/docker/frontend/entrypoint.sh +++ b/docker/frontend/entrypoint.sh @@ -45,6 +45,7 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} +__STRATUM_ENABLED__=${STRATUM_ENABLED:=false} # Export as environment variables to be used by envsubst export __MAINNET_ENABLED__ @@ -76,6 +77,7 @@ export __SERVICES_API__ export __PUBLIC_ACCELERATIONS__ export __HISTORICAL_PRICE__ export __ADDITIONAL_CURRENCIES__ +export __STRATUM_ENABLED__ folder=$(find /var/www/mempool -name "config.js" | xargs dirname) echo ${folder} diff --git a/frontend/README.md b/frontend/README.md index 069f1d5f0a..fb2a5e2917 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,7 +33,7 @@ $ npm run config:defaults:liquid ### 3. Run the Frontend -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer._ Install project dependencies and run the frontend server: @@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already. ### 1. Build the Frontend -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer._ Build the frontend: diff --git a/frontend/angular.json b/frontend/angular.json index 3aa1cb6a87..c5da6a09a6 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -261,20 +261,14 @@ "proxyConfig": "proxy.conf.mixed.js", "verbose": true }, - "staging": { - "proxyConfig": "proxy.conf.js", - "disableHostCheck": true, - "host": "0.0.0.0", - "verbose": true - }, "local-prod": { "proxyConfig": "proxy.conf.js", "disableHostCheck": true, "host": "0.0.0.0", "verbose": false }, - "local-staging": { - "proxyConfig": "proxy.conf.staging.js", + "parameterized": { + "proxyConfig": "proxy.conf.parameterized.js", "disableHostCheck": true, "host": "0.0.0.0", "verbose": false @@ -371,4 +365,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/custom-bitb-config.json b/frontend/custom-bitb-config.json new file mode 100644 index 0000000000..4938034fe8 --- /dev/null +++ b/frontend/custom-bitb-config.json @@ -0,0 +1,48 @@ +{ + "theme": "wiz", + "enterprise": "bitb", + "branding": { + "name": "bitb", + "title": "BITB", + "site_id": 20, + "header_img": "/resources/bitblogo.svg", + "footer_img": "/resources/bitblogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "BITB" + } + }, + { + "component": "goggles", + "mobileOrder": 5 + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "BITB", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "BITB" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/custom-meta-config.json b/frontend/custom-meta-config.json new file mode 100644 index 0000000000..6fa46192ad --- /dev/null +++ b/frontend/custom-meta-config.json @@ -0,0 +1,51 @@ +{ + "theme": "contrast", + "enterprise": "meta", + "branding": { + "name": "metaplanet", + "title": "Metaplanet", + "site_id": 21, + "header_img": "/resources/metalogo.svg", + "footer_img": "/resources/metalogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "3350" + } + }, + { + "component": "twitter", + "mobileOrder": 5, + "props": { + "handle": "Metaplanet_JP" + } + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "3350", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "3350" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/custom-river-config.json b/frontend/custom-river-config.json new file mode 100644 index 0000000000..aca537adf5 --- /dev/null +++ b/frontend/custom-river-config.json @@ -0,0 +1,51 @@ +{ + "theme": "wiz", + "enterprise": "river", + "branding": { + "name": "river", + "title": "river", + "site_id": 22, + "header_img": "/resources/riverlogo.svg", + "footer_img": "/resources/riverlogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "RIVER" + } + }, + { + "component": "twitter", + "mobileOrder": 5, + "props": { + "handle": "River" + } + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "RIVER", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "RIVER" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/custom-strategy-config.json b/frontend/custom-strategy-config.json new file mode 100644 index 0000000000..a06f1d32ca --- /dev/null +++ b/frontend/custom-strategy-config.json @@ -0,0 +1,42 @@ +{ + "theme": "wiz", + "enterprise": "strategy", + "branding": { + "name": "strategy", + "title": "strategy", + "site_id": 22, + "header_img": "/resources/strategylogo.svg", + "footer_img": "/resources/strategylogo.svg" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 1 + }, + { + "component": "difficulty", + "mobileOrder": 4 + }, + { + "component": "twitter", + "mobileOrder": 2, + "props": { + "handle": "MicroStrategy" + } + }, + { + "component": "goggles", + "mobileOrder": 3 + }, + { + "component": "blocks", + "mobileOrder": 5 + }, + { + "component": "transactions", + "mobileOrder": 6 + } + ] + } +} \ No newline at end of file diff --git a/frontend/custom-sv-config.json b/frontend/custom-sv-config.json index dee3dab18e..820a300fa1 100644 --- a/frontend/custom-sv-config.json +++ b/frontend/custom-sv-config.json @@ -16,10 +16,10 @@ "mobileOrder": 4 }, { - "component": "balance", + "component": "walletBalance", "mobileOrder": 1, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" + "wallet": "ONBTC" } }, { @@ -30,21 +30,27 @@ } }, { - "component": "address", + "component": "wallet", "mobileOrder": 2, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", - "period": "1m" + "wallet": "ONBTC", + "period": "1m", + "label": "bitcoin.gob.sv" } }, { - "component": "blocks" + "component": "simpleproof_cubo", + "mobileOrder": 6, + "props": { + "label": "CUBO+ Certificates", + "key": "cubo_certificates" + } }, { - "component": "addressTransactions", + "component": "walletTransactions", "mobileOrder": 3, "props": { - "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" + "wallet": "ONBTC" } } ] diff --git a/frontend/custom-xxi-config.json b/frontend/custom-xxi-config.json new file mode 100644 index 0000000000..6d3135aa0e --- /dev/null +++ b/frontend/custom-xxi-config.json @@ -0,0 +1,48 @@ +{ + "theme": "wiz", + "enterprise": "xxi", + "branding": { + "name": "xxi", + "title": "Twenty One", + "site_id": 23, + "header_img": "/resources/xxi/xxilogo.png", + "footer_img": "/resources/xxi/xxilogo.png" + }, + "dashboard": { + "widgets": [ + { + "component": "fees", + "mobileOrder": 4 + }, + { + "component": "walletBalance", + "mobileOrder": 1, + "props": { + "wallet": "xxi" + } + }, + { + "component": "goggles", + "mobileOrder": 5 + }, + { + "component": "wallet", + "mobileOrder": 2, + "props": { + "wallet": "xxi", + "period": "all" + } + }, + { + "component": "blocks" + }, + { + "component": "walletTransactions", + "mobileOrder": 3, + "props": { + "wallet": "xxi" + } + } + ] + } +} \ No newline at end of file diff --git a/frontend/cypress/e2e/liquid/liquid.spec.ts b/frontend/cypress/e2e/liquid/liquid.spec.ts index c7d2a92eed..2da3c41f56 100644 --- a/frontend/cypress/e2e/liquid/liquid.spec.ts +++ b/frontend/cypress/e2e/liquid/liquid.spec.ts @@ -57,11 +57,6 @@ describe('Liquid', () => { }); }); - it('loads the tv page - desktop', () => { - cy.visit(`${basePath}/tv`); - cy.waitForSkeletonGone(); - }); - it('loads the graphs page - mobile', () => { cy.visit(`${basePath}`) cy.waitForSkeletonGone(); @@ -144,10 +139,10 @@ describe('Liquid', () => { it('show unblinded TX', () => { cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc,2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`); cy.waitForSkeletonGone(); - cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', '0.02465000 L-BTC'); + cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', '0.02465000 LBTC'); cy.get('.table-tx-vin tr').should('have.class', 'assetBox'); - cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00100000 L-BTC'); - cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.02364760 L-BTC'); + cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00100000 LBTC'); + cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.02364760 LBTC'); cy.get('.table-tx-vout tr').should('have.class', 'assetBox'); }); @@ -174,13 +169,13 @@ describe('Liquid', () => { cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=100000,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,0ab9f70650f16b1db8dfada05237f7d0d65191c3a13183da8a2ddddfbde9a2ad,fd98b2edc5530d76acd553f206a431f4c1fab27e10e290ad719582af878e98fc`); cy.waitForSkeletonGone(); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'assetBox'); - cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00100000 L-BTC'); + cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00100000 LBTC'); }); it('show second unblinded vout', () => { cy.visit(`${basePath}/tx/f2f41c0850e8e7e3f1af233161fd596662e67c11ef10ed15943884186fbb7f46#blinded=2364760,6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d,90c7a43b15b905bca045ca42a01271cfe71d2efe3133f4197792c24505cb32ed,12eb5959d9293b8842e7dd8bc9aa9639fd3fd031c5de3ba911adeca94eb57a3a`); cy.get('.table-tx-vout tr:nth-child(2').should('have.class', 'assetBox'); - cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.02364760 L-BTC'); + cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.02364760 LBTC'); }); it('show invalid error unblinded TX', () => { diff --git a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts index 54e355ce80..b5038f89be 100644 --- a/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts +++ b/frontend/cypress/e2e/liquidtestnet/liquidtestnet.spec.ts @@ -57,11 +57,6 @@ describe('Liquid Testnet', () => { cy.waitForSkeletonGone(); }); - it('loads the tv page - desktop', () => { - cy.visit(`${basePath}/tv`); - cy.waitForSkeletonGone(); - }); - it('loads the graphs page - mobile', () => { cy.visit(`${basePath}`) cy.waitForSkeletonGone(); @@ -108,10 +103,10 @@ describe('Liquid Testnet', () => { it('show unblinded TX', () => { cy.visit(`${basePath}/tx/c3d908ab77891e4c569b0df71aae90f4720b157019ebb20db176f4f9c4d626b8#blinded=100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,df290ead654d7d110ebc5aaf0bcf11d5b5d360431a467f1cde0a856fde986893,33cb3a2fd2e76643843691cf44a78c5cd28ec652a414da752160ad63fbd37bc9,49741,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,edb0713bcbfcb3daabf601cb50978439667d208e15fed8a5ebbfea5696cda1d5,4de70115501e8c7d6bd763e229bf42781edeacf6e75e1d7bdfa4c63104bc508a`); cy.waitForSkeletonGone(); - cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', '0.00100000 tL-BTC'); + cy.get('.table-tx-vin tr:nth-child(1) .amount').should('contain.text', '0.00100000 tLBTC'); cy.get('.table-tx-vin tr').should('have.class', 'assetBox'); - cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00050000 tL-BTC'); - cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.00049741 tL-BTC'); + cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00050000 tLBTC'); + cy.get('.table-tx-vout tr:nth-child(2) .amount').should('contain.text', '0.00049741 tLBTC'); cy.get('.table-tx-vout tr').should('have.class', 'assetBox'); }); @@ -138,7 +133,7 @@ describe('Liquid Testnet', () => { cy.visit(`${basePath}/tx/0877bc0c7aa5c2b8d0e4b15450425879b8783c40e341806037a605ef836fb886#blinded=5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,328de54e90e867a9154b4f1eb7fcab86267e880fa2ee9e53b41a91e61dab86e6,8885831e6b089eaf06889d53a24843f0da533d300a7b1527b136883a6819f3ae,5000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,aca78b953615d69ae0ae68c4c5c3c0ee077c10bc20ad3f0c5960706004e6cb56,d2ec175afe5f761e2dbd443faf46abbb7091f341deb3387e5787d812bdb2df9f,100000,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,4b54a4ca809b3844f34dd88b68617c4c866d92a02211f02ba355755bac20a1c6,eddd02e92b0cfbad8cab89828570a50f2c643bb2a54d886c86e25ce47e818685,99729,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,8b86d565c9549eb0352bb81ee576d01d064435b64fddcc045decebeb1d9913ce,b082ce3448d40d47b5b39f15d72b285f4a1046b636b56c25f32f498ece29d062,10000,38fca2d939696061a8f76d4e6b5eecd54e3b4221c846f24a6b279e79952850a5,62b04ee86198d6b41681cdd0acb450ab366af727a010aaee8ba0b9e69ff43896,3f98429bca9b538dc943c22111f25d9c4448d45a63ff0f4e58b22fd434c0365e`); cy.waitForSkeletonGone(); cy.get('.table-tx-vout tr:nth-child(1)').should('have.class', 'assetBox'); - cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00099729 tL-BTC'); + cy.get('.table-tx-vout tr:nth-child(1) .amount').should('contain.text', '0.00099729 tLBTC'); }); it('show second unblinded vout (asset)', () => { diff --git a/frontend/cypress/e2e/mainnet/mainnet.spec.ts b/frontend/cypress/e2e/mainnet/mainnet.spec.ts index a1082b7690..a664f333c4 100644 --- a/frontend/cypress/e2e/mainnet/mainnet.spec.ts +++ b/frontend/cypress/e2e/mainnet/mainnet.spec.ts @@ -1,4 +1,4 @@ -import { emitMempoolInfo, dropWebSocket } from '../../support/websocket'; +import { emitMempoolInfo, dropWebSocket, receiveWebSocketMessageFromServer } from '../../support/websocket'; const baseModule = Cypress.env('BASE_MODULE'); @@ -216,6 +216,69 @@ describe('Mainnet', () => { cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').its('length').should('equal', 2); cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`); }); + + describe('address poisoning', () => { + it('highlights potential address poisoning attacks on outputs, prefix and infix', () => { + const txid = '152a5dea805f95d6f83e50a9fd082630f542a52a076ebabdb295723eaf53fa30'; + const prefix = '1DonatePLease'; + const infix1 = 'SenderAddressXVXCmAY'; + const infix2 = '5btcToSenderXXWBoKhB'; + + cy.visit(`/tx/${txid}`); + cy.waitForSkeletonGone(); + cy.get('.alert-mempool').should('exist'); + cy.get('.poison-alert').its('length').should('equal', 2); + + cy.get('.prefix') + .should('have.length', 2) + .each(($el) => { + cy.wrap($el).should('have.text', prefix); + }); + + cy.get('.infix') + .should('have.length', 2) + .then(($infixes) => { + cy.wrap($infixes[0]).should('have.text', infix1); + cy.wrap($infixes[1]).should('have.text', infix2); + }); + }); + + it('highlights potential address poisoning attacks on inputs and outputs, prefix, infix and postfix', () => { + const txid = '44544516084ea916ff1eb69c675c693e252addbbaf77102ffff86e3979ac6132'; + const prefix = 'bc1qge8'; + const infix1 = '6gqjnk8aqs3nvv7ejrvcd4zq6qur3'; + const infix2 = 'xyxjex6zzzx5g8hh65vsel4e548p2'; + const postfix1 = '6p6e3r'; + const postfix2 = '6p6e3r'; + + cy.visit(`/tx/${txid}`); + cy.waitForSkeletonGone(); + cy.get('.alert-mempool').should('exist'); + cy.get('.poison-alert').its('length').should('equal', 2); + + cy.get('.prefix') + .should('have.length', 2) + .each(($el) => { + cy.wrap($el).should('have.text', prefix); + }); + + cy.get('.infix') + .should('have.length', 2) + .then(($infixes) => { + cy.wrap($infixes[0]).should('have.text', infix1); + cy.wrap($infixes[1]).should('have.text', infix2); + }); + + cy.get('.postfix') + .should('have.length', 2) + .then(($postfixes) => { + cy.wrap($postfixes[0]).should('include.text', postfix1); + cy.wrap($postfixes[1]).should('include.text', postfix2); + }); + + }); + }); + }); describe('blocks navigation', () => { @@ -344,7 +407,9 @@ describe('Mainnet', () => { cy.visit('/'); cy.waitForSkeletonGone(); - cy.changeNetwork('testnet4'); + //TODO(knorrium): add a check for the proxied server + // cy.changeNetwork('testnet4'); + cy.changeNetwork('signet'); cy.changeNetwork('mainnet'); }); @@ -395,6 +460,7 @@ describe('Mainnet', () => { cy.get('#dropdownFees').should('be.visible'); cy.get('.btn-group').should('be.visible'); }); + it('check buttons - tablet', () => { cy.viewport('ipad-2'); cy.visit('/graphs'); @@ -403,6 +469,7 @@ describe('Mainnet', () => { cy.get('#dropdownFees').should('be.visible'); cy.get('.btn-group').should('be.visible'); }); + it('check buttons - desktop', () => { cy.viewport('macbook-16'); cy.visit('/graphs'); @@ -413,26 +480,6 @@ describe('Mainnet', () => { }); }); - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/graphs/mempool'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('macbook-16'); - cy.get('.chart-holder'); - cy.get('.blockchain-wrapper').should('be.visible'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.viewport('iphone-6'); - cy.visit('/tv'); - cy.waitForSkeletonGone(); - cy.get('.chart-holder'); - cy.get('.blockchain-wrapper').should('not.visible'); - }); - it('loads the api screen', () => { cy.visit('/'); cy.waitForSkeletonGone(); @@ -514,7 +561,44 @@ describe('Mainnet', () => { }); describe('RBF transactions', () => { - it('shows RBF transactions properly (mobile)', () => { + it('RBF page gets updated over websockets', () => { + cy.intercept('/api/v1/replacements', { + statusCode: 200, + body: [] + }); + + cy.intercept('/api/v1/fullrbf/replacements', { + statusCode: 200, + body: [] + }); + + cy.mockMempoolSocketV2(); + + cy.visit('/rbf'); + cy.get('.no-replacements'); + cy.get('.tree').should('have.length', 0); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'rbf_page/rbf_01.json' + } + } + }); + + cy.get('.tree').should('have.length', 1); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'rbf_page/rbf_02.json' + } + } + }); + cy.get('.tree').should('have.length', 2); + }); + + it('shows RBF transactions properly (mobile - details)', () => { cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', { fixture: 'mainnet_tx_cached.json' }).as('cached_tx'); @@ -525,7 +609,7 @@ describe('Mainnet', () => { cy.viewport('iphone-xr'); cy.mockMempoolSocket(); - cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f'); + cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f?mode=details'); cy.waitForSkeletonGone(); @@ -543,7 +627,120 @@ describe('Mainnet', () => { } }); + cy.get('.alert-mempool').should('be.visible'); + }); + + it('shows RBF transactions properly (mobile - tracker)', () => { + cy.mockMempoolSocketV2(); + cy.viewport('iphone-xr'); + + // API Mocks + cy.intercept('/api/v1/mining/pools/1w', { + fixture: 'details_rbf/api_mining_pools_1w.json' + }).as('api_mining_1w'); + + cy.intercept('/api/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29', { + statusCode: 404, + body: 'Transaction not found' + }).as('api_tx01_404'); + + cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/cached', { + fixture: 'details_rbf/tx01_api_cached.json' + }).as('api_tx01_cached'); + + cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/rbf', { + fixture: 'details_rbf/tx01_api_rbf.json' + }).as('api_tx01_rbf'); + + cy.visit('/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29?mode=tracker'); + cy.wait('@api_tx01_rbf'); + + // Start sending mocked WS messages + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_stratum_jobs.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_blocks_01.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_tx_replaced.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx01_ws_mempool_blocks_01.json' + } + } + }); + cy.get('.alert-replaced').should('be.visible'); + cy.get('.explainer').should('be.visible'); + cy.get('svg[data-icon=timeline]').should('be.visible'); + + // Second TX setup + cy.intercept('/api/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', { + fixture: 'details_rbf/tx02_api_tx.json' + }).as('tx02_api'); + + cy.intercept('/api/v1/transaction-times?txId%5B%5D=b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', { + fixture: 'details_rbf/tx02_api_tx_times.json' + }).as('tx02_api_tx_times'); + + cy.intercept('/api/v1/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698/rbf', { + fixture: 'details_rbf/tx02_api_rbf.json' + }).as('tx02_api_rbf'); + + cy.intercept('/api/v1/cpfp/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', { + fixture: 'details_rbf/tx02_api_cpfp.json' + }).as('tx02_api_cpfp'); + + // Go to the replacement tx + cy.get('.alert-replaced a').click(); + + cy.wait('@tx02_api_cpfp'); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx02_ws_tx_position.json' + } + } + }); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx02_ws_mempool_blocks_01.json' + } + } + }); + + cy.get('svg[data-icon=hourglass-half]').should('be.visible'); + + receiveWebSocketMessageFromServer({ + params: { + file: { + path: 'details_rbf/tx02_ws_block.json' + } + } + }); + cy.get('app-confirmations'); + cy.get('svg[data-icon=circle-check]').should('be.visible'); }); it('shows RBF transactions properly (desktop)', () => { diff --git a/frontend/cypress/e2e/signet/signet.spec.ts b/frontend/cypress/e2e/signet/signet.spec.ts index 11c47d14d6..ae591c6a7f 100644 --- a/frontend/cypress/e2e/signet/signet.spec.ts +++ b/frontend/cypress/e2e/signet/signet.spec.ts @@ -60,30 +60,6 @@ describe('Signet', () => { }); }); - describe.skip('tv mode', () => { - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/signet/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.get('.chart-holder').should('be.visible'); - cy.get('#mempool-block-0').should('be.visible'); - cy.get('.tv-only').should('not.exist'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.visit('/signet/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('iphone-8'); - cy.get('.chart-holder').should('be.visible'); - cy.get('.tv-only').should('not.exist'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - }); - it('loads the api screen', () => { cy.visit('/signet'); cy.waitForSkeletonGone(); diff --git a/frontend/cypress/e2e/testnet4/testnet4.spec.ts b/frontend/cypress/e2e/testnet4/testnet4.spec.ts index c67d2414b3..97af0e08e7 100644 --- a/frontend/cypress/e2e/testnet4/testnet4.spec.ts +++ b/frontend/cypress/e2e/testnet4/testnet4.spec.ts @@ -60,30 +60,6 @@ describe('Testnet4', () => { }); }); - describe('tv mode', () => { - it('loads the tv screen - desktop', () => { - cy.viewport('macbook-16'); - cy.visit('/testnet4/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.wait(1000); - cy.get('.tv-only').should('not.exist'); - cy.get('#mempool-block-0').should('be.visible'); - }); - }); - - it('loads the tv screen - mobile', () => { - cy.visit('/testnet4/graphs'); - cy.waitForSkeletonGone(); - cy.get('#btn-tv').click().then(() => { - cy.viewport('iphone-6'); - cy.wait(1000); - cy.get('.tv-only').should('not.exist'); - }); - }); - }); - - it('loads the api screen', () => { cy.visit('/testnet4'); cy.waitForSkeletonGone(); diff --git a/frontend/cypress/fixtures/assets.minimal.json b/frontend/cypress/fixtures/assets.minimal.json index c80ae7f411..6f52957317 100644 --- a/frontend/cypress/fixtures/assets.minimal.json +++ b/frontend/cypress/fixtures/assets.minimal.json @@ -13,7 +13,7 @@ ], "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d": [ null, - "L-BTC", + "LBTC", "Liquid Bitcoin", 8 ], diff --git a/frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json b/frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json new file mode 100644 index 0000000000..889c5d763b --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_accelerator_estimate.json @@ -0,0 +1,60 @@ +{ + "txSummary": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "effectiveVsize": 224, + "effectiveFee": 960, + "ancestorCount": 1 + }, + "cost": 1000, + "targetFeeRate": 3, + "nextBlockFee": 672, + "userBalance": 0, + "mempoolBaseFee": 50000, + "vsizeFee": 0, + "pools": [ + 36, + 102, + 112, + 44, + 4, + 2, + 6, + 94, + 143, + 43, + 105, + 115, + 142, + 111 + ], + "options": [ + { + "fee": 1000 + }, + { + "fee": 2000 + }, + { + "fee": 10000 + } + ], + "hasAccess": false, + "availablePaymentMethods": { + "bitcoin": { + "enabled": true, + "min": 1000, + "max": 10000000 + }, + "applePay": { + "enabled": true, + "min": 10, + "max": 1000 + }, + "googlePay": { + "enabled": true, + "min": 10, + "max": 1000 + } + }, + "unavailable": false +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/api_accelerator_version.json b/frontend/cypress/fixtures/details_rbf/api_accelerator_version.json new file mode 100644 index 0000000000..a01c899b89 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_accelerator_version.json @@ -0,0 +1,3 @@ +{ + "gitCommit": "62f80296" +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/api_block_history.json b/frontend/cypress/fixtures/details_rbf/api_block_history.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_block_history.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json b/frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json new file mode 100644 index 0000000000..3a678ca029 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json @@ -0,0 +1,260 @@ +{ + "pools": [ + { + "poolId": 112, + "name": "Foundry USA", + "link": "https://foundrydigital.com", + "blockCount": 323, + "rank": 1, + "emptyBlocks": 0, + "slug": "foundryusa", + "avgMatchRate": 99.96, + "avgFeeDelta": "-0.01971455", + "poolUniqueId": 111 + }, + { + "poolId": 45, + "name": "AntPool", + "link": "https://www.antpool.com", + "blockCount": 171, + "rank": 2, + "emptyBlocks": 0, + "slug": "antpool", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.04227368", + "poolUniqueId": 44 + }, + { + "poolId": 74, + "name": "ViaBTC", + "link": "https://viabtc.com", + "blockCount": 166, + "rank": 3, + "emptyBlocks": 0, + "slug": "viabtc", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.02530964", + "poolUniqueId": 73 + }, + { + "poolId": 37, + "name": "F2Pool", + "link": "https://www.f2pool.com", + "blockCount": 104, + "rank": 4, + "emptyBlocks": 0, + "slug": "f2pool", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.03299327", + "poolUniqueId": 36 + }, + { + "poolId": 116, + "name": "MARA Pool", + "link": "https://marapool.com", + "blockCount": 66, + "rank": 5, + "emptyBlocks": 0, + "slug": "marapool", + "avgMatchRate": 99.97, + "avgFeeDelta": "0.02366061", + "poolUniqueId": 115 + }, + { + "poolId": 103, + "name": "SpiderPool", + "link": "https://www.spiderpool.com", + "blockCount": 46, + "rank": 6, + "emptyBlocks": 1, + "slug": "spiderpool", + "avgMatchRate": 97.82, + "avgFeeDelta": "-0.07258913", + "poolUniqueId": 102 + }, + { + "poolId": 142, + "name": "SECPOOL", + "link": "https://www.secpool.com", + "blockCount": 30, + "rank": 7, + "emptyBlocks": 1, + "slug": "secpool", + "avgMatchRate": 96.67, + "avgFeeDelta": "-0.06596000", + "poolUniqueId": 141 + }, + { + "poolId": 106, + "name": "Binance Pool", + "link": "https://pool.binance.com", + "blockCount": 28, + "rank": 8, + "emptyBlocks": 0, + "slug": "binancepool", + "avgMatchRate": 99.99, + "avgFeeDelta": "-0.05834286", + "poolUniqueId": 105 + }, + { + "poolId": 5, + "name": "Luxor", + "link": "https://mining.luxor.tech", + "blockCount": 28, + "rank": 9, + "emptyBlocks": 0, + "slug": "luxor", + "avgMatchRate": 100, + "avgFeeDelta": "-0.05496071", + "poolUniqueId": 4 + }, + { + "poolId": 143, + "name": "OCEAN", + "link": "https://ocean.xyz/", + "blockCount": 12, + "rank": 10, + "emptyBlocks": 0, + "slug": "ocean", + "avgMatchRate": 91.9, + "avgFeeDelta": "-0.14650833", + "poolUniqueId": 142 + }, + { + "poolId": 44, + "name": "Braiins Pool", + "link": "https://braiins.com/pool", + "blockCount": 12, + "rank": 11, + "emptyBlocks": 0, + "slug": "braiinspool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.03553333", + "poolUniqueId": 43 + }, + { + "poolId": 113, + "name": "SBI Crypto", + "link": "https://sbicrypto.com", + "blockCount": 8, + "rank": 12, + "emptyBlocks": 0, + "slug": "sbicrypto", + "avgMatchRate": 98.65, + "avgFeeDelta": "-0.04246250", + "poolUniqueId": 112 + }, + { + "poolId": 152, + "name": "Carbon Negative", + "link": "https://github.com/bitcoin-data/mining-pools/issues/48", + "blockCount": 7, + "rank": 13, + "emptyBlocks": 0, + "slug": "carbonnegative", + "avgMatchRate": 99.75, + "avgFeeDelta": "-0.04407143", + "poolUniqueId": 151 + }, + { + "poolId": 7, + "name": "BTC.com", + "link": "https://pool.btc.com", + "blockCount": 5, + "rank": 14, + "emptyBlocks": 0, + "slug": "btccom", + "avgMatchRate": 99.98, + "avgFeeDelta": "-0.02496000", + "poolUniqueId": 6 + }, + { + "poolId": 162, + "name": "Mining Squared", + "link": "https://pool.bsquared.network/", + "blockCount": 4, + "rank": 15, + "emptyBlocks": 0, + "slug": "miningsquared", + "avgMatchRate": 100, + "avgFeeDelta": "-0.00915000", + "poolUniqueId": 161 + }, + { + "poolId": 95, + "name": "Poolin", + "link": "https://www.poolin.com", + "blockCount": 4, + "rank": 16, + "emptyBlocks": 0, + "slug": "poolin", + "avgMatchRate": 100, + "avgFeeDelta": "-0.26485000", + "poolUniqueId": 94 + }, + { + "poolId": 1, + "name": "Unknown", + "link": "https://learnmeabitcoin.com/technical/coinbase-transaction", + "blockCount": 4, + "rank": 17, + "emptyBlocks": 0, + "slug": "unknown", + "avgMatchRate": 100, + "avgFeeDelta": "-0.06490000", + "poolUniqueId": 0 + }, + { + "poolId": 144, + "name": "WhitePool", + "link": "https://whitebit.com/mining-pool", + "blockCount": 3, + "rank": 18, + "emptyBlocks": 0, + "slug": "whitepool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.01293333", + "poolUniqueId": 143 + }, + { + "poolId": 3, + "name": "ULTIMUSPOOL", + "link": "https://www.ultimuspool.com", + "blockCount": 1, + "rank": 19, + "emptyBlocks": 0, + "slug": "ultimuspool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.16130000", + "poolUniqueId": 2 + }, + { + "poolId": 50, + "name": "Solo CK", + "link": "https://solo.ckpool.org", + "blockCount": 1, + "rank": 20, + "emptyBlocks": 0, + "slug": "solock", + "avgMatchRate": 100, + "avgFeeDelta": "-0.01510000", + "poolUniqueId": 49 + }, + { + "poolId": 158, + "name": "BitFuFuPool", + "link": "https://www.bitfufu.com/pool", + "blockCount": 1, + "rank": 21, + "emptyBlocks": 0, + "slug": "bitfufupool", + "avgMatchRate": 100, + "avgFeeDelta": "-0.01630000", + "poolUniqueId": 157 + } + ], + "blockCount": 1024, + "lastEstimatedHashrate": 786391245138648900000, + "lastEstimatedHashrate3d": 797683179385121300000, + "lastEstimatedHashrate1w": 827836055441520300000 +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_api_cached.json b/frontend/cypress/fixtures/details_rbf/tx01_api_cached.json new file mode 100644 index 0000000000..62184cc9d3 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_api_cached.json @@ -0,0 +1,55 @@ +{ + "txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9", + "vout": 0, + "prevout": { + "scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj", + "value": 50000 + }, + "scriptsig": "483045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb3014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "scriptsig_asm": "OP_PUSHBYTES_72 3045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb301 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "5120a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795", + "scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795", + "scriptpubkey_type": "v1_p2tr", + "scriptpubkey_address": "bc1p5t79edz9w4fcnmkjjk4e6k2tp7kd8cqggz37gal60sp9gyk9x72sk4mk0f", + "value": 49394 + } + ], + "size": 233, + "weight": 932, + "sigops": 0, + "fee": 606, + "status": { + "confirmed": false + }, + "order": 701313494, + "vsize": 233, + "adjustedVsize": 233, + "feePerVsize": 2.6008583690987126, + "adjustedFeePerVsize": 2.6008583690987126, + "effectiveFeePerVsize": 2.6008583690987126, + "firstSeen": 1743541407, + "inputs": [], + "cpfpDirty": false, + "ancestors": [], + "descendants": [], + "bestDescendant": null, + "position": { + "block": 0, + "vsize": 318595.5 + }, + "flags": 1099511645193 +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json b/frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json new file mode 100644 index 0000000000..5278f9ffeb --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json @@ -0,0 +1,34 @@ +{ + "replacements": { + "tx": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "fee": 960, + "vsize": 224, + "value": 49040, + "rate": 4.285714285714286, + "time": 1743541726, + "rbf": true, + "fullRbf": false + }, + "time": 1743541726, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29", + "fee": 606, + "vsize": 233, + "value": 49394, + "rate": 2.6008583690987126, + "time": 1743541407, + "rbf": true + }, + "time": 1743541407, + "interval": 319, + "fullRbf": false, + "replaces": [] + } + ] + }, + "replaces": null +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json new file mode 100644 index 0000000000..32b6578fc3 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json @@ -0,0 +1,579 @@ +{ + "blocks": [ + { + "id": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4", + "height": 890440, + "version": 559235072, + "timestamp": 1743535677, + "bits": 386038124, + "nonce": 2920325684, + "difficulty": 113757508810854, + "merkle_root": "c793d5fdbfb1ebe99e14a13a6d65370057d311774d33c71da166663b18722474", + "tx_count": 3823, + "size": 1578209, + "weight": 3993461, + "previousblockhash": "000000000000000000020fb2e24425793e17e60e188205dc1694d221790348b2", + "mediantime": 1743532406, + "stale": false, + "extras": { + "reward": 319838750, + "coinbaseRaw": "0348960d082f5669614254432f2cfabe6d6d294719da11c017243828bf32c405341db7f19387fee92c25413c45e114907f9810000000000000001058bf9601429f9fa7a6c160d10d00000000000000", + "orphans": [], + "medianFee": 4, + "feeRange": [ + 3, + 3, + 3.0191082802547773, + 3.980952380952381, + 5, + 10, + 427.748502994012 + ], + "totalFees": 7338750, + "avgFee": 1920, + "avgFeeRate": 7, + "utxoSetChange": 4093, + "avgTxSize": 412.71000000000004, + "totalInputs": 7430, + "totalOutputs": 11523, + "totalOutputAmt": 547553568373, + "segwitTotalTxs": 3432, + "segwitTotalSize": 1467920, + "segwitTotalWeight": 3552413, + "feePercentiles": null, + "virtualSize": 998365.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003H–\r\b/ViaBTC/,ú¾mm)G\u0019Ú\u0011À\u0017$8(¿2Ä\u00054\u001d·ñ“‡þé,%Aìg/Foundry USA Pool #dropgold/\u0001S\nEaç\u0002\u0000\u0000\u0000\u0000\u0000", + "header": "00a03220b46014bdbeb557d1d87065e0ecb1b0ab33654bb7f579000000000000000000003ed60f06cec16df4399b5dafa7077036c2eb58cc6a16e6cdca559b9e2f7e4525bb3eec676c790217b7b3c9cb", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2792968, + "expectedWeight": 3991959, + "similarity": 0.9951416839808291 + } + }, + { + "id": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "height": 890442, + "version": 557981696, + "timestamp": 1743536834, + "bits": 386038124, + "nonce": 470697326, + "difficulty": 113757508810854, + "merkle_root": "5e92e681c1db2797a5b3e5016729059f8b60a256cafb51d835dac2b3964c0db4", + "tx_count": 3566, + "size": 1628328, + "weight": 3993552, + "previousblockhash": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c", + "mediantime": 1743532867, + "stale": false, + "extras": { + "reward": 318057766, + "coinbaseRaw": "034a960d194d696e656420627920416e74506f6f6c204d000201e15e2989fabe6d6dd599e9dfa40be51f1517c8f512c5c3d51c7656182f1df335d34b98ee02c527db080000000000000000004f92b702000000000000", + "orphans": [], + "medianFee": 3.00860164711668, + "feeRange": [ + 1.5174418604651163, + 2.0140845070422535, + 2.492354740061162, + 3, + 4.020942408376963, + 7, + 200 + ], + "totalFees": 5557766, + "avgFee": 1558, + "avgFeeRate": 5, + "utxoSetChange": 1971, + "avgTxSize": 456.48, + "totalInputs": 7938, + "totalOutputs": 9909, + "totalOutputAmt": 900044492230, + "segwitTotalTxs": 3214, + "segwitTotalSize": 1526463, + "segwitTotalWeight": 3586200, + "feePercentiles": null, + "virtualSize": 998388, + "coinbaseAddress": "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "coinbaseAddresses": [ + "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "39C7fxSzEACPjM78Z7xdPxhf7mKxJwvfMJ" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 42402a28dd61f2718a4b27ae72a4791d5bbdade7 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003J–\r\u0019Mined by AntPool M\u0000\u0002\u0001á^)‰ú¾mmՙéߤ\u000bå\u001f\u0015\u0017Èõ\u0012ÅÃÕ\u001cvV\u0018/\u001dó5ÓK˜î\u0002Å'Û\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000O’·\u0002\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "002042219cd10cfc6daf06d3faad645fe0914cea859745275aef00000000000000000000b40d4c96b3c2da35d851fbca56a2608b9f05296701e5b3a59727dbc181e6925ec242ec676c7902176e450e1c", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 44, + "name": "AntPool", + "slug": "antpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5764747, + "expectedWeight": 3991786, + "similarity": 0.9029319155137951 + } + }, + { + "id": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "height": 890443, + "version": 706666496, + "timestamp": 1743537197, + "bits": 386038124, + "nonce": 321696065, + "difficulty": 113757508810854, + "merkle_root": "3d7574f7eca741fa94b4690868a242e5b286f8a0417ad0275d4ab05893e96350", + "tx_count": 2155, + "size": 1700002, + "weight": 3993715, + "previousblockhash": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "mediantime": 1743533789, + "stale": false, + "extras": { + "reward": 315112344, + "coinbaseRaw": "034b960d21202020204d696e656420627920536563706f6f6c2020202070000b05e388958c01fabe6d6db7ae4bfa7b1294e16e800b4563f1f5ddeb5c0740319eba45600f3f05d2d7272910000000000000000000c2cb7e020000", + "orphans": [], + "medianFee": 1.4360674424569184, + "feeRange": [ + 1, + 1.0135135135135136, + 1.09717868338558, + 2.142857142857143, + 3.009584664536741, + 4.831858407079646, + 196.07843137254903 + ], + "totalFees": 2612344, + "avgFee": 1212, + "avgFeeRate": 2, + "utxoSetChange": -2880, + "avgTxSize": 788.64, + "totalInputs": 9773, + "totalOutputs": 6893, + "totalOutputAmt": 264603969671, + "segwitTotalTxs": 1933, + "segwitTotalSize": 1556223, + "segwitTotalWeight": 3418707, + "feePercentiles": null, + "virtualSize": 998428.75, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003K–\r! Mined by Secpool p\u0000\u000b\u0005㈕Œ\u0001ú¾mm·®Kú{\u0012”án€\u000bEcñõÝë\\\u0007@1žºE`\u000f?\u0005Ò×')\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÂË~\u0002\u0000\u0000", + "header": "00e01e2acedf6db0523987887ed8b989d4f58b3d6b878a974548010000000000000000005063e99358b04a5d27d07a41a0f886b2e542a2680869b494fa41a7ecf774753d2d44ec676c79021741b12c13", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2623934, + "expectedWeight": 3991917, + "similarity": 0.9951244468050102 + } + }, + { + "id": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "height": 890444, + "version": 671080448, + "timestamp": 1743539347, + "bits": 386038124, + "nonce": 994357124, + "difficulty": 113757508810854, + "merkle_root": "c891d4bf68e22916274b667eb3287d50da2ddd63f8dad892da045cc2ad4a7b21", + "tx_count": 3797, + "size": 1500309, + "weight": 3993525, + "previousblockhash": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "mediantime": 1743533986, + "stale": false, + "extras": { + "reward": 318708524, + "coinbaseRaw": "034c960d082f5669614254432f2cfabe6d6d45b7fd7ab53a0914da7dcc9d21fe44f0936f5354169a56df9d5139f07afbc2b41000000000000000106fc0eb03f0ac2e851d18d8d9f85ad70000000000", + "orphans": [], + "medianFee": 4.064775540157046, + "feeRange": [ + 3.014354066985646, + 3.18368700265252, + 3.602836879432624, + 4.231825525040388, + 5.581730769230769, + 10, + 697.7151162790698 + ], + "totalFees": 6208524, + "avgFee": 1635, + "avgFeeRate": 6, + "utxoSetChange": 5755, + "avgTxSize": 395.02, + "totalInputs": 6681, + "totalOutputs": 12436, + "totalOutputAmt": 835839828101, + "segwitTotalTxs": 3351, + "segwitTotalSize": 1354446, + "segwitTotalWeight": 3410181, + "feePercentiles": null, + "virtualSize": 998381.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003L–\r\b/ViaBTC/,ú¾mmE·ýzµ:\t\u0014Ú}̝!þDð“oST\u0016šVߝQ9ðzû´\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010oÀë\u0003ð¬.…\u001d\u0018ØÙøZ×\u0000\u0000\u0000\u0000\u0000", + "header": "00e0ff27f6f596dc1a210647d530ed3b351b5173428370b2086e02000000000000000000217b4aadc25c04da92d8daf863dd2dda507d28b37e664b271629e268bfd491c8934cec676c79021784af443b", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 73, + "name": "ViaBTC", + "slug": "viabtc", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 6253024, + "expectedWeight": 3991868, + "similarity": 0.9862862477811569 + } + }, + { + "id": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "height": 890445, + "version": 601202688, + "timestamp": 1743539574, + "bits": 386038124, + "nonce": 1647397133, + "difficulty": 113757508810854, + "merkle_root": "61d8294afa8f6bafa4d979a77d187dee5f75a6392f957ea647d96eefbbbc5e9b", + "tx_count": 3579, + "size": 1659862, + "weight": 3993406, + "previousblockhash": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "mediantime": 1743535677, + "stale": false, + "extras": { + "reward": 315617086, + "coinbaseRaw": "034d960d04764dec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f4fac7c451540000000000000", + "orphans": [], + "medianFee": 2.5565329189526835, + "feeRange": [ + 1.521613832853026, + 2, + 2.2411347517730498, + 3, + 3, + 3.954954954954955, + 162.78343949044586 + ], + "totalFees": 3117086, + "avgFee": 871, + "avgFeeRate": 3, + "utxoSetChange": 1881, + "avgTxSize": 463.65000000000003, + "totalInputs": 7893, + "totalOutputs": 9774, + "totalOutputAmt": 324878597485, + "segwitTotalTxs": 3189, + "segwitTotalSize": 1538741, + "segwitTotalWeight": 3509030, + "feePercentiles": null, + "virtualSize": 998351.5, + "coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "coinbaseAddresses": [ + "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38" + ], + "coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3", + "coinbaseSignatureAscii": "\u0003M–\r\u0004vMìg/Foundry USA Pool #dropgold/O¬|E\u0015@\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "00a0d5230d2965fa5bd3e9406f5d665f975d9fc34eae70c46eb3010000000000000000009b5ebcbbef6ed947a67e952f39a6755fee7d187da779d9a4af6b8ffa4a29d861764dec676c7902170d493162", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3145370, + "expectedWeight": 3991903, + "similarity": 0.9903353189076812 + } + }, + { + "id": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "height": 890446, + "version": 537722880, + "timestamp": 1743541107, + "bits": 386038124, + "nonce": 826569764, + "difficulty": 113757508810854, + "merkle_root": "d9b320d7cb5aace80ca20b934b13b4a272121fbdd59f3aaba690e0326ca2c144", + "tx_count": 3998, + "size": 1541360, + "weight": 3993545, + "previousblockhash": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "mediantime": 1743535803, + "stale": false, + "extras": { + "reward": 317976882, + "coinbaseRaw": "034e960d20202020204d696e656420627920536563706f6f6c2020202070001b04fad5fdfefabe6d6d59dd8ebce6e5aab8fb943bbdcede474b6f2d00a395a717970104a6958c17f1ca100000000000000000008089c9350200", + "orphans": [], + "medianFee": 3.3750830641948864, + "feeRange": [ + 2.397163120567376, + 3, + 3, + 3.463647199046484, + 4.49438202247191, + 7.213930348258707, + 476.1904761904762 + ], + "totalFees": 5476882, + "avgFee": 1370, + "avgFeeRate": 5, + "utxoSetChange": 4951, + "avgTxSize": 385.41, + "totalInputs": 7054, + "totalOutputs": 12005, + "totalOutputAmt": 983289729453, + "segwitTotalTxs": 3538, + "segwitTotalSize": 1396505, + "segwitTotalWeight": 3414233, + "feePercentiles": null, + "virtualSize": 998386.25, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003N–\r Mined by Secpool p\u0000\u001b\u0004úÕýþú¾mmYݎ¼æåª¸û”;½ÎÞGKo-\u0000£•§\u0017—\u0001\u0004¦•Œ\u0017ñÊ\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000€‰É5\u0002\u0000", + "header": "00000d208a33fa2f65c6d3662ac962c9bd595147b940f96520400100000000000000000044c1a26c32e090a6ab3a9fd5bd1f1272a2b4134b930ba20ce8ac5acbd720b3d97353ec676c79021724744431", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5601814, + "expectedWeight": 3991928, + "similarity": 0.9537877497871488 + } + }, + { + "id": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "height": 890447, + "version": 568860672, + "timestamp": 1743541240, + "bits": 386038124, + "nonce": 4008077709, + "difficulty": 113757508810854, + "merkle_root": "8c3b098e4e50b67075a4fc52bf4cd603aaa450c240c18a865c9ddc0f27104f5f", + "tx_count": 1919, + "size": 1747789, + "weight": 3993172, + "previousblockhash": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "mediantime": 1743536834, + "stale": false, + "extras": { + "reward": 314435106, + "coinbaseRaw": "034f960d0f2f736c7573682f65000002fba05ef1fabe6d6df8d29032ea6f9ab1debd223651f30887df779c6195869e70a7b787b3a15f4b1710000000000000000000ee5f0c00b20200000000", + "orphans": [], + "medianFee": 1.4653828213500366, + "feeRange": [ + 1.0845070422535212, + 1.2, + 1.51, + 2.0141129032258065, + 2.3893805309734515, + 4.025477707006369, + 300.0065359477124 + ], + "totalFees": 1935106, + "avgFee": 1008, + "avgFeeRate": 1, + "utxoSetChange": -4244, + "avgTxSize": 910.58, + "totalInputs": 9909, + "totalOutputs": 5665, + "totalOutputAmt": 210763861504, + "segwitTotalTxs": 1720, + "segwitTotalSize": 1629450, + "segwitTotalWeight": 3519924, + "feePercentiles": null, + "virtualSize": 998293, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003O–\r\u000f/slush/e\u0000\u0000\u0002û ^ñú¾mmøÒ2êoš±Þ½\"6Qó\b‡ßwœa•†žp§·‡³¡_K\u0017\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000î_\f\u0000²\u0002\u0000\u0000\u0000\u0000", + "header": "0020e8216b30f547b3d47824c1cb06db9221fcd04621b0263dfe010000000000000000005f4f10270fdc9d5c868ac140c250a4aa03d64cbf52fca47570b6504e8e093b8cf853ec676c7902178d69e6ee", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2059571, + "expectedWeight": 3991720, + "similarity": 0.9149852183486826 + } + } + ], + "mempool-blocks": [ + { + "blockSize": 1779311, + "blockVSize": 997968.5, + "nTx": 2132, + "totalFees": 2902870, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0721153846153846, + 1.9980563654033041, + 2.2195704057279237, + 3.009493670886076, + 3.4955223880597015, + 6.0246913580246915, + 218.1818181818182 + ] + }, + { + "blockSize": 1959636, + "blockVSize": 997903.5, + "nTx": 497, + "totalFees": 1093076, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0401794819498267, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0761096766260911, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477260, + "blockVSize": 997997.25, + "nTx": 720, + "totalFees": 1016195, + "medianFee": 1.007409072434199, + "feeRange": [ + 1, + 1.0019120458891013, + 1.0040863981319323, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0485018498190837 + ] + }, + { + "blockSize": 1021308, + "blockVSize": 431071.5, + "nTx": 823, + "totalFees": 432342, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0028011204481793, + 1.0042075736325387, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json new file mode 100644 index 0000000000..47a685757c --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_mempool_blocks_01.json @@ -0,0 +1,68 @@ +{ + "mempool-blocks": [ + { + "blockSize": 1780038, + "blockVSize": 997989.75, + "nTx": 2134, + "totalFees": 2919589, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0101010101010102, + 2, + 2.235576923076923, + 3.010452961672474, + 3.5240274599542336, + 6.032085561497326, + 218.1818181818182 + ] + }, + { + "blockSize": 1958446, + "blockVSize": 997996, + "nTx": 503, + "totalFees": 1093277, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0101010101010102, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.067677314564158, + 1.0761096766260911, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477611, + "blockVSize": 997927.5, + "nTx": 725, + "totalFees": 1016311, + "medianFee": 1.0075971559364956, + "feeRange": [ + 1, + 1.0019334049409236, + 1.0042075736325387, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0548148148148149 + ] + }, + { + "blockSize": 1028219, + "blockVSize": 435137, + "nTx": 833, + "totalFees": 436414, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0028011204481793, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json new file mode 100644 index 0000000000..48d2fe01e5 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json @@ -0,0 +1,1235 @@ +{ + "stratumJobs": { + "2": { + "jobId": "1743541734_2517178", + "extraNonce": "00004237", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d142f756c74696d75732f373833e5005202fe934b78fabe6d6d4bec519f2060f58bc5ba10289fbedb1f9456cc864a15f153cbfed624324382181000000000000000", + "coinbase2": "ffffffff05220200000000000017a914332c82217820c32fb9c107b67356cfdc8f8da6a6876271cc120000000017a91472c52c9c71c5f644cf6e712661710503e65e69ad870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501ebbaf365b0d5fa072e2b2429db23696291f2c0383d81f61600af32dd72a5ac21aaf1274a3862af6d00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541734950, + "pool": 2, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d142f756c74696d75732f373833e5005202fe934b78fabe6d6d4bec519f2060f58bc5ba10289fbedb1f9456cc864a15f153cbfed624324382181000000000000000000042370000000000000000ffffffff05220200000000000017a914332c82217820c32fb9c107b67356cfdc8f8da6a6876271cc120000000017a91472c52c9c71c5f644cf6e712661710503e65e69ad870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501ebbaf365b0d5fa072e2b2429db23696291f2c0383d81f61600af32dd72a5ac21aaf1274a3862af6d00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315388804, + "scriptsig": "0350960d142f756c74696d75732f373833e5005202fe934b78fabe6d6d4bec519f2060f58bc5ba10289fbedb1f9456cc864a15f153cbfed624324382181000000000000000000042370000000000000000" + }, + "4": { + "jobId": "450915", + "extraNonce": "00", + "extraNonce2Size": 7, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e6564206279204c75786f72205465636868005702fe933c3bfabe6d6d8ae1255df7029c6f68cd8ea446d227ef3dc39ef1ac7a55085969307c7bfaa736100000000000000000005813", + "coinbase2": "ffffffff05220200000000000017a914bf73ad4cf3a107812bad3deb310611bee49a3c79875173cc120000000017a914056adde53ebc396a1b3b678bb0d3a5c116ff430c870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a37cf4faa0758b26dca666f3e36d42fa15cc0106f459cc4ca322d298304ff163b2a360d756c5db8400000000000000002b6a2952534b424c4f434b3a1357f52232e7585b34c3e0cd5d433a50aac01937aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "71b686070fe7bdfaf6a8812ab487fd91d15e21c90fcf5d6bafca745e9b474a19", + "802911f2f657223b6cf06eec9b523b005b6bc2bec56b8fb35d777ef275398ba7", + "7a2626e535df0e88542c11e2c939de4d49e29a654da437bdbbf9d4b89ad8cea1", + "c49488289a1335c2bcf24b8661ce01ace0e6c35e0105a116fb55ccf981e46c59", + "8f8908b51423dc8ea13a97524f4159d629754f89c8ddfab140b8c695442215f3", + "48cc791e2e2fd0d16c9bf774967162a333ee2eeeedbace83f9ac5368bbc1dc97", + "8e25b97a71a9121cbec116aae71825cc1f8679687c44ef7dd94d2841244e16d9", + "e5328ff6cc72161b5c5e97152a4544f3900ac0a63abde9564919ffa30e28505d", + "3f12570807f882825eaa97b598308bd27e7f36d8ba79e039d81dcfe6cbf1fdd6", + "6afcf4b7a3fd41e8b6290459722bf83f0aff5a36b2b54e9f4210f9cb8bef51b9", + "1a9bdc30e04b9705106827938046b8485384b584254c2f93afdc50ed42a189b2" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541735146, + "pool": 4, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e6564206279204c75786f72205465636868005702fe933c3bfabe6d6d8ae1255df7029c6f68cd8ea446d227ef3dc39ef1ac7a55085969307c7bfaa7361000000000000000000058130000000000000000ffffffff05220200000000000017a914bf73ad4cf3a107812bad3deb310611bee49a3c79875173cc120000000017a914056adde53ebc396a1b3b678bb0d3a5c116ff430c870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a37cf4faa0758b26dca666f3e36d42fa15cc0106f459cc4ca322d298304ff163b2a360d756c5db8400000000000000002b6a2952534b424c4f434b3a1357f52232e7585b34c3e0cd5d433a50aac01937aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315389299, + "scriptsig": "0350960d1b4d696e6564206279204c75786f72205465636868005702fe933c3bfabe6d6d8ae1255df7029c6f68cd8ea446d227ef3dc39ef1ac7a55085969307c7bfaa7361000000000000000000058130000000000000000" + }, + "36": { + "jobId": "B75cp1aWq", + "extraNonce": "00", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff640350960d2cfabe6d6da35ece7974edab96759db21f0953f4eaa2200241771a0976d18c5c6e27c5340a10000000f09f909f092f4632506f6f6c2f6b000000000000000000000000000000000000000000000000000000000000000000000005", + "coinbase2": "0722020000000000001976a914c6740a12d0a7d556f89782bf5faf0e12cf25a63988ac3a4acb12000000001976a91469f2a01f4ff9e6ac24df9062e9828753474b348088ac0000000000000000266a24aa21a9ed534cb114f7fd20c22a47d45e6bb6c5460bf3a9cce404265e0961cfd6c881190700000000000000002f6a2d434f5245017c706ca44a28fdd25761250961276bd61d5aa87be7ec323813c943336c579e238228a8ebd096a7e50000000000000000126a10455853415401051b0f0e0e0b1f1200130000000000000000266a2448617468241c617362765014f47be645944e22747340fba7c5a705548dbae9b4a84e62ff00000000000000002c6a4c2952534b424c4f434b3a81cc66e44e74bd510bfde3b83b286662e6a2e597aa8a53c435370b130070fc91dc608334", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "723847223fbee3db7918859bb384999905cba259070859ec77ebfa329b05065a", + "cd7913bf8e499b13ad83c84b2dcc5de216823ff59e45cc76af8f47570114476e", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "6c30f7579f6b6734d6ce96b86e33d318513bafeee287870a960c5a7081244a0c", + "aac144dd768fb69fb813f0eebdd02afb8b55e00d0a61101cf167ecda07a1bc2b", + "ed81f05edaf48584c328a7d4c1f2de3720c2eaba0fa7aa37e4f0792603c60b67", + "98ce77b30480722596a787892493dfdac342b5c717aa5d2a3a4ca1a4b461321c", + "d1372489dd7304c4c44214da63b071c987ee084440861140fb56d5ea03ba8e39", + "efeeb673f474c3d4724bf2503a0903ff73ff01afabc37ffa694f8c79022d7078" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55cb", + "cleanJobs": false, + "received": 1743541719279, + "pool": 36, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff640350960d2cfabe6d6da35ece7974edab96759db21f0953f4eaa2200241771a0976d18c5c6e27c5340a10000000f09f909f092f4632506f6f6c2f6b0000000000000000000000000000000000000000000000000000000000000000000000050000000000000000000722020000000000001976a914c6740a12d0a7d556f89782bf5faf0e12cf25a63988ac3a4acb12000000001976a91469f2a01f4ff9e6ac24df9062e9828753474b348088ac0000000000000000266a24aa21a9ed534cb114f7fd20c22a47d45e6bb6c5460bf3a9cce404265e0961cfd6c881190700000000000000002f6a2d434f5245017c706ca44a28fdd25761250961276bd61d5aa87be7ec323813c943336c579e238228a8ebd096a7e50000000000000000126a10455853415401051b0f0e0e0b1f1200130000000000000000266a2448617468241c617362765014f47be645944e22747340fba7c5a705548dbae9b4a84e62ff00000000000000002c6a4c2952534b424c4f434b3a81cc66e44e74bd510bfde3b83b286662e6a2e597aa8a53c435370b130070fc91dc608334", + "height": 890448, + "timestamp": 1743541707, + "reward": 315313244, + "scriptsig": "0350960d2cfabe6d6da35ece7974edab96759db21f0953f4eaa2200241771a0976d18c5c6e27c5340a10000000f09f909f092f4632506f6f6c2f6b0000000000000000000000000000000000000000000000000000000000000000000000050000000000" + }, + "43": { + "jobId": "131d", + "extraNonce": "00", + "extraNonce2Size": 6, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c0350960d0f2f736c7573682f65000303fe9334c4fabe6d6dbb9c929fe497398b9a628aa161b72ed26b10e8ca5f29c485f42b468355043a15100000000000000000004e802e", + "coinbase2": "ffffffff038473cc120000000017a9141f0cbbec8bc4c945e4e16249b11eee911eded55f870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541734908, + "pool": 43, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c0350960d0f2f736c7573682f65000303fe9334c4fabe6d6dbb9c929fe497398b9a628aa161b72ed26b10e8ca5f29c485f42b468355043a15100000000000000000004e802e00000000000000ffffffff038473cc120000000017a9141f0cbbec8bc4c945e4e16249b11eee911eded55f870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315388804, + "scriptsig": "0350960d0f2f736c7573682f65000303fe9334c4fabe6d6dbb9c929fe497398b9a628aa161b72ed26b10e8ca5f29c485f42b468355043a15100000000000000000004e802e00000000000000" + }, + "44": { + "jobId": "3750742", + "extraNonce": "00002c10", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e656420627920416e74506f6f6c3830369c001700fe93b694fabe6d6d433177d6fe558d8cee3035bb1b481156f8b411c21bddef7fb8aec740e4617c501000000000000000", + "coinbase2": "ffffffff06220200000000000017a91442402a28dd61f2718a4b27ae72a4791d5bbdade7876271cc120000000017a9145249bdf2c131d43995cff42e8feee293f79297a8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d54e3ecda72cb7961caa4b541b1e322bcfe0b5a0300000000000000000146a12455853415401000d130f0e0e0b041f12001300000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e7", + "cleanJobs": false, + "received": 1743541735750, + "pool": 44, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff580350960d1b4d696e656420627920416e74506f6f6c3830369c001700fe93b694fabe6d6d433177d6fe558d8cee3035bb1b481156f8b411c21bddef7fb8aec740e4617c50100000000000000000002c100000000000000000ffffffff06220200000000000017a91442402a28dd61f2718a4b27ae72a4791d5bbdade7876271cc120000000017a9145249bdf2c131d43995cff42e8feee293f79297a8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d54e3ecda72cb7961caa4b541b1e322bcfe0b5a0300000000000000000146a12455853415401000d130f0e0e0b041f12001300000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541735, + "reward": 315388804, + "scriptsig": "0350960d1b4d696e656420627920416e74506f6f6c3830369c001700fe93b694fabe6d6d433177d6fe558d8cee3035bb1b481156f8b411c21bddef7fb8aec740e4617c50100000000000000000002c100000000000000000" + }, + "49": { + "jobId": "67444bb00005e280", + "extraNonce": "1eecec72", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff350350960d0004d155ec67047a5741090c", + "coinbase2": "0a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672fffffffff03dd276b12000000001976a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac483a60000000000016001451ed61d2f6aa260cc72cdf743e4e436a82c010270000000000000000266a24aa21a9ed6c1f16b33ae5c1c628b53c6011f38866955c8b59448e5aee14f9967c8cb7ab1c00000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "723847223fbee3db7918859bb384999905cba259070859ec77ebfa329b05065a", + "cd7913bf8e499b13ad83c84b2dcc5de216823ff59e45cc76af8f47570114476e", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "6c30f7579f6b6734d6ce96b86e33d318513bafeee287870a960c5a7081244a0c", + "717d5dac15b3253aeec8e1079141760337bebe0101d0b4e4c43384416ca132ad", + "9eba7f7def1ad83bb3d7df151f880fa76c4b4ec3b531a4e0856a1e7437d3d060", + "0558088354e8bf625f19e28e6ec02231d61a31c26f215d2e1253baf0452064bb", + "6c25959fb0244719945408c5da48055f0d9ad30240ebc520c2d81b5df1b0f3d7", + "150163eb46086007fafbef5c2c71faf3f8b4690c338e2d74a966ff2650df63c3" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55d1", + "cleanJobs": false, + "received": 1743541713511, + "pool": 49, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff350350960d0004d155ec67047a5741090c1eecec7200000000000000000a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672fffffffff03dd276b12000000001976a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac483a60000000000016001451ed61d2f6aa260cc72cdf743e4e436a82c010270000000000000000266a24aa21a9ed6c1f16b33ae5c1c628b53c6011f38866955c8b59448e5aee14f9967c8cb7ab1c00000000", + "height": 890448, + "timestamp": 1743541713, + "reward": 315318821, + "scriptsig": "0350960d0004d155ec67047a5741090c1eecec7200000000000000000a636b706f6f6c112f736f6c6f2e636b706f6f6c2e6f72672f" + }, + "73": { + "jobId": "8711", + "extraNonce": "41fade6a", + "extraNonce2Size": 4, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff610350960d1e2f5669614254432f4d696e6564206279206d6f6e6f6e6175746963616c2f2cfabe6d6d0d586d106170cc5c7380e81f3e79c40726b2313bdb6263eedba507d9cdc53bca1000000000000000101187c609caa65f88", + "coinbase2": "ffffffff040388cb12000000001976a914fb37342f6275b13936799def06f2eb4c0f20151588ac00000000000000002b6a2952534b424c4f434b3a280a7381d0ccac30cd6dcfd0beae78f22cb58d6caa8a53c435370b120070fc920000000000000000146a124558534154011508000113021b1a1f1200130000000000000000266a24aa21a9ed10c31ffb387781fdab75b9b2e4994223622049c78f1f81d05a4a421ae178626000000000", + "merkleBranches": [ + "b4efa4f5b9d41f9fc63480038ba7586dac490d08fd738aaa691d49bcbee1dd54", + "97c694c3a659a43a7ed8f7e2ae4d06927a152b348f33e7a51c1b5dd242631e72", + "09350f4bc26ad644fadeec9bea500daadb35bf43f8f39d82a8adb708661a8f18", + "e5f3663e21ccc3f4565529a73b11811dd68abcc9ac701aa494b7b18782eefa23", + "bcdfb143b322e8677b8edb90417a1ec739a6d9eb8efc84e5ba6424e7f3adfc0f", + "b90794158874321306985105e45aa872a9646a5bf683a070d7ca6602450ad8e5", + "26e4fb36ec030a6510c9047120575dca2a8b8760674af4284f5c27e53f03f46b", + "e6a15e522b650f9cebfdd003c0d29f5ca571bf704d63b9aa627ce915acb229f7", + "392d3e593f80e9180a3bce84ebaa044d5406cad39897c78e58e66ca59e7499c8", + "ce8409542b60ed5ca9c567b89fa9a7c2e5e8e2bbf2b42b0ce8edb0836a7a2e82", + "0e8201e0367364ed40263747547d3a0c26926d2b94799b414c22a25c6fc04d43", + "33e95cd9da59d9e33e239b9b4e647a2da98f36ef6a137f40ba428782bb1c4021" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55d7", + "cleanJobs": false, + "received": 1743541719896, + "pool": 73, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff610350960d1e2f5669614254432f4d696e6564206279206d6f6e6f6e6175746963616c2f2cfabe6d6d0d586d106170cc5c7380e81f3e79c40726b2313bdb6263eedba507d9cdc53bca1000000000000000101187c609caa65f8841fade6a00000000ffffffff040388cb12000000001976a914fb37342f6275b13936799def06f2eb4c0f20151588ac00000000000000002b6a2952534b424c4f434b3a280a7381d0ccac30cd6dcfd0beae78f22cb58d6caa8a53c435370b120070fc920000000000000000146a124558534154011508000113021b1a1f1200130000000000000000266a24aa21a9ed10c31ffb387781fdab75b9b2e4994223622049c78f1f81d05a4a421ae178626000000000", + "height": 890448, + "timestamp": 1743541719, + "reward": 315328515, + "scriptsig": "0350960d1e2f5669614254432f4d696e6564206279206d6f6e6f6e6175746963616c2f2cfabe6d6d0d586d106170cc5c7380e81f3e79c40726b2313bdb6263eedba507d9cdc53bca1000000000000000101187c609caa65f8841fade6a00000000" + }, + "94": { + "jobId": "1186343", + "extraNonce": "0000776f", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d1a2f706f6f6c696e2e636f6d2f66702f3936369c01b704fe93ba97fabe6d6d89255a8f9c17d3a3dc753a3e2b2eb55a116c1d7280bcf4b4f76a0af1f2e12ce31000000000000000", + "coinbase2": "ffffffff038473cc120000000017a9141366dca425687186b9b54e27e4ed48163b5beb80870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e7", + "cleanJobs": false, + "received": 1743541735127, + "pool": 94, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d1a2f706f6f6c696e2e636f6d2f66702f3936369c01b704fe93ba97fabe6d6d89255a8f9c17d3a3dc753a3e2b2eb55a116c1d7280bcf4b4f76a0af1f2e12ce310000000000000000000776f0000000000000000ffffffff038473cc120000000017a9141366dca425687186b9b54e27e4ed48163b5beb80870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541735, + "reward": 315388804, + "scriptsig": "0350960d1a2f706f6f6c696e2e636f6d2f66702f3936369c01b704fe93ba97fabe6d6d89255a8f9c17d3a3dc753a3e2b2eb55a116c1d7280bcf4b4f76a0af1f2e12ce310000000000000000000776f0000000000000000" + }, + "102": { + "jobId": "33", + "extraNonce": "04264b21", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d04d655ec67537069646572506f6f6c2f7573383838382ffabe6d6d013135a66fcdf604440c12e28ffb6f448d1d7b633d16737217081646c41912270100000000000000b286b3fc", + "coinbase2": "ffffffff04b55dcb12000000001976a914717a4c9074577a05af94271c32b249d298a22d9888ac0000000000000000266a24aa21a9eded8df9e053ff66fe09268a9df4e369d60a48437a012a1b157e143bb967379cab00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d596a6689031f48a857d344e1a42fdb272bb15d6210000000000000000126a10455853415401120f080304111f12001300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "723847223fbee3db7918859bb384999905cba259070859ec77ebfa329b05065a", + "cd7913bf8e499b13ad83c84b2dcc5de216823ff59e45cc76af8f47570114476e", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "6c30f7579f6b6734d6ce96b86e33d318513bafeee287870a960c5a7081244a0c", + "717d5dac15b3253aeec8e1079141760337bebe0101d0b4e4c43384416ca132ad", + "9eba7f7def1ad83bb3d7df151f880fa76c4b4ec3b531a4e0856a1e7437d3d060", + "5fb573157a34ed041274de444f9dd86e210189cf325bdbde44a92edded8f1b90", + "1e77c2e67bf442dd78a2e078fe9b7bb1bece599bf7e77e2086e5d0659afedf9e", + "e6c75e3d1859a1d8d6a10ac5fe990969202fa37d0715c7ca50c2159e3b3a463e" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55ce", + "cleanJobs": false, + "received": 1743541719031, + "pool": 102, + "coinbase": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff570350960d04d655ec67537069646572506f6f6c2f7573383838382ffabe6d6d013135a66fcdf604440c12e28ffb6f448d1d7b633d16737217081646c41912270100000000000000b286b3fc04264b210000000000000000ffffffff04b55dcb12000000001976a914717a4c9074577a05af94271c32b249d298a22d9888ac0000000000000000266a24aa21a9eded8df9e053ff66fe09268a9df4e369d60a48437a012a1b157e143bb967379cab00000000000000002f6a2d434f52450164db24a662e20bbdf72d1cc6e973dbb2d12897d596a6689031f48a857d344e1a42fdb272bb15d6210000000000000000126a10455853415401120f080304111f12001300000000", + "height": 890448, + "timestamp": 1743541710, + "reward": 315317685, + "scriptsig": "0350960d04d655ec67537069646572506f6f6c2f7573383838382ffabe6d6d013135a66fcdf604440c12e28ffb6f448d1d7b633d16737217081646c41912270100000000000000b286b3fc04264b210000000000000000" + }, + "105": { + "jobId": "4287955", + "extraNonce": "0000e9a5", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff500350960d1362696e616e63652f3832319c001606fe93bd58fabe6d6d2cbf0bfed0a1d41f829160053963a92e7a2c44942646e992955ce00fd53f62991000000000000000", + "coinbase2": "ffffffff05220200000000000017a914265ae1340f5d442d099ffc27b443ffdba4bc0346876271cc120000000017a9149e3e8a50d9acb3ac7649625432e2207c25e0faf8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f5245012d058b58dcf4b0db11168c62d3109f6e02710b02221456a6c24c9891680d295eb6bc57c6fb68e8f100000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e7", + "cleanJobs": false, + "received": 1743541735177, + "pool": 105, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff500350960d1362696e616e63652f3832319c001606fe93bd58fabe6d6d2cbf0bfed0a1d41f829160053963a92e7a2c44942646e992955ce00fd53f629910000000000000000000e9a50000000000000000ffffffff05220200000000000017a914265ae1340f5d442d099ffc27b443ffdba4bc0346876271cc120000000017a9149e3e8a50d9acb3ac7649625432e2207c25e0faf8870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f5245012d058b58dcf4b0db11168c62d3109f6e02710b02221456a6c24c9891680d295eb6bc57c6fb68e8f100000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541735, + "reward": 315388804, + "scriptsig": "0350960d1362696e616e63652f3832319c001606fe93bd58fabe6d6d2cbf0bfed0a1d41f829160053963a92e7a2c44942646e992955ce00fd53f629910000000000000000000e9a50000000000000000" + }, + "111": { + "jobId": "7", + "extraNonce": "31a044e7", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff310350960d04e155ec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f", + "coinbase2": "ffffffff0522020000000000002251200f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a37f28cc12000000002200207086320071974eef5e72eaa01dd9096e10c0383483855ea6b344259c244f73c20000000000000000266a24aa21a9eda9d49baa733153f684d3e885e731818571f650adfef974d99fe1e11b69af0bc600000000000000002f6a2d434f524501359b5fbc5b294e953dbec5cbb769e2186bb30e56e6d18fda214e5b9f350ffc7b6cf3058b9026e76500000000000000002b6a2952534b424c4f434b3aaadde8fc40282f7f5a2c02a16700e42ad9f6bc5aaa8a53c435370b120070fc9200000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "470ea829e1e3085254952441894afcb97582e5675fbb35c226edf79a226890d5", + "09a4875a0b37d78e9643154473bb974b2eba5c800e3cd58c99adfb196f383358", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "2af55ecd2164a88f5df8c3de727633690dabb2de4b61827ac86adcd927cd3e24", + "8601c35a421ac857f22a1ea05b53f003771aaaaf1e65bea339bdc80a9986e671", + "230d09f9ee198e35e1b1be1de2f87acf1a1e73690d680b85ba96bd21790e8766", + "8f4cee6b0fc9bca8608134c4a93116cf4de4ab753863219201cd20ff06b18d1e", + "4de04f813d0c4fbd5b4fb79e490f76ba4da31391d25f0447f1e9eccc935c9fe4", + "c3de6f84d58eeb5277be653e241331dddd19d056ce00ff7d3d9ecdefadfa9067" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e1", + "cleanJobs": false, + "received": 1743541729815, + "pool": 111, + "coinbase": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff310350960d04e155ec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f31a044e70000000000000000ffffffff0522020000000000002251200f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a37f28cc12000000002200207086320071974eef5e72eaa01dd9096e10c0383483855ea6b344259c244f73c20000000000000000266a24aa21a9eda9d49baa733153f684d3e885e731818571f650adfef974d99fe1e11b69af0bc600000000000000002f6a2d434f524501359b5fbc5b294e953dbec5cbb769e2186bb30e56e6d18fda214e5b9f350ffc7b6cf3058b9026e76500000000000000002b6a2952534b424c4f434b3aaadde8fc40282f7f5a2c02a16700e42ad9f6bc5aaa8a53c435370b120070fc9200000000", + "height": 890448, + "timestamp": 1743541729, + "reward": 315370145, + "scriptsig": "0350960d04e155ec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f31a044e70000000000000000" + }, + "112": { + "jobId": "12", + "extraNonce": "050300c0", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff290350960d04db55ec672f53424943727970746f2e636f6d20506f6f6c2f", + "coinbase2": "ffffffff02ffc8cb1200000000160014cab30e9b367d646f326ace7fcaf3c9ce4afc37530000000000000000266a24aa21a9edd0dd8d14ec5a97c6fd3de15f02c86d889e932e0262807795fa7c127e5a8fa0be00000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "b41e3afa7672a36d6a462612f3bdc9ee8000388471b29ba1d187135b435f3efa", + "4236249ae2883af9d68c5570f893aa808a2a15d61ea654444cebfb9b3ba2c598", + "2cec318ccfb9046dd1b929e52e7d131cfbf0499fa0f17470944bd0a39d7392a7", + "98531b18c2ce0bfd02dbc7efc9463a4a891cb30c3dda244ebc17d34a52e6e2b9", + "66a73af7f957129069b86ec454ab2e8ac4399e2f6549d6e0e4367844abc8d546", + "4b5b7ee5fe7813f0e1ae5d1265215062d449d5c50b5edede2816274216e88a8d", + "6624f27663e2330c010fe68f5f397ebef70a1f6188ccb104e9eff97a9e48bb02", + "b94d8d0e4cc4b8b62bd4f4d70fc4497f85b05280b43e0b5dc399cc8778a1074a", + "a97d374c628f07cc5283b577558c3c0e3f2f3c8dac8df6ccdea7cf26b6776fa5", + "190f5f1bc8535d731fd095dd8e97ec3c33facdbce8614ca654017f3baf84a3fe", + "102b7769fe8b53ca163151eb461c767d331c465d95056f556e38a07298e6046a" + ], + "version": "20000004", + "bits": "1702796c", + "time": "67ec55db", + "cleanJobs": false, + "received": 1743541728640, + "pool": 112, + "coinbase": "02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff290350960d04db55ec672f53424943727970746f2e636f6d20506f6f6c2f050300c00000000000000000ffffffff02ffc8cb1200000000160014cab30e9b367d646f326ace7fcaf3c9ce4afc37530000000000000000266a24aa21a9edd0dd8d14ec5a97c6fd3de15f02c86d889e932e0262807795fa7c127e5a8fa0be00000000", + "height": 890448, + "timestamp": 1743541723, + "reward": 315345151, + "scriptsig": "0350960d04db55ec672f53424943727970746f2e636f6d20506f6f6c2f050300c00000000000000000" + }, + "141": { + "jobId": "1832214", + "extraNonce": "0000ac40", + "extraNonce2Size": 4, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d184d696e656420627920536563506f6f6c58007904fe939454fabe6d6d2e07daf9ae89d5bfcdb87dd64455e7357ca9fb3708a2cf08ca8f30a38142a68e1000000000000000", + "coinbase2": "ffffffff05220200000000000017a9148ee90177614ecde53314fd67c46162f315852a07875173cc120000000017a9146582f2551e2a47e1ae8b03fb666401ed7c4552ef870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a21cbd3caa4fe89bccd1d716c92ce4533e4d4733942c01262fa046fdb8ba0dfce3753405c74aed0e00000000000000002b6a2952534b424c4f434b3a0374e206447243d32da96205942c62a298fddbd7aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "71b686070fe7bdfaf6a8812ab487fd91d15e21c90fcf5d6bafca745e9b474a19", + "802911f2f657223b6cf06eec9b523b005b6bc2bec56b8fb35d777ef275398ba7", + "7a2626e535df0e88542c11e2c939de4d49e29a654da437bdbbf9d4b89ad8cea1", + "c49488289a1335c2bcf24b8661ce01ace0e6c35e0105a116fb55ccf981e46c59", + "8f8908b51423dc8ea13a97524f4159d629754f89c8ddfab140b8c695442215f3", + "48cc791e2e2fd0d16c9bf774967162a333ee2eeeedbace83f9ac5368bbc1dc97", + "8e25b97a71a9121cbec116aae71825cc1f8679687c44ef7dd94d2841244e16d9", + "e5328ff6cc72161b5c5e97152a4544f3900ac0a63abde9564919ffa30e28505d", + "3f12570807f882825eaa97b598308bd27e7f36d8ba79e039d81dcfe6cbf1fdd6", + "6afcf4b7a3fd41e8b6290459722bf83f0aff5a36b2b54e9f4210f9cb8bef51b9", + "1a9bdc30e04b9705106827938046b8485384b584254c2f93afdc50ed42a189b2" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541735015, + "pool": 141, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff510350960d184d696e656420627920536563506f6f6c58007904fe939454fabe6d6d2e07daf9ae89d5bfcdb87dd64455e7357ca9fb3708a2cf08ca8f30a38142a68e10000000000000000000ac4000000000ffffffff05220200000000000017a9148ee90177614ecde53314fd67c46162f315852a07875173cc120000000017a9146582f2551e2a47e1ae8b03fb666401ed7c4552ef870000000000000000266a24aa21a9ed0844cc9ee7cc307219c6002e18930cdde972a552bc8d1eabad7976208a0e1b8000000000000000002f6a2d434f524501a21cbd3caa4fe89bccd1d716c92ce4533e4d4733942c01262fa046fdb8ba0dfce3753405c74aed0e00000000000000002b6a2952534b424c4f434b3a0374e206447243d32da96205942c62a298fddbd7aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315389299, + "scriptsig": "0350960d184d696e656420627920536563506f6f6c58007904fe939454fabe6d6d2e07daf9ae89d5bfcdb87dd64455e7357ca9fb3708a2cf08ca8f30a38142a68e10000000000000000000ac4000000000" + }, + "142": { + "jobId": "67ec55c62cc0f502", + "extraNonce": "b10cf017", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1e0350960d163c204f4345414e2e58595a203e0f0f30304f43423400024087ffffffff140000000000000000166a144f4342313fd6fdce13000000290000002c029c30562b26000000000016001472fb714ecb210311655a3da22729ff8626bdea590000000000000000106a0e1a27", + "coinbase2": "f65ccf020000000017a9142b636a84e5b12177cd49c687b5a1ebd07fb83fbf8783c76e0200000000220020ac11a29a5721e9602e396acd7e5216c9196b7dbb14cf573921c46e6fea38ac09d6632e020000000017a91449b58fb476449dab3a2e175fe2c1f0d753a33e92878b8ad9010000000017a914cc888b5846746a5a7f449eea90082feaa09b9bf387833cbe01000000001600146983f7fe983e96dc5b43677fb14822a075596364b136ae0100000000220020dc406857cac1b9d3f53c6986acb78e7774c604281076d10d3fc8e873e18df6d0663e6e0100000000160014d192719c5be868068f023b9383b3cbecb6afbb1639205300000000001976a91412c00ad1271bd4c91e4f8ff11e738c285181399d88ac49844e000000000017a9141ef31e9bc9ccb532b8c01950727990cca190656a877aa148000000000016001404afd9f6e36ba7cadb62133c458a5318f41a33de731b42000000000016001415c0899a887e36f6620619b621855d40b41cbf838b5a3b0000000000220020e65791d3c340710f7aebf117d1f38e1c5383c1df0345a2082e8ebff945e9994646d83a00000000001600140345e587cb42c201c30e505ea9f6f5fb935876ac5df731000000000017a914278dadd16d1314122e4dde67ff9152a61c3a9f4787254f3000000000001600146d095474bec41ccc8ad40f94c74dd25b79b83579e85b7e020000000017a914413b5901fe4e591c95405fd446b5b002da575bf0870000000000000000266a24aa21a9ed2a3f54b40a557c2884545b770a050154a46b5dc132eec3604f01714f413b32a300000000", + "merkleBranches": [ + "2b797d78bf7de288eccd666e7b81bf04bc96beb0672b6ece12a8e66c8ab3e989", + "81890975bdb859a5c5527a40a7354d0d60b133fa4eaef69f1738ece447714e2c", + "a7fe518c62a02f13cc9e18d4622af75b5fcd4ebad10f129ea0e9eee165cae6b5", + "626773fea3f749684748ec54e167fceb58e3520cbdcf7926717ab7d3b481a2f1", + "de27b0f244aeed874d5b8e03730e7c386905016563058919a476a7efa0414921", + "52903b24bb6d22b7df633c6a426d014cab6c592069043d51ec4b9a910bb98c3e", + "0834ec8e29e9cea5ff2b05cea6f5ee6ce71088569a44fac069c94f777366e806", + "f792f333b3687ca868e80449265720091bcc29a8bd986b5e5d35a297986f9ea2", + "6ea6eca3a29cf603146043e1e5d6bb20e0460dbc72157bedfe585bf38c7e6c56", + "36fe42124e6a5e5250af65cc12e6d7b6370793e559b5fc76b7a46760a83396d1", + "f1c2b5b11af512e743b9c9f219192dc82b8826760b4678683ea16303e2b65508" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55c5", + "cleanJobs": false, + "received": 1743541714701, + "pool": 142, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1e0350960d163c204f4345414e2e58595a203e0f0f30304f43423400024087ffffffff140000000000000000166a144f4342313fd6fdce13000000290000002c029c30562b26000000000016001472fb714ecb210311655a3da22729ff8626bdea590000000000000000106a0e1a27b10cf0170000000000000000f65ccf020000000017a9142b636a84e5b12177cd49c687b5a1ebd07fb83fbf8783c76e0200000000220020ac11a29a5721e9602e396acd7e5216c9196b7dbb14cf573921c46e6fea38ac09d6632e020000000017a91449b58fb476449dab3a2e175fe2c1f0d753a33e92878b8ad9010000000017a914cc888b5846746a5a7f449eea90082feaa09b9bf387833cbe01000000001600146983f7fe983e96dc5b43677fb14822a075596364b136ae0100000000220020dc406857cac1b9d3f53c6986acb78e7774c604281076d10d3fc8e873e18df6d0663e6e0100000000160014d192719c5be868068f023b9383b3cbecb6afbb1639205300000000001976a91412c00ad1271bd4c91e4f8ff11e738c285181399d88ac49844e000000000017a9141ef31e9bc9ccb532b8c01950727990cca190656a877aa148000000000016001404afd9f6e36ba7cadb62133c458a5318f41a33de731b42000000000016001415c0899a887e36f6620619b621855d40b41cbf838b5a3b0000000000220020e65791d3c340710f7aebf117d1f38e1c5383c1df0345a2082e8ebff945e9994646d83a00000000001600140345e587cb42c201c30e505ea9f6f5fb935876ac5df731000000000017a914278dadd16d1314122e4dde67ff9152a61c3a9f4787254f3000000000001600146d095474bec41ccc8ad40f94c74dd25b79b83579e85b7e020000000017a914413b5901fe4e591c95405fd446b5b002da575bf0870000000000000000266a24aa21a9ed2a3f54b40a557c2884545b770a050154a46b5dc132eec3604f01714f413b32a300000000", + "height": 890448, + "timestamp": 1743541701, + "reward": 315238004, + "scriptsig": "0350960d163c204f4345414e2e58595a203e0f0f30304f43423400024087" + }, + "161": { + "jobId": "1611979", + "extraNonce": "000002dc", + "extraNonce2Size": 8, + "prevHash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "coinbase1": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4f0350960d122f62737175617265642fe5006405fe934bb9fabe6d6d2db144ad7b2eed39c89a2cde9f88f06d9082613911584463126673a700303e881000000000000000", + "coinbase2": "ffffffff05220200000000000017a9141bd79ae4d7811daea94bd25db93761b10fecccd7876271cc120000000017a9146547a37cae8db12fd4c8f191f69d4a52cfa886fa870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501307f36ff0aff7000ebd4eea1e8e9bbbfa0e1134cb203d7822f404faca98985eb083e8fd9e16ad64700000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "merkleBranches": [ + "15992cde4826516b7dda391bcec363f8da4f6f335199ec76550b535d667c6bcc", + "4703e3571e20a0fa02766ca895c2043bd1c16364b4163a5d7fe3b669fa47b371", + "1d3822d7192f4c8978ca54d5e3b460ab420907199ba2d751ca682b76b9d8e030", + "55b47a1a7ab09d05ea95e4c36b2ece4f441f1787b6548656a67c93850bad28ca", + "1a231983cfb2837a358b85d7bba94851eeedb3f1af915a67165c49ca36c4ea42", + "1ef6e6152101ed2948ef74ebd6d62959f6b21e5c8c404def4faf88976a999c12", + "d28d9eb88badf7d180885592f79da466d0db6dbfd50b440d1c85a2c9ff1b8f01", + "eddef6dd40e0ed35c83129b484f4848021d781cc05da764df8bac3bc82745d12", + "b62ac40bb764914198729c39526751adc6bb047d3f0990ca742c863028a201da", + "7ec1b0edb5aef0b4a71ebf222d22220eb2c38b9fb07603485c9503b65ff43440", + "b4c149bc9f2f48b6887800706e50c669c86af320759914bcd2a53f7f89b92eda", + "c1cde7654ebfad4609b999082eba464c5a20eb1434e46ccd6d81510a13aa853b" + ], + "version": "20000000", + "bits": "1702796c", + "time": "67ec55e6", + "cleanJobs": false, + "received": 1743541734876, + "pool": 161, + "coinbase": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4f0350960d122f62737175617265642fe5006405fe934bb9fabe6d6d2db144ad7b2eed39c89a2cde9f88f06d9082613911584463126673a700303e881000000000000000000002dc0000000000000000ffffffff05220200000000000017a9141bd79ae4d7811daea94bd25db93761b10fecccd7876271cc120000000017a9146547a37cae8db12fd4c8f191f69d4a52cfa886fa870000000000000000266a24aa21a9ed93bbaa9811f4ac5527dcd50345b7b15a3d319778bf43ea027337d6fd660826fe00000000000000002f6a2d434f524501307f36ff0aff7000ebd4eea1e8e9bbbfa0e1134cb203d7822f404faca98985eb083e8fd9e16ad64700000000000000002b6a2952534b424c4f434b3a7ad7ddd490a0e384e30c34942e26100ab1e97b25aa8a53c435370b100070fc9300000000", + "height": 890448, + "timestamp": 1743541734, + "reward": 315388804, + "scriptsig": "0350960d122f62737175617265642fe5006405fe934bb9fabe6d6d2db144ad7b2eed39c89a2cde9f88f06d9082613911584463126673a700303e881000000000000000000002dc0000000000000000" + } + }, + "mempoolInfo": { + "loaded": true, + "size": 4173, + "bytes": 3427792, + "usage": 16974288, + "total_fee": 0.05445324, + "maxmempool": 300000000, + "mempoolminfee": 0.00001, + "minrelaytxfee": 0.00001, + "incrementalrelayfee": 0.00001, + "unbroadcastcount": 0, + "fullrbf": true + }, + "vBytesPerSecond": 1415, + "mempool-blocks": [ + { + "blockSize": 1779311, + "blockVSize": 997968.5, + "nTx": 2132, + "totalFees": 2902870, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0721153846153846, + 1.9980563654033041, + 2.2195704057279237, + 3.009493670886076, + 3.4955223880597015, + 6.0246913580246915, + 218.1818181818182 + ] + }, + { + "blockSize": 1959636, + "blockVSize": 997903.5, + "nTx": 497, + "totalFees": 1093076, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0401794819498267, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0761096766260911, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477260, + "blockVSize": 997997.25, + "nTx": 720, + "totalFees": 1016195, + "medianFee": 1.007409072434199, + "feeRange": [ + 1, + 1.0019120458891013, + 1.0040863981319323, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0485018498190837 + ] + }, + { + "blockSize": 1021308, + "blockVSize": 431071.5, + "nTx": 823, + "totalFees": 432342, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0028011204481793, + 1.0042075736325387, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "transactions": [ + { + "txid": "bccfbff8d129df7dcfb65b7587186a807ab2c7e4b8494188ac27733705d9e482", + "fee": 564, + "vsize": 140.25, + "value": 160404, + "rate": 4.021390374331551, + "time": 1743541739 + }, + { + "txid": "94c81b3fc40369520fd7607ed54f18416057aab46bc6da328c0588e5967a2ebd", + "fee": 1044, + "vsize": 315.75, + "value": 64814, + "rate": 3.3064133016627077, + "time": 1743541739 + }, + { + "txid": "a16276de4ce8b2eba0013428af73255060ad04e685bee98731504bf973d49bf7", + "fee": 426, + "vsize": 141.25, + "value": 349522, + "rate": 3.015929203539823, + "time": 1743541739 + }, + { + "txid": "bb25deab784cb213dc4cba234f5a41a6c8950c432a05b5858053b39ceda6d1a3", + "fee": 356, + "vsize": 152.25, + "value": 1562308, + "rate": 2.3382594417077174, + "time": 1743541738 + }, + { + "txid": "17a9e5194345d8da287d49c0bf595474e08fc81434646925d6640964929aa7f9", + "fee": 11400, + "vsize": 189.5, + "value": 3352669, + "rate": 60.15831134564644, + "time": 1743541738 + }, + { + "txid": "ced722a2f9007182b8268240af19fee486e8af07da83efccea6e2374aa484d07", + "fee": 534, + "vsize": 177, + "value": 302121849, + "rate": 3.016949152542373, + "time": 1743541736 + } + ], + "loadingIndicators": {}, + "fees": { + "fastestFee": 3, + "halfHourFee": 3, + "hourFee": 3, + "economyFee": 2, + "minimumFee": 1 + }, + "rbfSummary": [ + { + "txid": "51f468b28c4ee790bdfa3152a3add656f29e908828b55fb295cf55dc6a33a855", + "mined": false, + "fullRbf": false, + "oldFee": 19437, + "oldVsize": 166.5, + "newFee": 19770, + "newVsize": 166.5 + }, + { + "txid": "8fb448ef9a26ca3cee8341e3627adf57ae9374c37b83082ad5d90491955cc38c", + "mined": false, + "fullRbf": false, + "oldFee": 19270, + "oldVsize": 166.25, + "newFee": 19603, + "newVsize": 166.5 + }, + { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "mined": false, + "fullRbf": false, + "oldFee": 606, + "oldVsize": 233, + "newFee": 960, + "newVsize": 224 + }, + { + "txid": "a2b7c8a1e8d0f17108c5089198ccbf6c50341c99abb91959a25a41f9bb44fa60", + "mined": false, + "fullRbf": false, + "oldFee": 504, + "oldVsize": 225, + "newFee": 958, + "newVsize": 226 + }, + { + "txid": "798fb7eb84e69c58a596478436f48d3a620a825624ff88d5f15a0b1e17fa8fdf", + "mined": false, + "fullRbf": false, + "oldFee": 1680, + "oldVsize": 381.25, + "newFee": 2240, + "newVsize": 381.5 + }, + { + "txid": "86dd928e524e12fdfbb6d2ba51e1a0e4d8557d05552184bb20c0427c3e0d19f8", + "mined": false, + "fullRbf": false, + "oldFee": 1476, + "oldVsize": 328.5, + "newFee": 1968, + "newVsize": 328.5 + } + ], + "backend": "esplora", + "blocks": [ + { + "id": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4", + "height": 890440, + "version": 559235072, + "timestamp": 1743535677, + "bits": 386038124, + "nonce": 2920325684, + "difficulty": 113757508810854, + "merkle_root": "c793d5fdbfb1ebe99e14a13a6d65370057d311774d33c71da166663b18722474", + "tx_count": 3823, + "size": 1578209, + "weight": 3993461, + "previousblockhash": "000000000000000000020fb2e24425793e17e60e188205dc1694d221790348b2", + "mediantime": 1743532406, + "stale": false, + "extras": { + "reward": 319838750, + "coinbaseRaw": "0348960d082f5669614254432f2cfabe6d6d294719da11c017243828bf32c405341db7f19387fee92c25413c45e114907f9810000000000000001058bf9601429f9fa7a6c160d10d00000000000000", + "orphans": [], + "medianFee": 4, + "feeRange": [ + 3, + 3, + 3.0191082802547773, + 3.980952380952381, + 5, + 10, + 427.748502994012 + ], + "totalFees": 7338750, + "avgFee": 1920, + "avgFeeRate": 7, + "utxoSetChange": 4093, + "avgTxSize": 412.71000000000004, + "totalInputs": 7430, + "totalOutputs": 11523, + "totalOutputAmt": 547553568373, + "segwitTotalTxs": 3432, + "segwitTotalSize": 1467920, + "segwitTotalWeight": 3552413, + "feePercentiles": null, + "virtualSize": 998365.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003H–\r\b/ViaBTC/,ú¾mm)G\u0019Ú\u0011À\u0017$8(¿2Ä\u00054\u001d·ñ“‡þé,%Aìg/Foundry USA Pool #dropgold/\u0001S\nEaç\u0002\u0000\u0000\u0000\u0000\u0000", + "header": "00a03220b46014bdbeb557d1d87065e0ecb1b0ab33654bb7f579000000000000000000003ed60f06cec16df4399b5dafa7077036c2eb58cc6a16e6cdca559b9e2f7e4525bb3eec676c790217b7b3c9cb", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2792968, + "expectedWeight": 3991959, + "similarity": 0.9951416839808291 + } + }, + { + "id": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "height": 890442, + "version": 557981696, + "timestamp": 1743536834, + "bits": 386038124, + "nonce": 470697326, + "difficulty": 113757508810854, + "merkle_root": "5e92e681c1db2797a5b3e5016729059f8b60a256cafb51d835dac2b3964c0db4", + "tx_count": 3566, + "size": 1628328, + "weight": 3993552, + "previousblockhash": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c", + "mediantime": 1743532867, + "stale": false, + "extras": { + "reward": 318057766, + "coinbaseRaw": "034a960d194d696e656420627920416e74506f6f6c204d000201e15e2989fabe6d6dd599e9dfa40be51f1517c8f512c5c3d51c7656182f1df335d34b98ee02c527db080000000000000000004f92b702000000000000", + "orphans": [], + "medianFee": 3.00860164711668, + "feeRange": [ + 1.5174418604651163, + 2.0140845070422535, + 2.492354740061162, + 3, + 4.020942408376963, + 7, + 200 + ], + "totalFees": 5557766, + "avgFee": 1558, + "avgFeeRate": 5, + "utxoSetChange": 1971, + "avgTxSize": 456.48, + "totalInputs": 7938, + "totalOutputs": 9909, + "totalOutputAmt": 900044492230, + "segwitTotalTxs": 3214, + "segwitTotalSize": 1526463, + "segwitTotalWeight": 3586200, + "feePercentiles": null, + "virtualSize": 998388, + "coinbaseAddress": "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "coinbaseAddresses": [ + "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z", + "39C7fxSzEACPjM78Z7xdPxhf7mKxJwvfMJ" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 42402a28dd61f2718a4b27ae72a4791d5bbdade7 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003J–\r\u0019Mined by AntPool M\u0000\u0002\u0001á^)‰ú¾mmՙéߤ\u000bå\u001f\u0015\u0017Èõ\u0012ÅÃÕ\u001cvV\u0018/\u001dó5ÓK˜î\u0002Å'Û\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000O’·\u0002\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "002042219cd10cfc6daf06d3faad645fe0914cea859745275aef00000000000000000000b40d4c96b3c2da35d851fbca56a2608b9f05296701e5b3a59727dbc181e6925ec242ec676c7902176e450e1c", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 44, + "name": "AntPool", + "slug": "antpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5764747, + "expectedWeight": 3991786, + "similarity": 0.9029319155137951 + } + }, + { + "id": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "height": 890443, + "version": 706666496, + "timestamp": 1743537197, + "bits": 386038124, + "nonce": 321696065, + "difficulty": 113757508810854, + "merkle_root": "3d7574f7eca741fa94b4690868a242e5b286f8a0417ad0275d4ab05893e96350", + "tx_count": 2155, + "size": 1700002, + "weight": 3993715, + "previousblockhash": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce", + "mediantime": 1743533789, + "stale": false, + "extras": { + "reward": 315112344, + "coinbaseRaw": "034b960d21202020204d696e656420627920536563706f6f6c2020202070000b05e388958c01fabe6d6db7ae4bfa7b1294e16e800b4563f1f5ddeb5c0740319eba45600f3f05d2d7272910000000000000000000c2cb7e020000", + "orphans": [], + "medianFee": 1.4360674424569184, + "feeRange": [ + 1, + 1.0135135135135136, + 1.09717868338558, + 2.142857142857143, + 3.009584664536741, + 4.831858407079646, + 196.07843137254903 + ], + "totalFees": 2612344, + "avgFee": 1212, + "avgFeeRate": 2, + "utxoSetChange": -2880, + "avgTxSize": 788.64, + "totalInputs": 9773, + "totalOutputs": 6893, + "totalOutputAmt": 264603969671, + "segwitTotalTxs": 1933, + "segwitTotalSize": 1556223, + "segwitTotalWeight": 3418707, + "feePercentiles": null, + "virtualSize": 998428.75, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003K–\r! Mined by Secpool p\u0000\u000b\u0005㈕Œ\u0001ú¾mm·®Kú{\u0012”án€\u000bEcñõÝë\\\u0007@1žºE`\u000f?\u0005Ò×')\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÂË~\u0002\u0000\u0000", + "header": "00e01e2acedf6db0523987887ed8b989d4f58b3d6b878a974548010000000000000000005063e99358b04a5d27d07a41a0f886b2e542a2680869b494fa41a7ecf774753d2d44ec676c79021741b12c13", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2623934, + "expectedWeight": 3991917, + "similarity": 0.9951244468050102 + } + }, + { + "id": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "height": 890444, + "version": 671080448, + "timestamp": 1743539347, + "bits": 386038124, + "nonce": 994357124, + "difficulty": 113757508810854, + "merkle_root": "c891d4bf68e22916274b667eb3287d50da2ddd63f8dad892da045cc2ad4a7b21", + "tx_count": 3797, + "size": 1500309, + "weight": 3993525, + "previousblockhash": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6", + "mediantime": 1743533986, + "stale": false, + "extras": { + "reward": 318708524, + "coinbaseRaw": "034c960d082f5669614254432f2cfabe6d6d45b7fd7ab53a0914da7dcc9d21fe44f0936f5354169a56df9d5139f07afbc2b41000000000000000106fc0eb03f0ac2e851d18d8d9f85ad70000000000", + "orphans": [], + "medianFee": 4.064775540157046, + "feeRange": [ + 3.014354066985646, + 3.18368700265252, + 3.602836879432624, + 4.231825525040388, + 5.581730769230769, + 10, + 697.7151162790698 + ], + "totalFees": 6208524, + "avgFee": 1635, + "avgFeeRate": 6, + "utxoSetChange": 5755, + "avgTxSize": 395.02, + "totalInputs": 6681, + "totalOutputs": 12436, + "totalOutputAmt": 835839828101, + "segwitTotalTxs": 3351, + "segwitTotalSize": 1354446, + "segwitTotalWeight": 3410181, + "feePercentiles": null, + "virtualSize": 998381.25, + "coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4", + "coinbaseAddresses": [ + "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4" + ], + "coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG", + "coinbaseSignatureAscii": "\u0003L–\r\b/ViaBTC/,ú¾mmE·ýzµ:\t\u0014Ú}̝!þDð“oST\u0016šVߝQ9ðzû´\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010oÀë\u0003ð¬.…\u001d\u0018ØÙøZ×\u0000\u0000\u0000\u0000\u0000", + "header": "00e0ff27f6f596dc1a210647d530ed3b351b5173428370b2086e02000000000000000000217b4aadc25c04da92d8daf863dd2dda507d28b37e664b271629e268bfd491c8934cec676c79021784af443b", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 73, + "name": "ViaBTC", + "slug": "viabtc", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 6253024, + "expectedWeight": 3991868, + "similarity": 0.9862862477811569 + } + }, + { + "id": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "height": 890445, + "version": 601202688, + "timestamp": 1743539574, + "bits": 386038124, + "nonce": 1647397133, + "difficulty": 113757508810854, + "merkle_root": "61d8294afa8f6bafa4d979a77d187dee5f75a6392f957ea647d96eefbbbc5e9b", + "tx_count": 3579, + "size": 1659862, + "weight": 3993406, + "previousblockhash": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d", + "mediantime": 1743535677, + "stale": false, + "extras": { + "reward": 315617086, + "coinbaseRaw": "034d960d04764dec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f4fac7c451540000000000000", + "orphans": [], + "medianFee": 2.5565329189526835, + "feeRange": [ + 1.521613832853026, + 2, + 2.2411347517730498, + 3, + 3, + 3.954954954954955, + 162.78343949044586 + ], + "totalFees": 3117086, + "avgFee": 871, + "avgFeeRate": 3, + "utxoSetChange": 1881, + "avgTxSize": 463.65000000000003, + "totalInputs": 7893, + "totalOutputs": 9774, + "totalOutputAmt": 324878597485, + "segwitTotalTxs": 3189, + "segwitTotalSize": 1538741, + "segwitTotalWeight": 3509030, + "feePercentiles": null, + "virtualSize": 998351.5, + "coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "coinbaseAddresses": [ + "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63", + "bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38" + ], + "coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3", + "coinbaseSignatureAscii": "\u0003M–\r\u0004vMìg/Foundry USA Pool #dropgold/O¬|E\u0015@\u0000\u0000\u0000\u0000\u0000\u0000", + "header": "00a0d5230d2965fa5bd3e9406f5d665f975d9fc34eae70c46eb3010000000000000000009b5ebcbbef6ed947a67e952f39a6755fee7d187da779d9a4af6b8ffa4a29d861764dec676c7902170d493162", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 111, + "name": "Foundry USA", + "slug": "foundryusa", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3145370, + "expectedWeight": 3991903, + "similarity": 0.9903353189076812 + } + }, + { + "id": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "height": 890446, + "version": 537722880, + "timestamp": 1743541107, + "bits": 386038124, + "nonce": 826569764, + "difficulty": 113757508810854, + "merkle_root": "d9b320d7cb5aace80ca20b934b13b4a272121fbdd59f3aaba690e0326ca2c144", + "tx_count": 3998, + "size": 1541360, + "weight": 3993545, + "previousblockhash": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a", + "mediantime": 1743535803, + "stale": false, + "extras": { + "reward": 317976882, + "coinbaseRaw": "034e960d20202020204d696e656420627920536563706f6f6c2020202070001b04fad5fdfefabe6d6d59dd8ebce6e5aab8fb943bbdcede474b6f2d00a395a717970104a6958c17f1ca100000000000000000008089c9350200", + "orphans": [], + "medianFee": 3.3750830641948864, + "feeRange": [ + 2.397163120567376, + 3, + 3, + 3.463647199046484, + 4.49438202247191, + 7.213930348258707, + 476.1904761904762 + ], + "totalFees": 5476882, + "avgFee": 1370, + "avgFeeRate": 5, + "utxoSetChange": 4951, + "avgTxSize": 385.41, + "totalInputs": 7054, + "totalOutputs": 12005, + "totalOutputAmt": 983289729453, + "segwitTotalTxs": 3538, + "segwitTotalSize": 1396505, + "segwitTotalWeight": 3414233, + "feePercentiles": null, + "virtualSize": 998386.25, + "coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "coinbaseAddresses": [ + "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9", + "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL", + "coinbaseSignatureAscii": "\u0003N–\r Mined by Secpool p\u0000\u001b\u0004úÕýþú¾mmYݎ¼æåª¸û”;½ÎÞGKo-\u0000£•§\u0017—\u0001\u0004¦•Œ\u0017ñÊ\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000€‰É5\u0002\u0000", + "header": "00000d208a33fa2f65c6d3662ac962c9bd595147b940f96520400100000000000000000044c1a26c32e090a6ab3a9fd5bd1f1272a2b4134b930ba20ce8ac5acbd720b3d97353ec676c79021724744431", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 141, + "name": "SECPOOL", + "slug": "secpool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 5601814, + "expectedWeight": 3991928, + "similarity": 0.9537877497871488 + } + }, + { + "id": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "height": 890447, + "version": 568860672, + "timestamp": 1743541240, + "bits": 386038124, + "nonce": 4008077709, + "difficulty": 113757508810854, + "merkle_root": "8c3b098e4e50b67075a4fc52bf4cd603aaa450c240c18a865c9ddc0f27104f5f", + "tx_count": 1919, + "size": 1747789, + "weight": 3993172, + "previousblockhash": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b", + "mediantime": 1743536834, + "stale": false, + "extras": { + "reward": 314435106, + "coinbaseRaw": "034f960d0f2f736c7573682f65000002fba05ef1fabe6d6df8d29032ea6f9ab1debd223651f30887df779c6195869e70a7b787b3a15f4b1710000000000000000000ee5f0c00b20200000000", + "orphans": [], + "medianFee": 1.4653828213500366, + "feeRange": [ + 1.0845070422535212, + 1.2, + 1.51, + 2.0141129032258065, + 2.3893805309734515, + 4.025477707006369, + 300.0065359477124 + ], + "totalFees": 1935106, + "avgFee": 1008, + "avgFeeRate": 1, + "utxoSetChange": -4244, + "avgTxSize": 910.58, + "totalInputs": 9909, + "totalOutputs": 5665, + "totalOutputAmt": 210763861504, + "segwitTotalTxs": 1720, + "segwitTotalSize": 1629450, + "segwitTotalWeight": 3519924, + "feePercentiles": null, + "virtualSize": 998293, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003O–\r\u000f/slush/e\u0000\u0000\u0002û ^ñú¾mmøÒ2êoš±Þ½\"6Qó\b‡ßwœa•†žp§·‡³¡_K\u0017\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000î_\f\u0000²\u0002\u0000\u0000\u0000\u0000", + "header": "0020e8216b30f547b3d47824c1cb06db9221fcd04621b0263dfe010000000000000000005f4f10270fdc9d5c868ac140c250a4aa03d64cbf52fca47570b6504e8e093b8cf853ec676c7902178d69e6ee", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 2059571, + "expectedWeight": 3991720, + "similarity": 0.9149852183486826 + } + } + ], + "conversions": { + "time": 1743541504, + "USD": 85181, + "EUR": 78939, + "GBP": 65914, + "CAD": 121813, + "CHF": 75399, + "AUD": 135492, + "JPY": 12753000 + }, + "backendInfo": { + "hostname": "node205.tk7.mempool.space", + "version": "3.1.0-dev", + "gitCommit": "3c08b5c72", + "lightning": false, + "backend": "esplora" + }, + "da": { + "progressPercent": 68.99801587301587, + "difficultyChange": 2.7058667283164084, + "estimatedRetargetDate": 1743907120500, + "remainingBlocks": 625, + "remainingTime": 365382500, + "previousRetarget": 1.433804484570416, + "previousTime": 1742728542, + "nextRetargetHeight": 891072, + "timeAvg": 584612, + "adjustedTimeAvg": 584612, + "timeOffset": 0, + "expectedBlocks": 1355.3266666666666 + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json b/frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json new file mode 100644 index 0000000000..7af7235463 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx01_ws_tx_replaced.json @@ -0,0 +1,5 @@ +{ + "txReplaced": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698" + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json b/frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json new file mode 100644 index 0000000000..478eecfbc8 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json @@ -0,0 +1,9 @@ +{ + "ancestors": [], + "bestDescendant": null, + "descendants": [], + "effectiveFeePerVsize": 4.285714285714286, + "sigops": 4, + "fee": 960, + "adjustedVsize": 224 +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json b/frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json new file mode 100644 index 0000000000..96bd1efdd1 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json @@ -0,0 +1,36 @@ +{ + "replacements": { + "tx": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "fee": 960, + "vsize": 224, + "value": 49040, + "rate": 4.285714285714286, + "time": 1743541726, + "rbf": true, + "fullRbf": false + }, + "time": 1743541726, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29", + "fee": 606, + "vsize": 233, + "value": 49394, + "rate": 2.6008583690987126, + "time": 1743541407, + "rbf": true + }, + "time": 1743541407, + "interval": 319, + "fullRbf": false, + "replaces": [] + } + ] + }, + "replaces": [ + "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29" + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_tx.json b/frontend/cypress/fixtures/details_rbf/tx02_api_tx.json new file mode 100644 index 0000000000..cbcb4fe8c8 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_tx.json @@ -0,0 +1,38 @@ +{ + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "version": 2, + "locktime": 0, + "vin": [ + { + "txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9", + "vout": 0, + "prevout": { + "scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj", + "value": 50000 + }, + "scriptsig": "483045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f438014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "scriptsig_asm": "OP_PUSHBYTES_72 3045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f43801 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df", + "is_coinbase": false, + "sequence": 4294967293 + } + ], + "vout": [ + { + "scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj", + "value": 49040 + } + ], + "size": 224, + "weight": 896, + "sigops": 4, + "fee": 960, + "status": { + "confirmed": false + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json b/frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json new file mode 100644 index 0000000000..365c3885eb --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_api_tx_times.json @@ -0,0 +1,3 @@ +[ + 1743541726 +] \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_block.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_block.json new file mode 100644 index 0000000000..9fc358795d --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_block.json @@ -0,0 +1,116 @@ +{ + "block": { + "id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61", + "height": 890448, + "version": 626941952, + "timestamp": 1743541850, + "bits": 386038124, + "nonce": 1177284424, + "difficulty": 113757508810854, + "merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3", + "tx_count": 2229, + "size": 1763153, + "weight": 3993275, + "previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "mediantime": 1743537197, + "stale": false, + "extras": { + "reward": 315498786, + "coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000", + "orphans": [], + "medianFee": 2.144206217858874, + "feeRange": [ + 1.0845921450151057, + 2, + 2.2448979591836733, + 3, + 3.5985915492957745, + 6, + 217.0212765957447 + ], + "totalFees": 2998786, + "avgFee": 1345, + "avgFeeRate": 3, + "utxoSetChange": -3073, + "avgTxSize": 790.84, + "totalInputs": 9558, + "totalOutputs": 6485, + "totalOutputAmt": 442206797883, + "segwitTotalTxs": 1986, + "segwitTotalSize": 1676431, + "segwitTotalWeight": 3646495, + "feePercentiles": null, + "virtualSize": 998318.75, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000", + "header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3287140, + "expectedWeight": 3991809, + "similarity": 0.9079894021278392 + } + }, + "mempool-blocks": [ + { + "blockSize": 1979907, + "blockVSize": 997974.75, + "nTx": 461, + "totalFees": 1429178, + "medianFee": 1.1020797417745662, + "feeRange": [ + 1.0666666666666667, + 1.0746847720659554, + 1.102059530141031, + 3, + 3.4233409610983982, + 5.017605633802817, + 148.4084084084084 + ] + }, + { + "blockSize": 1363691, + "blockVSize": 997986.5, + "nTx": 1080, + "totalFees": 1023741, + "medianFee": 1.014827018121911, + "feeRange": [ + 1, + 1.0036011703803736, + 1.0054683365672958, + 1.0186757215619695, + 1.0548148148148149, + 1.0548148148148149, + 1.068146618482189 + ] + }, + { + "blockSize": 1337253, + "blockVSize": 563516.25, + "nTx": 901, + "totalFees": 564834, + "medianFee": 1.0028011204481793, + "feeRange": [ + 1, + 1, + 1, + 1.0025062656641603, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698" +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json new file mode 100644 index 0000000000..9fc358795d --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_block_confirmation.json @@ -0,0 +1,116 @@ +{ + "block": { + "id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61", + "height": 890448, + "version": 626941952, + "timestamp": 1743541850, + "bits": 386038124, + "nonce": 1177284424, + "difficulty": 113757508810854, + "merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3", + "tx_count": 2229, + "size": 1763153, + "weight": 3993275, + "previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab", + "mediantime": 1743537197, + "stale": false, + "extras": { + "reward": 315498786, + "coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000", + "orphans": [], + "medianFee": 2.144206217858874, + "feeRange": [ + 1.0845921450151057, + 2, + 2.2448979591836733, + 3, + 3.5985915492957745, + 6, + 217.0212765957447 + ], + "totalFees": 2998786, + "avgFee": 1345, + "avgFeeRate": 3, + "utxoSetChange": -3073, + "avgTxSize": 790.84, + "totalInputs": 9558, + "totalOutputs": 6485, + "totalOutputAmt": 442206797883, + "segwitTotalTxs": 1986, + "segwitTotalSize": 1676431, + "segwitTotalWeight": 3646495, + "feePercentiles": null, + "virtualSize": 998318.75, + "coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11", + "coinbaseAddresses": [ + "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11" + ], + "coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL", + "coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000", + "header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46", + "utxoSetSize": null, + "totalInputAmt": null, + "pool": { + "id": 43, + "name": "Braiins Pool", + "slug": "braiinspool", + "minerNames": null + }, + "matchRate": 100, + "expectedFees": 3287140, + "expectedWeight": 3991809, + "similarity": 0.9079894021278392 + } + }, + "mempool-blocks": [ + { + "blockSize": 1979907, + "blockVSize": 997974.75, + "nTx": 461, + "totalFees": 1429178, + "medianFee": 1.1020797417745662, + "feeRange": [ + 1.0666666666666667, + 1.0746847720659554, + 1.102059530141031, + 3, + 3.4233409610983982, + 5.017605633802817, + 148.4084084084084 + ] + }, + { + "blockSize": 1363691, + "blockVSize": 997986.5, + "nTx": 1080, + "totalFees": 1023741, + "medianFee": 1.014827018121911, + "feeRange": [ + 1, + 1.0036011703803736, + 1.0054683365672958, + 1.0186757215619695, + 1.0548148148148149, + 1.0548148148148149, + 1.068146618482189 + ] + }, + { + "blockSize": 1337253, + "blockVSize": 563516.25, + "nTx": 901, + "totalFees": 564834, + "medianFee": 1.0028011204481793, + "feeRange": [ + 1, + 1, + 1, + 1.0025062656641603, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698" +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json new file mode 100644 index 0000000000..4c83cf7974 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_mempool_blocks_01.json @@ -0,0 +1,75 @@ +{ + "mempool-blocks": [ + { + "blockSize": 1779823, + "blockVSize": 997995.25, + "nTx": 2133, + "totalFees": 2922926, + "medianFee": 2.0479263387949875, + "feeRange": [ + 1.0825892857142858, + 2, + 2.2439024390243905, + 3.010452961672474, + 3.554973821989529, + 6.032085561497326, + 218.1818181818182 + ] + }, + { + "blockSize": 1957833, + "blockVSize": 997953, + "nTx": 500, + "totalFees": 1093270, + "medianFee": 1.102049424602265, + "feeRange": [ + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.0548148148148149, + 1.067677314564158, + 1.0766488413547237, + 1.1021605957228275 + ] + }, + { + "blockSize": 1477864, + "blockVSize": 997999, + "nTx": 730, + "totalFees": 1016458, + "medianFee": 1.0075971559364956, + "feeRange": [ + 1, + 1.0019552465783186, + 1.004255319148936, + 1.0081019768823594, + 1.018450184501845, + 1.0203327171903882, + 1.0548148148148149 + ] + }, + { + "blockSize": 1030954, + "blockVSize": 436613.5, + "nTx": 838, + "totalFees": 437891, + "medianFee": 0, + "feeRange": [ + 1, + 1, + 1, + 1.0026525198938991, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txPosition": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "position": { + "block": 0, + "vsize": 111102 + } + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json new file mode 100644 index 0000000000..4245d46114 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_next_block.json @@ -0,0 +1,75 @@ +{ + "mempool-blocks": [ + { + "blockSize": 1719945, + "blockVSize": 997952.25, + "nTx": 2558, + "totalFees": 3287140, + "medianFee": 2.4046448299072485, + "feeRange": [ + 1.073446327683616, + 2, + 2.2567567567567566, + 3.0106761565836297, + 3.6169014084507043, + 6.015037593984962, + 218.1818181818182 + ] + }, + { + "blockSize": 2022898, + "blockVSize": 997983.25, + "nTx": 131, + "totalFees": 1098129, + "medianFee": 1.1020797417745662, + "feeRange": [ + 1.0625, + 1.0691217722793642, + 1.073436083408885, + 1.0761096766260911, + 1.080091533180778, + 1.102110739151618, + 1.1021909190121146 + ] + }, + { + "blockSize": 1363844, + "blockVSize": 997998.5, + "nTx": 1073, + "totalFees": 1023651, + "medianFee": 1.014827018121911, + "feeRange": [ + 1, + 1.003584229390681, + 1.0054683365672958, + 1.0186757215619695, + 1.0548148148148149, + 1.0548148148148149, + 1.068146618482189 + ] + }, + { + "blockSize": 1335390, + "blockVSize": 562453.5, + "nTx": 902, + "totalFees": 563772, + "medianFee": 1.0028011204481793, + "feeRange": [ + 1, + 1, + 1, + 1.0025402201524132, + 1.004231311706629, + 1.0053475935828877, + 1.0068649885583525 + ] + } + ], + "txPosition": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "position": { + "block": 0, + "vsize": 128920 + } + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json b/frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json new file mode 100644 index 0000000000..c4a2db35a2 --- /dev/null +++ b/frontend/cypress/fixtures/details_rbf/tx02_ws_tx_position.json @@ -0,0 +1,9 @@ +{ + "txPosition": { + "txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698", + "position": { + "block": 0, + "vsize": 110880 + } + } +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/mainnet_mempoolInfo.json b/frontend/cypress/fixtures/mainnet_mempoolInfo.json index d9e441277f..584364e9a9 100644 --- a/frontend/cypress/fixtures/mainnet_mempoolInfo.json +++ b/frontend/cypress/fixtures/mainnet_mempoolInfo.json @@ -750,7 +750,7 @@ }, "backendInfo": { "hostname": "node205.tk7.mempool.space", - "version": "3.0.0-rc1", + "version": "3.1.0-dev", "gitCommit": "abbc8a134", "lightning": false }, diff --git a/frontend/cypress/fixtures/rbf_page/rbf_01.json b/frontend/cypress/fixtures/rbf_page/rbf_01.json new file mode 100644 index 0000000000..2c79f142a1 --- /dev/null +++ b/frontend/cypress/fixtures/rbf_page/rbf_01.json @@ -0,0 +1,37 @@ +{ + "rbfLatest": [ + { + "tx": { + "txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68", + "fee": 1185, + "vsize": 223, + "value": 41729, + "rate": 5.313901345291479, + "time": 1743587177, + "rbf": true, + "fullRbf": false, + "mined": true + }, + "time": 1743587177, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f", + "fee": 504, + "vsize": 222, + "value": 42410, + "rate": 2.27027027027027, + "time": 1743586081, + "rbf": true + }, + "time": 1743586081, + "interval": 1096, + "fullRbf": false, + "replaces": [] + } + ], + "mined": true + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/fixtures/rbf_page/rbf_02.json b/frontend/cypress/fixtures/rbf_page/rbf_02.json new file mode 100644 index 0000000000..3b50110029 --- /dev/null +++ b/frontend/cypress/fixtures/rbf_page/rbf_02.json @@ -0,0 +1,68 @@ +{ + "rbfLatest": [ + { + "tx": { + "txid": "d313b479acfbae719afb488a078e0fe0e052a67b9f65f73f7c75d3d95fd36acc", + "fee": 672, + "vsize": 167.25, + "value": 29996328, + "rate": 4.017937219730942, + "time": 1743587365, + "rbf": true, + "fullRbf": false + }, + "time": 1743587365, + "fullRbf": false, + "replaces": [ + { + "tx": { + "txid": "eb5aa786cabda307cc9642cfb9c41a3b405ac20a391eefbe54be7930bea61865", + "fee": 336, + "vsize": 167.5, + "value": 29996664, + "rate": 2.005970149253731, + "time": 1743586424, + "rbf": true + }, + "time": 1743586424, + "interval": 941, + "fullRbf": false, + "replaces": [] + } + ] + }, + { + "tx": { + "txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68", + "fee": 1185, + "vsize": 223, + "value": 41729, + "rate": 5.313901345291479, + "time": 1743587177, + "rbf": true, + "fullRbf": false, + "mined": true + }, + "time": 1743587177, + "fullRbf": true, + "replaces": [ + { + "tx": { + "txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f", + "fee": 504, + "vsize": 222, + "value": 42410, + "rate": 2.27027027027027, + "time": 1743586081, + "rbf": true + }, + "time": 1743586081, + "interval": 1096, + "fullRbf": false, + "replaces": [] + } + ], + "mined": true + } + ] +} \ No newline at end of file diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index 018f63569d..2ce198241b 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -44,6 +44,7 @@ import { PageIdleDetector } from './PageIdleDetector'; import { mockWebSocket } from './websocket'; +import { mockWebSocketV2 } from './websocket'; /* global Cypress */ const codes = { @@ -72,6 +73,10 @@ Cypress.Commands.add('mockMempoolSocket', () => { mockWebSocket(); }); +Cypress.Commands.add('mockMempoolSocketV2', () => { + mockWebSocketV2(); +}); + Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => { cy.get('.dropdown-toggle').click().then(() => { cy.get(`a.${network}`).click().then(() => { diff --git a/frontend/cypress/support/index.d.ts b/frontend/cypress/support/index.d.ts index 2c53283017..21ffe6a2d9 100644 --- a/frontend/cypress/support/index.d.ts +++ b/frontend/cypress/support/index.d.ts @@ -5,6 +5,7 @@ declare namespace Cypress { waitForSkeletonGone(): Chainable waitForPageIdle(): Chainable mockMempoolSocket(): Chainable + mockMempoolSocketV2(): Chainable changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable } } \ No newline at end of file diff --git a/frontend/cypress/support/websocket.ts b/frontend/cypress/support/websocket.ts index 1356ccc769..b067cc6e87 100644 --- a/frontend/cypress/support/websocket.ts +++ b/frontend/cypress/support/websocket.ts @@ -27,6 +27,37 @@ const createMock = (url: string) => { return mocks[url]; }; +export const mockWebSocketV2 = () => { + cy.on('window:before:load', (win) => { + const winWebSocket = win.WebSocket; + cy.stub(win, 'WebSocket').callsFake((url) => { + console.log(url); + if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) { + const { server, websocket } = createMock(url); + + win.mockServer = server; + win.mockServer.on('connection', (socket) => { + win.mockSocket = socket; + }); + + win.mockServer.on('message', (message) => { + console.log(message); + }); + + return websocket; + } else { + return new winWebSocket(url); + } + }); + }); + + cy.on('window:before:unload', () => { + for (const url in mocks) { + cleanupMock(url); + } + }); +}; + export const mockWebSocket = () => { cy.on('window:before:load', (win) => { const winWebSocket = win.WebSocket; @@ -65,6 +96,27 @@ export const mockWebSocket = () => { }); }; +export const receiveWebSocketMessageFromServer = ({ + params +}: { params?: any } = {}) => { + cy.window().then((win) => { + if (params.message) { + console.log('sending message'); + win.mockSocket.send(params.message.contents); + } + + if (params.file) { + cy.readFile(`cypress/fixtures/${params.file.path}`, 'utf-8').then((fixture) => { + console.log('sending payload'); + win.mockSocket.send(JSON.stringify(fixture)); + }); + + } + }); + return; +}; + + export const emitMempoolInfo = ({ params }: { params?: any } = {}) => { @@ -82,16 +134,22 @@ export const emitMempoolInfo = ({ switch (params.command) { case "init": { win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}'); - cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => { + cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'utf-8').then((fixture) => { win.mockSocket.send(JSON.stringify(fixture)); }); - cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => { + cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'utf-8').then((fixture) => { win.mockSocket.send(JSON.stringify(fixture)); }); break; } case "rbfTransaction": { - cy.readFile('cypress/fixtures/mainnet_rbf.json', 'ascii').then((fixture) => { + cy.readFile('cypress/fixtures/mainnet_rbf.json', 'utf-8').then((fixture) => { + win.mockSocket.send(JSON.stringify(fixture)); + }); + break; + } + case 'trackTx': { + cy.readFile('cypress/fixtures/track_tx.json', 'utf-8').then((fixture) => { win.mockSocket.send(JSON.stringify(fixture)); }); break; diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index f9f2576d67..70dc2edbac 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -27,5 +27,6 @@ "ACCELERATOR": false, "ACCELERATOR_BUTTON": true, "PUBLIC_ACCELERATIONS": false, + "STRATUM_ENABLED": false, "SERVICES_API": "https://mempool.space/api/v1/services" } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a75c49bf31..7f100b7937 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-frontend", - "version": "3.0.0-rc1", + "version": "3.3-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-frontend", - "version": "3.0.0-rc1", + "version": "3.3-dev", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", @@ -23,9 +23,9 @@ "@angular/router": "^17.3.1", "@angular/ssr": "^17.3.1", "@fortawesome/angular-fontawesome": "~0.14.1", - "@fortawesome/fontawesome-common-types": "~6.6.0", - "@fortawesome/fontawesome-svg-core": "~6.6.0", - "@fortawesome/free-solid-svg-icons": "~6.6.0", + "@fortawesome/fontawesome-common-types": "~6.7.2", + "@fortawesome/fontawesome-svg-core": "~6.7.2", + "@fortawesome/free-solid-svg-icons": "~6.7.2", "@mempool/mempool.js": "2.3.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0", "@types/qrcode": "~1.5.0", @@ -33,16 +33,15 @@ "browserify": "^17.0.0", "clipboard": "^2.0.11", "domino": "^2.1.6", - "echarts": "~5.5.0", - "esbuild": "^0.23.0", - "lightweight-charts": "~3.8.0", + "echarts": "~5.6.0", + "esbuild": "^0.25.8", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.6.0", + "tslib": "~2.8.0", "zone.js": "~0.14.4" }, "devDependencies": { @@ -51,7 +50,7 @@ "@types/node": "^18.11.9", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", - "browser-sync": "^3.0.0", + "browser-sync": "^3.0.3", "eslint": "^8.57.0", "http-proxy-middleware": "~2.0.6", "prettier": "^3.0.0", @@ -62,7 +61,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.13.0", + "cypress": "^13.17.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -699,6 +698,11 @@ "node": ">=10" } }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@angular-devkit/build-webpack": { "version": "0.1703.1", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz", @@ -3108,9 +3112,10 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", + "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", + "license": "Apache-2.0", "optional": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -3119,16 +3124,16 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.1", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -3136,6 +3141,22 @@ "node": ">= 6" } }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@cypress/schematic": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-2.5.0.tgz", @@ -3196,9 +3217,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -3211,9 +3232,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -3226,9 +3247,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -3241,9 +3262,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -3256,9 +3277,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -3271,9 +3292,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -3286,9 +3307,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -3301,9 +3322,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -3316,9 +3337,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -3331,9 +3352,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -3346,9 +3367,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -3361,9 +3382,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -3376,9 +3397,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -3391,9 +3412,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -3406,9 +3427,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -3421,9 +3442,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -3436,9 +3457,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -3450,10 +3471,25 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -3466,9 +3502,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -3481,9 +3517,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -3495,10 +3531,25 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -3511,9 +3562,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -3526,9 +3577,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -3541,9 +3592,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -3669,30 +3720,33 @@ } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", + "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", + "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" @@ -4308,9 +4362,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], @@ -4320,9 +4374,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], @@ -4332,9 +4386,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], @@ -4344,9 +4398,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], @@ -4356,9 +4410,21 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], @@ -4368,9 +4434,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], @@ -4380,9 +4446,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], @@ -4391,10 +4457,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], @@ -4403,10 +4481,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -4416,9 +4506,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -4428,9 +4518,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], @@ -4440,9 +4530,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], @@ -4452,9 +4542,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], @@ -4759,9 +4849,9 @@ "devOptional": true }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "devOptional": true, "dependencies": { "@types/node": "*" @@ -4796,9 +4886,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/express": { "version": "4.17.13", @@ -5632,6 +5722,7 @@ "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": "~2.1.0" @@ -5666,6 +5757,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", "optional": true, "engines": { "node": ">=0.8" @@ -5786,15 +5878,17 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", "optional": true, "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT", "optional": true }, "node_modules/axios": { @@ -5952,6 +6046,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { "tweetnacl": "^0.14.3" @@ -6014,9 +6109,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6026,7 +6121,7 @@ "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" @@ -6060,20 +6155,6 @@ "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -6182,13 +6263,13 @@ } }, "node_modules/browser-sync": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", - "integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", "devOptional": true, "dependencies": { - "browser-sync-client": "^3.0.2", - "browser-sync-ui": "^3.0.2", + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", "bs-recipes": "1.3.4", "chalk": "4.1.2", "chokidar": "^3.5.1", @@ -6202,15 +6283,15 @@ "fs-extra": "3.0.1", "http-proxy": "^1.18.1", "immutable": "^3", - "micromatch": "^4.0.2", + "micromatch": "^4.0.8", "opn": "5.3.0", "portscanner": "2.2.0", "raw-body": "^2.3.2", "resp-modifier": "6.0.2", "rx": "4.1.0", - "send": "0.16.2", - "serve-index": "1.9.1", - "serve-static": "1.13.2", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", "server-destroy": "1.0.1", "socket.io": "^4.4.1", "ua-parser-js": "^1.0.33", @@ -6224,9 +6305,9 @@ } }, "node_modules/browser-sync-client": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.2.tgz", - "integrity": "sha512-tBWdfn9L0wd2Pjuz/NWHtNEKthVb1Y67vg8/qyGNtCqetNz5lkDkFnrsx5UhPNPYUO8vci50IWC/BhYaQskDiQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", "devOptional": true, "dependencies": { "etag": "1.8.1", @@ -6238,9 +6319,9 @@ } }, "node_modules/browser-sync-ui": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.2.tgz", - "integrity": "sha512-V3FwWAI+abVbFLTyJjXJlCMBwjc3GXf/BPGfwO2fMFACWbIGW9/4SrBOFYEOOtqzCjQE0Di+U3VIb7eES4omNA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", "devOptional": true, "dependencies": { "async-each-series": "0.1.1", @@ -6385,30 +6466,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, - "node_modules/browser-sync/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "devOptional": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/browser-sync/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "devOptional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/browser-sync/node_modules/destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "devOptional": true - }, "node_modules/browser-sync/node_modules/fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -6429,27 +6486,6 @@ "node": ">=8" } }, - "node_modules/browser-sync/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "devOptional": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/browser-sync/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "devOptional": true - }, "node_modules/browser-sync/node_modules/jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -6459,75 +6495,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/browser-sync/node_modules/mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "devOptional": true, - "bin": { - "mime": "cli.js" - } - }, - "node_modules/browser-sync/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "devOptional": true - }, - "node_modules/browser-sync/node_modules/send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "devOptional": true, - "dependencies": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/browser-sync/node_modules/serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "devOptional": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/browser-sync/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "devOptional": true - }, - "node_modules/browser-sync/node_modules/statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "devOptional": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/browser-sync/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7116,6 +7083,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7155,6 +7134,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0", "optional": true }, "node_modules/chai": { @@ -7257,15 +7237,16 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "optional": true, "engines": { "node": ">=8" @@ -7492,30 +7473,22 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7529,6 +7502,33 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression/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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7668,9 +7668,9 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -8040,13 +8040,14 @@ "peer": true }, "node_modules/cypress": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "@cypress/request": "^3.0.0", + "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -8057,6 +8058,7 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", @@ -8071,7 +8073,6 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -8086,6 +8087,7 @@ "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", + "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -8288,6 +8290,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -8658,6 +8661,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8774,6 +8790,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", "optional": true, "dependencies": { "jsbn": "~0.1.0", @@ -8781,12 +8798,12 @@ } }, "node_modules/echarts": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", - "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "dependencies": { "tslib": "2.3.0", - "zrender": "5.5.0" + "zrender": "5.6.1" } }, "node_modules/echarts/node_modules/tslib": { @@ -8805,9 +8822,9 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -8879,9 +8896,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "devOptional": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -8889,98 +8906,47 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", "devOptional": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" } }, "node_modules/engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true, "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "devOptional": true, "engines": { "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -9086,12 +9052,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -9109,6 +9072,32 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -9205,9 +9194,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -9216,30 +9205,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/esbuild-wasm": { @@ -9870,36 +9861,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "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.10", "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", @@ -9918,6 +9909,14 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9934,20 +9933,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10049,6 +10034,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true }, "node_modules/falafel": { @@ -10065,11 +10051,6 @@ "node": ">=0.4.0" } }, - "node_modules/fancy-canvas": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz", - "integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA==" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10172,12 +10153,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "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", @@ -10196,6 +10177,14 @@ "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -10329,23 +10318,26 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", "optional": true, "engines": { "node": "*" } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "optional": true, "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/forwarded": { @@ -10487,15 +10479,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "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" }, "engines": { "node": ">= 0.4" @@ -10512,6 +10509,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -10536,6 +10545,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0" @@ -10635,11 +10645,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10718,21 +10728,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -10741,11 +10740,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -10809,9 +10808,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -10987,14 +10986,15 @@ } }, "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" }, "engines": { "node": ">=0.10" @@ -11356,18 +11356,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "optional": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -11617,6 +11605,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT", "optional": true }, "node_modules/is-unicode-supported": { @@ -11681,6 +11670,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT", "optional": true }, "node_modules/istanbul-lib-coverage": { @@ -11814,6 +11804,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT", "optional": true }, "node_modules/jsesc": { @@ -11842,6 +11833,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)", "optional": true }, "node_modules/json-schema-traverse": { @@ -11859,6 +11851,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", "optional": true }, "node_modules/json5": { @@ -11919,6 +11912,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "1.0.0", @@ -12242,14 +12236,6 @@ } } }, - "node_modules/lightweight-charts": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz", - "integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==", - "dependencies": { - "fancy-canvas": "0.2.2" - } - }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -12632,6 +12618,14 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "optional": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -12662,9 +12656,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -12688,12 +12685,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -13382,9 +13379,9 @@ "optional": true }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "dependencies": { "isarray": "0.0.1" @@ -13669,9 +13666,12 @@ } }, "node_modules/object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -13719,9 +13719,9 @@ } }, "node_modules/on-headers": { - "version": "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "engines": { "node": ">= 0.8" } @@ -14185,9 +14185,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -14240,6 +14240,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", "optional": true }, "node_modules/picocolors": { @@ -14670,12 +14671,6 @@ "node": ">= 0.10" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "optional": true - }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -14761,12 +14756,11 @@ } }, "node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "optional": true, + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -14792,12 +14786,6 @@ "node": ">=0.4.x" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "optional": true - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -15222,11 +15210,11 @@ } }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -15236,19 +15224,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -15472,9 +15463,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15613,19 +15604,27 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "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" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/server-destroy": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", @@ -15717,13 +15716,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15895,62 +15898,42 @@ } }, "node_modules/socket.io": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", - "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "devOptional": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.0", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "dependencies": { - "ws": "~8.11.0" - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "node_modules/socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", "devOptional": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.5.2", + "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" }, "engines": { @@ -16161,9 +16144,10 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", "optional": true, "dependencies": { "asn1": "~0.2.3", @@ -16713,6 +16697,26 @@ "readable-stream": "3" } }, + "node_modules/tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tldts-core": "^6.1.70" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "license": "MIT", + "optional": true + }, "node_modules/tlite": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz", @@ -16757,27 +16761,16 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "optional": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/transform-ast": { @@ -16925,9 +16918,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, "node_modules/tuf-js": { "version": "2.2.0", @@ -16946,6 +16939,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "optional": true, "dependencies": { "safe-buffer": "^5.0.1" @@ -16958,6 +16952,7 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense", "optional": true }, "node_modules/type": { @@ -17266,16 +17261,6 @@ "querystring": "0.2.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "optional": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -17343,6 +17328,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", @@ -17821,30 +17807,16 @@ } }, "node_modules/wait-on/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, - "node_modules/wait-on/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/wait-on/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -18298,9 +18270,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -18326,9 +18298,9 @@ } }, "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", "devOptional": true, "engines": { "node": ">=0.4.0" @@ -18509,9 +18481,9 @@ } }, "node_modules/zrender": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", - "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", "dependencies": { "tslib": "2.3.0" } @@ -18849,6 +18821,11 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -20493,9 +20470,9 @@ } }, "@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.7.tgz", + "integrity": "sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==", "optional": true, "requires": { "aws-sign2": "~0.7.0", @@ -20504,18 +20481,29 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.1", "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", + "tough-cookie": "^5.0.0", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" + }, + "dependencies": { + "qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "optional": true, + "requires": { + "side-channel": "^1.0.6" + } + } } }, "@cypress/schematic": { @@ -20572,147 +20560,159 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" }, "@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "optional": true }, "@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "optional": true }, "@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "optional": true }, "@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "optional": true }, "@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "optional": true }, "@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "optional": true }, "@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "optional": true }, "@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "optional": true }, "@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "optional": true }, "@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "optional": true }, "@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", "optional": true }, "@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "optional": true }, "@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "optional": true }, "@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "optional": true }, "@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "optional": true }, "@eslint-community/eslint-utils": { @@ -20794,24 +20794,24 @@ } }, "@fortawesome/fontawesome-common-types": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", - "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==" + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==" }, "@fortawesome/fontawesome-svg-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", - "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", + "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "requires": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" } }, "@fortawesome/free-solid-svg-icons": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", - "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", + "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", "requires": { - "@fortawesome/fontawesome-common-types": "6.6.0" + "@fortawesome/fontawesome-common-types": "6.7.2" } }, "@goto-bus-stop/common-shake": { @@ -21256,81 +21256,99 @@ "peer": true }, "@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "optional": true }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "optional": true }, "@schematics/angular": { @@ -21598,9 +21616,9 @@ "devOptional": true }, "@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "devOptional": true, "requires": { "@types/node": "*" @@ -21634,9 +21652,9 @@ } }, "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "@types/express": { "version": "4.17.13", @@ -22396,9 +22414,9 @@ "optional": true }, "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "optional": true }, "axios": { @@ -22572,9 +22590,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -22584,7 +22602,7 @@ "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" @@ -22610,14 +22628,6 @@ "requires": { "ee-first": "1.1.1" } - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } } } }, @@ -22707,13 +22717,13 @@ } }, "browser-sync": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", - "integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", "devOptional": true, "requires": { - "browser-sync-client": "^3.0.2", - "browser-sync-ui": "^3.0.2", + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", "bs-recipes": "1.3.4", "chalk": "4.1.2", "chokidar": "^3.5.1", @@ -22727,15 +22737,15 @@ "fs-extra": "3.0.1", "http-proxy": "^1.18.1", "immutable": "^3", - "micromatch": "^4.0.2", + "micromatch": "^4.0.8", "opn": "5.3.0", "portscanner": "2.2.0", "raw-body": "^2.3.2", "resp-modifier": "6.0.2", "rx": "4.1.0", - "send": "0.16.2", - "serve-index": "1.9.1", - "serve-static": "1.13.2", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", "server-destroy": "1.0.1", "socket.io": "^4.4.1", "ua-parser-js": "^1.0.33", @@ -22787,27 +22797,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "devOptional": true, - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "devOptional": true - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "devOptional": true - }, "fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -22825,24 +22814,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "devOptional": true }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "devOptional": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "devOptional": true - }, "jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -22852,63 +22823,6 @@ "graceful-fs": "^4.1.6" } }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "devOptional": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "devOptional": true - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "devOptional": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "devOptional": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "devOptional": true - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "devOptional": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -22971,9 +22885,9 @@ } }, "browser-sync-client": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.2.tgz", - "integrity": "sha512-tBWdfn9L0wd2Pjuz/NWHtNEKthVb1Y67vg8/qyGNtCqetNz5lkDkFnrsx5UhPNPYUO8vci50IWC/BhYaQskDiQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", "devOptional": true, "requires": { "etag": "1.8.1", @@ -22982,9 +22896,9 @@ } }, "browser-sync-ui": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.2.tgz", - "integrity": "sha512-V3FwWAI+abVbFLTyJjXJlCMBwjc3GXf/BPGfwO2fMFACWbIGW9/4SrBOFYEOOtqzCjQE0Di+U3VIb7eES4omNA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", "devOptional": true, "requires": { "async-each-series": "0.1.1", @@ -23437,6 +23351,15 @@ "set-function-length": "^1.2.1" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -23529,9 +23452,9 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" }, "ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "optional": true }, "cipher-base": { @@ -23713,24 +23636,19 @@ } }, "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -23743,6 +23661,16 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==" + }, + "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==" } } }, @@ -23854,9 +23782,9 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -24127,12 +24055,12 @@ "peer": true }, "cypress": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "optional": true, "requires": { - "@cypress/request": "^3.0.0", + "@cypress/request": "^3.0.6", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -24143,6 +24071,7 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", "commander": "^6.2.1", @@ -24157,7 +24086,6 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -24172,6 +24100,7 @@ "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.3", + "tree-kill": "1.2.2", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -24599,6 +24528,16 @@ "domhandler": "^5.0.3" } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -24697,12 +24636,12 @@ } }, "echarts": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz", - "integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "requires": { "tslib": "2.3.0", - "zrender": "5.5.0" + "zrender": "5.6.1" }, "dependencies": { "tslib": { @@ -24723,9 +24662,9 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "requires": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -24792,9 +24731,9 @@ } }, "engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "devOptional": true, "requires": { "@types/cookie": "^0.4.1", @@ -24802,60 +24741,38 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "dependencies": { "cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} } } }, "engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", "devOptional": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - }, - "dependencies": { - "engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" } }, "engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true }, "enhanced-resolve": { @@ -24939,12 +24856,9 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", @@ -24956,6 +24870,26 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==" }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "optional": true, + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -25044,34 +24978,36 @@ } }, "esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", - "requires": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "requires": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "esbuild-wasm": { @@ -25540,36 +25476,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "requires": { "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.10", "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", @@ -25585,6 +25521,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25598,14 +25539,6 @@ "ee-first": "1.1.1" } }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -25689,11 +25622,6 @@ "object-keys": "^1.0.6" } }, - "fancy-canvas": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-0.2.2.tgz", - "integrity": "sha512-50qi8xA0QkHbjmb8h7XQ6k2fvD7y/yMfiUw9YTarJ7rWrq6o5/3CCXPouYk+XSLASvvxtjyiQLRBFt3qkE3oyA==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -25778,12 +25706,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "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", @@ -25799,6 +25727,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25892,13 +25825,15 @@ "optional": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "optional": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -26003,15 +25938,20 @@ "optional": true }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "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-package-type": { @@ -26019,6 +25959,15 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -26111,12 +26060,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.9", @@ -26175,22 +26121,17 @@ "es-define-property": "^1.0.0" } }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" - }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" } }, "hash-base": { @@ -26230,9 +26171,9 @@ } }, "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "requires": { "function-bind": "^1.1.2" } @@ -26360,14 +26301,14 @@ } }, "http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "optional": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" } }, "https-browserify": { @@ -26624,15 +26565,6 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" }, - "is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "optional": true, - "requires": { - "ci-info": "^3.2.0" - } - }, "is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -27266,14 +27198,6 @@ "webpack-sources": "^3.0.0" } }, - "lightweight-charts": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-3.8.0.tgz", - "integrity": "sha512-7yFGnYuE1RjRJG9RwUTBz5wvF1QtjBOSW4FFlikr8Dh+/TDNt4ci+HsWSYmStgQUpawpvkCJ3j5/W25GppGj9Q==", - "requires": { - "fancy-canvas": "0.2.2" - } - }, "limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -27567,6 +27491,11 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "optional": true }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -27591,9 +27520,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -27611,12 +27540,12 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "miller-rabin": { @@ -28156,9 +28085,9 @@ "optional": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "requires": { "isarray": "0.0.1" @@ -28364,9 +28293,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "object-keys": { "version": "1.1.1", @@ -28399,9 +28328,9 @@ } }, "on-headers": { - "version": "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==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==" }, "once": { "version": "1.4.0", @@ -28740,9 +28669,9 @@ } }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -29057,12 +28986,6 @@ "event-stream": "=3.3.4" } }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "optional": true - }, "public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -29137,12 +29060,11 @@ } }, "qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "optional": true, + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "querystring": { @@ -29155,12 +29077,6 @@ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "optional": true - }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -29495,24 +29411,27 @@ } }, "rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", - "requires": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", - "@types/estree": "1.0.5", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "requires": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@types/estree": "1.0.6", "fsevents": "~2.3.2" } }, @@ -29663,9 +29582,9 @@ } }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -29786,14 +29705,21 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "server-destroy": { @@ -29869,13 +29795,14 @@ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -29993,47 +29920,39 @@ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, "socket.io": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", - "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", "devOptional": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.0", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "requires": { - "ws": "~8.11.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", "devOptional": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.5.2", + "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, @@ -30206,9 +30125,9 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "requires": { "asn1": "~0.2.3", @@ -30622,6 +30541,21 @@ } } }, + "tldts": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.70.tgz", + "integrity": "sha512-/W1YVgYVJd9ZDjey5NXadNh0mJXkiUMUue9Zebd0vpdo1sU+H4zFFTaJ1RKD4N6KFoHfcXy6l+Vu7bh+bdWCzA==", + "optional": true, + "requires": { + "tldts-core": "^6.1.70" + } + }, + "tldts-core": { + "version": "6.1.70", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.70.tgz", + "integrity": "sha512-RNnIXDB1FD4T9cpQRErEqw6ZpjLlGdMOitdV+0xtbsnwr4YFka1zpc7D4KD+aAn8oSG5JyFrdasZTE04qDE9Yg==", + "optional": true + }, "tlite": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/tlite/-/tlite-0.1.9.tgz", @@ -30654,23 +30588,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "optional": true, "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "dependencies": { - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "optional": true - } + "tldts": "^6.1.32" } }, "transform-ast": { @@ -30763,9 +30686,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, "tuf-js": { "version": "2.2.0", @@ -31006,16 +30929,6 @@ } } }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "optional": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -31277,27 +31190,16 @@ }, "dependencies": { "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -31612,9 +31514,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xhr2": { @@ -31623,9 +31525,9 @@ "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" }, "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", "devOptional": true }, "xtend": { @@ -31766,9 +31668,9 @@ } }, "zrender": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz", - "integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", "requires": { "tslib": "2.3.0" }, diff --git a/frontend/package.json b/frontend/package.json index b9fa4d3bc6..56a5521954 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.0.0-rc1", + "version": "3.3-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", @@ -23,16 +23,14 @@ "ng": "./node_modules/@angular/cli/bin/ng.js", "tsc": "./node_modules/typescript/bin/tsc", "i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf", - "i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force", + "i18n-pull-from-transifex": "tx pull -a --minimum-perc 1 --force", "serve": "npm run generate-config && npm run ng -- serve -c local", - "serve:stg": "npm run generate-config && npm run ng -- serve -c staging", "serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod", - "serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging", + "serve:parameterized": "npm run generate-config && npm run ng -- serve -c parameterized", "start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local", + "start:parameterized": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized", "start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora", - "start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging", "start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod", - "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging", "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js", "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'", @@ -58,8 +56,8 @@ "cypress:run:record": "cypress run --record", "cypress:open:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open", "cypress:run:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record", - "cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open", - "cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record" + "cypress:open:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:open", + "cypress:run:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:run:record" }, "dependencies": { "@angular-devkit/build-angular": "^17.3.1", @@ -76,9 +74,9 @@ "@angular/router": "^17.3.1", "@angular/ssr": "^17.3.1", "@fortawesome/angular-fontawesome": "~0.14.1", - "@fortawesome/fontawesome-common-types": "~6.6.0", - "@fortawesome/fontawesome-svg-core": "~6.6.0", - "@fortawesome/free-solid-svg-icons": "~6.6.0", + "@fortawesome/fontawesome-common-types": "~6.7.2", + "@fortawesome/fontawesome-svg-core": "~6.7.2", + "@fortawesome/free-solid-svg-icons": "~6.7.2", "@mempool/mempool.js": "2.3.0", "@ng-bootstrap/ng-bootstrap": "^16.0.0", "@types/qrcode": "~1.5.0", @@ -86,16 +84,15 @@ "browserify": "^17.0.0", "clipboard": "^2.0.11", "domino": "^2.1.6", - "echarts": "~5.5.0", - "lightweight-charts": "~3.8.0", + "echarts": "~5.6.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", - "esbuild": "^0.23.0", + "esbuild": "^0.25.8", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.6.0", + "tslib": "~2.8.0", "zone.js": "~0.14.4" }, "devDependencies": { @@ -105,7 +102,7 @@ "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", "eslint": "^8.57.0", - "browser-sync": "^3.0.0", + "browser-sync": "^3.0.3", "http-proxy-middleware": "~2.0.6", "prettier": "^3.0.0", "source-map-support": "^0.5.21", @@ -115,7 +112,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.13.0", + "cypress": "^13.17.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", diff --git a/frontend/proxy.conf.parameterized.js b/frontend/proxy.conf.parameterized.js new file mode 100644 index 0000000000..eee9bf1f72 --- /dev/null +++ b/frontend/proxy.conf.parameterized.js @@ -0,0 +1,36 @@ +const fs = require('fs'); + +const PROXY_CONFIG = require('./proxy.conf'); + +const addApiKeyHeader = (proxyReq, req, res) => { + if (process.env.MEMPOOL_CI_API_KEY) { + proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY); + } +}; + +PROXY_CONFIG.forEach((entry) => { + const mempoolHostname = process.env.MEMPOOL_HOSTNAME + ? process.env.MEMPOOL_HOSTNAME + : 'mempool.space'; + + const liquidHostname = process.env.LIQUID_HOSTNAME + ? process.env.LIQUID_HOSTNAME + : 'liquid.network'; + + entry.target = entry.target.replace('mempool.space', mempoolHostname); + entry.target = entry.target.replace('liquid.network', liquidHostname); + + if (entry.onProxyReq) { + const originalProxyReq = entry.onProxyReq; + entry.onProxyReq = (proxyReq, req, res) => { + originalProxyReq(proxyReq, req, res); + if (process.env.MEMPOOL_CI_API_KEY) { + proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY); + } + }; + } else { + entry.onProxyReq = addApiKeyHeader; + } +}); + +module.exports = PROXY_CONFIG; diff --git a/frontend/proxy.conf.staging.js b/frontend/proxy.conf.staging.js deleted file mode 100644 index e24662038e..0000000000 --- a/frontend/proxy.conf.staging.js +++ /dev/null @@ -1,10 +0,0 @@ -const fs = require('fs'); - -let PROXY_CONFIG = require('./proxy.conf'); - -PROXY_CONFIG.forEach(entry => { - entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space"); - entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space"); -}); - -module.exports = PROXY_CONFIG; diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1f2e3f5311..5f69b55cf0 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -1,15 +1,13 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AppPreloadingStrategy } from './app.preloading-strategy' -import { BlockViewComponent } from './components/block-view/block-view.component'; -import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; -import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; -import { ClockComponent } from './components/clock/clock.component'; -import { StatusViewComponent } from './components/status-view/status-view.component'; -import { AddressGroupComponent } from './components/address-group/address-group.component'; -import { TrackerComponent } from './components/tracker/tracker.component'; -import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component'; -import { TrackerGuard } from './route-guards'; +import { AppPreloadingStrategy } from '@app/app.preloading-strategy' +import { BlockViewComponent } from '@components/block-view/block-view.component'; +import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component'; +import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component'; +import { ClockComponent } from '@components/clock/clock.component'; +import { StatusViewComponent } from '@components/status-view/status-view.component'; +import { AddressGroupComponent } from '@components/address-group/address-group.component'; +import { TrackerGuard } from '@app/route-guards'; const browserWindow = window || {}; // @ts-ignore @@ -22,16 +20,16 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -45,7 +43,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { @@ -60,12 +58,12 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { @@ -83,7 +81,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { @@ -103,16 +101,16 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -126,7 +124,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { @@ -138,22 +136,22 @@ let routes: Routes = [ { path: '', pathMatch: 'full', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, { path: 'tx', canMatch: [TrackerGuard], runGuardsAndResolvers: 'always', - loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), + loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule), }, { path: '', - loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -165,19 +163,19 @@ let routes: Routes = [ children: [ { path: '', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'testnet', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'testnet4', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'signet', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, ], }, @@ -212,7 +210,7 @@ let routes: Routes = [ }, { path: '', - loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), data: { preload: true }, }, ]; @@ -225,16 +223,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: '', pathMatch: 'full', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), + loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -248,7 +246,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: '', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, { @@ -260,16 +258,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: '', pathMatch: 'full', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, { path: '', - loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), + loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -281,11 +279,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [ { path: '', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, { path: 'testnet', - loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) + loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule) }, ], }, @@ -296,7 +294,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: '', - loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), + loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), data: { preload: true }, }, ]; diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index cef630984e..2b6661fccf 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -1,4 +1,5 @@ export const defaultMempoolFeeColors = [ + '007d3d', '557d00', '5d7d01', '637d02', @@ -40,6 +41,7 @@ export const defaultMempoolFeeColors = [ ]; export const contrastMempoolFeeColors = [ + '06adef', '0082e6', '0984df', '1285d9', @@ -81,6 +83,7 @@ export const contrastMempoolFeeColors = [ ]; export const chartColors = [ + "#A81524", "#D81B60", "#8E24AA", "#5E35B1", @@ -120,12 +123,13 @@ export const chartColors = [ "#263238", "#801313", ]; +export const originalChartColors = chartColors.slice(1); export const poolsColor = { 'unknown': '#FDD835', }; -export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, +export const feeLevels = [0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; export interface Language { @@ -270,7 +274,12 @@ export const specialBlocks = { labelEvent: 'Bitcoin\'s 15th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block', networks: ['mainnet', 'testnet', 'testnet4'], - } + }, + '3477600': { + labelEvent: 'Simplicity activation', + labelEventCompleted: 'Simplicity has been activated!', + networks: ['liquid'], + }, }; export const fiatCurrencies = { @@ -439,4 +448,39 @@ export const fiatCurrencies = { code: 'ZAR', indexed: true, }, -}; \ No newline at end of file +}; + +export interface Timezone { + offset: string; + name: string; +} + +export const timezones: Timezone[] = [ + { offset: '-12', name: 'Anywhere on Earth (AoE)' }, + { offset: '-11', name: 'Samoa Standard Time (SST)' }, + { offset: '-10', name: 'Hawaii Standard Time (HST)' }, + { offset: '-9', name: 'Alaska Standard Time (AKST)' }, + { offset: '-8', name: 'Pacific Standard Time (PST)' }, + { offset: '-7', name: 'Mountain Standard Time (MST)' }, + { offset: '-6', name: 'Central Standard Time (CST)' }, + { offset: '-5', name: 'Eastern Standard Time (EST)' }, + { offset: '-4', name: 'Atlantic Standard Time (AST)' }, + { offset: '-3', name: 'Argentina Time (ART)' }, + { offset: '-2', name: 'Fernando de Noronha Time (FNT)' }, + { offset: '-1', name: 'Azores Time (AZOT)' }, + { offset: '+0', name: 'Greenwich Mean Time (GMT)' }, + { offset: '+1', name: 'Central European Time (CET)' }, + { offset: '+2', name: 'Eastern European Time (EET)' }, + { offset: '+3', name: 'Moscow Standard Time (MSK)' }, + { offset: '+4', name: 'Armenia Time (AMT)' }, + { offset: '+5', name: 'Pakistan Standard Time (PKT)' }, + { offset: '+6', name: 'Xinjiang Time (XJT)' }, + { offset: '+7', name: 'Indochina Time (ICT)' }, + { offset: '+8', name: 'Hong Kong Time (HKT)' }, + { offset: '+9', name: 'Japan Standard Time (JST)' }, + { offset: '+10', name: 'Australian Eastern Standard Time (AEST)' }, + { offset: '+11', name: 'Norfolk Time (NFT)' }, + { offset: '+12', name: 'New Zealand Standard Time (NZST)' }, + { offset: '+13', name: 'Tonga Time (TOT)' }, + { offset: '+14', name: 'Line Islands Time (LINT)' } +]; \ No newline at end of file diff --git a/frontend/src/app/app.module.server.ts b/frontend/src/app/app.module.server.ts index 4149fa5938..56096891da 100644 --- a/frontend/src/app/app.module.server.ts +++ b/frontend/src/app/app.module.server.ts @@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; -import { ZONE_SERVICE } from './injection-tokens'; +import { ZONE_SERVICE } from '@app/injection-tokens'; import { AppModule } from './app.module'; -import { AppComponent } from './components/app/app.component'; -import { HttpCacheInterceptor } from './services/http-cache.interceptor'; -import { ZoneService } from './services/zone.service'; +import { AppComponent } from '@components/app/app.component'; +import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; +import { ZoneService } from '@app/services/zone.service'; @NgModule({ @@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service'; ], bootstrap: [AppComponent], }) -export class AppServerModule {} \ No newline at end of file +export class AppServerModule {} diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 50bbd88b90..1b764c0030 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -2,35 +2,38 @@ import { BrowserModule } from '@angular/platform-browser'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ZONE_SERVICE } from './injection-tokens'; +import { ZONE_SERVICE } from '@app/injection-tokens'; import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './components/app/app.component'; -import { ElectrsApiService } from './services/electrs-api.service'; -import { StateService } from './services/state.service'; -import { CacheService } from './services/cache.service'; -import { PriceService } from './services/price.service'; -import { EnterpriseService } from './services/enterprise.service'; -import { WebsocketService } from './services/websocket.service'; -import { AudioService } from './services/audio.service'; -import { PreloadService } from './services/preload.service'; -import { SeoService } from './services/seo.service'; -import { OpenGraphService } from './services/opengraph.service'; -import { ZoneService } from './services/zone-shim.service'; -import { SharedModule } from './shared/shared.module'; -import { StorageService } from './services/storage.service'; -import { HttpCacheInterceptor } from './services/http-cache.interceptor'; -import { LanguageService } from './services/language.service'; -import { ThemeService } from './services/theme.service'; -import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; -import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; -import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; -import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; -import { AppPreloadingStrategy } from './app.preloading-strategy'; -import { ServicesApiServices } from './services/services-api.service'; +import { AppComponent } from '@components/app/app.component'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { OrdApiService } from '@app/services/ord-api.service'; +import { StateService } from '@app/services/state.service'; +import { CacheService } from '@app/services/cache.service'; +import { PriceService } from '@app/services/price.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; +import { WebsocketService } from '@app/services/websocket.service'; +import { AudioService } from '@app/services/audio.service'; +import { PreloadService } from '@app/services/preload.service'; +import { SeoService } from '@app/services/seo.service'; +import { OpenGraphService } from '@app/services/opengraph.service'; +import { ZoneService } from '@app/services/zone-shim.service'; +import { SharedModule } from '@app/shared/shared.module'; +import { StorageService } from '@app/services/storage.service'; +import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor'; +import { LanguageService } from '@app/services/language.service'; +import { ThemeService } from '@app/services/theme.service'; +import { TimeService } from '@app/services/time.service'; +import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe'; +import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe'; +import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe'; +import { AppPreloadingStrategy } from '@app/app.preloading-strategy'; +import { ServicesApiServices } from '@app/services/services-api.service'; import { DatePipe } from '@angular/common'; const providers = [ ElectrsApiService, + OrdApiService, StateService, CacheService, PriceService, @@ -42,6 +45,7 @@ const providers = [ EnterpriseService, LanguageService, ThemeService, + TimeService, ShortenStringPipe, FiatShortenerPipe, FiatCurrencyPipe, diff --git a/frontend/src/app/bitcoin-graphs.module.ts b/frontend/src/app/bitcoin-graphs.module.ts index 7107432450..f5b1557b18 100644 --- a/frontend/src/app/bitcoin-graphs.module.ts +++ b/frontend/src/app/bitcoin-graphs.module.ts @@ -1,13 +1,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; -import { MasterPageComponent } from './components/master-page/master-page.component'; +import { MasterPageComponent } from '@components/master-page/master-page.component'; const routes: Routes = [ { path: '', component: MasterPageComponent, - loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule), + loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule), data: { preload: true }, } ]; diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 92d3de7f39..b949cde3c2 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -1,5 +1,5 @@ -import { Transaction, Vin } from './interfaces/electrs.interface'; -import { Hash } from './shared/sha256'; +import { Transaction, Vin } from '@interfaces/electrs.interface'; +import { Hash } from '@app/shared/sha256'; const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH @@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb return; } const opN = ops.pop(); - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)[0], 10); @@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb } } const opM = ops.pop(); - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)[0], 10); @@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise { return hashArray .map((bytes) => bytes.toString(16).padStart(2, '0')) .join(''); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/about/about-sponsors.component.ts b/frontend/src/app/components/about/about-sponsors.component.ts index 6a47c3bd4b..f42944173b 100644 --- a/frontend/src/app/components/about/about-sponsors.component.ts +++ b/frontend/src/app/components/about/about-sponsors.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { EnterpriseService } from '../../services/enterprise.service'; +import { EnterpriseService } from '@app/services/enterprise.service'; @Component({ selector: 'app-about-sponsors', diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 1af8d8e62e..5484342d0e 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -12,6 +12,7 @@
The Mempool Open Source Project ®

Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.

+
Be your own explorer