mirror of
https://github.com/Retropex/mempool.git
synced 2025-05-12 18:20:41 +02:00
commit
52e1919aba
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@ -3,16 +3,19 @@ name: CI Pipeline for the Backend and Frontend
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, review_requested, synchronize]
|
types: [opened, review_requested, synchronize]
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend:
|
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["20", "21"]
|
node: ["22.14.0"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
||||||
steps:
|
steps:
|
||||||
@ -66,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
cache:
|
cache:
|
||||||
name: "Cache assets for builds"
|
name: "Cache assets for builds"
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -157,13 +160,13 @@ jobs:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
needs: cache
|
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ["20", "21"]
|
node: ["22.14.0"]
|
||||||
flavor: ["dev", "prod"]
|
flavor: ["dev", "prod"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
||||||
steps:
|
steps:
|
||||||
@ -245,8 +248,8 @@ jobs:
|
|||||||
VERBOSE: 1
|
VERBOSE: 1
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
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'"
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
needs: frontend
|
needs: frontend
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -263,7 +266,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
|
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
|
||||||
|
|
||||||
@ -309,7 +312,7 @@ jobs:
|
|||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
working-directory: ${{ matrix.module }}/frontend
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
build: npm run config:defaults:${{ matrix.module }}
|
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: "http://localhost:4200"
|
||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
@ -334,7 +337,7 @@ jobs:
|
|||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
working-directory: ${{ matrix.module }}/frontend
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
build: npm run config:defaults:${{ matrix.module }}
|
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: "http://localhost:4200"
|
||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
@ -359,7 +362,7 @@ jobs:
|
|||||||
tag: ${{ github.event_name }}
|
tag: ${{ github.event_name }}
|
||||||
working-directory: ${{ matrix.module }}/frontend
|
working-directory: ${{ matrix.module }}/frontend
|
||||||
build: npm run config:defaults:mempool
|
build: npm run config:defaults:mempool
|
||||||
start: npm run start:local-staging
|
start: npm run start:parameterized
|
||||||
wait-on: "http://localhost:4200"
|
wait-on: "http://localhost:4200"
|
||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
record: true
|
record: true
|
||||||
@ -375,10 +378,9 @@ jobs:
|
|||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
|
||||||
validate_docker_json:
|
validate_docker_json:
|
||||||
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'"
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
name: Validate generated backend Docker JSON
|
name: Validate generated backend Docker JSON
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
181
.github/workflows/docker_update_latest_tag.yml
vendored
Normal file
181
.github/workflows/docker_update_latest_tag.yml
vendored
Normal file
@ -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 }}"
|
273
.github/workflows/e2e_parameterized.yml
vendored
Normal file
273
.github/workflows/e2e_parameterized.yml
vendored
Normal file
@ -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 }}
|
@ -4,7 +4,7 @@ on: [workflow_dispatch]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
print-backend-sha:
|
print-backend-sha:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: ubuntu-latest
|
||||||
name: Get block height
|
name: Get block height
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
2
.github/workflows/get_backend_hash.yml
vendored
2
.github/workflows/get_backend_hash.yml
vendored
@ -4,7 +4,7 @@ on: [workflow_dispatch]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
print-backend-sha:
|
print-backend-sha:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: ubuntu-latest
|
||||||
name: Print backend hashes
|
name: Print backend hashes
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
2
.github/workflows/get_image_digest.yml
vendored
2
.github/workflows/get_image_digest.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
jobs:
|
jobs:
|
||||||
print-images-sha:
|
print-images-sha:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: ubuntu-latest
|
||||||
name: Print digest for images
|
name: Print digest for images
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
29
.github/workflows/on-tag.yml
vendored
29
.github/workflows/on-tag.yml
vendored
@ -2,7 +2,7 @@ name: Docker build on tag
|
|||||||
env:
|
env:
|
||||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||||
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
|
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
|
||||||
DOCKER_BUILDKIT: 0
|
DOCKER_BUILDKIT: 1 # Enable BuildKit for better performance
|
||||||
COMPOSE_DOCKER_CLI_BUILD: 0
|
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@ -25,13 +25,12 @@ jobs:
|
|||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
name: Build and push to DockerHub
|
name: Build and push to DockerHub
|
||||||
steps:
|
steps:
|
||||||
# Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1
|
|
||||||
- name: Replace the current swap file
|
- name: Replace the current swap file
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo swapoff /mnt/swapfile
|
sudo swapoff /mnt/swapfile || true
|
||||||
sudo rm -v /mnt/swapfile
|
sudo rm -f /mnt/swapfile
|
||||||
sudo fallocate -l 13G /mnt/swapfile
|
sudo fallocate -l 16G /mnt/swapfile
|
||||||
sudo chmod 600 /mnt/swapfile
|
sudo chmod 600 /mnt/swapfile
|
||||||
sudo mkswap /mnt/swapfile
|
sudo mkswap /mnt/swapfile
|
||||||
sudo swapon /mnt/swapfile
|
sudo swapon /mnt/swapfile
|
||||||
@ -50,7 +49,7 @@ jobs:
|
|||||||
echo "Directory '/var/lib/docker' not found"
|
echo "Directory '/var/lib/docker' not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 systemctl restart docker
|
||||||
sudo df -h | grep docker
|
sudo df -h | grep docker
|
||||||
|
|
||||||
@ -75,10 +74,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
id: qemu
|
id: qemu
|
||||||
|
|
||||||
- name: Setup Docker buildx action
|
- name: Setup Docker buildx action
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
driver-opts: |
|
||||||
|
network=host
|
||||||
id: buildx
|
id: buildx
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
@ -89,19 +94,19 @@ jobs:
|
|||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-
|
${{ runner.os }}-buildx-${{ matrix.service }}-
|
||||||
|
|
||||||
- name: Run Docker buildx for ${{ matrix.service }} against tag
|
- name: Run Docker buildx for ${{ matrix.service }} against tag
|
||||||
run: |
|
run: |
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
--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 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
||||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
|
|
||||||
--build-context rustgbt=./rust \
|
--build-context rustgbt=./rust \
|
||||||
--build-context backend=./backend \
|
--build-context backend=./backend \
|
||||||
--output "type=registry" ./${{ matrix.service }}/ \
|
--output "type=registry,push=true" \
|
||||||
--build-arg commitHash=$SHORT_SHA
|
--build-arg commitHash=$SHORT_SHA \
|
||||||
|
./${{ matrix.service }}/
|
2
LICENSE
2
LICENSE
@ -10,7 +10,7 @@ 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.
|
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.
|
or any other contributor to The Mempool Open Source Project.
|
||||||
|
|
||||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®,
|
||||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||||
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||||
|
32
backend/package-lock.json
generated
32
backend/package-lock.json
generated
@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.1.0-dev",
|
"version": "3.2.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.1.0-dev",
|
"version": "3.2.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"axios": "1.7.2",
|
"axios": "1.8.1",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.21.1",
|
"express": "~4.21.1",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.12.0",
|
"mysql2": "~3.13.0",
|
||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
@ -2275,9 +2275,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.2",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
@ -6173,9 +6173,9 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/mysql2": {
|
"node_modules/mysql2": {
|
||||||
"version": "3.12.0",
|
"version": "3.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
|
||||||
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
|
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-ssl-profiles": "^1.1.1",
|
"aws-ssl-profiles": "^1.1.1",
|
||||||
@ -9459,9 +9459,9 @@
|
|||||||
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
|
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "1.7.2",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@ -12337,9 +12337,9 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"mysql2": {
|
"mysql2": {
|
||||||
"version": "3.12.0",
|
"version": "3.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
|
||||||
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
|
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"aws-ssl-profiles": "^1.1.1",
|
"aws-ssl-profiles": "^1.1.1",
|
||||||
"denque": "^2.1.0",
|
"denque": "^2.1.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.1.0-dev",
|
"version": "3.2.0",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@ -41,12 +41,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"axios": "1.7.2",
|
"axios": "1.8.1",
|
||||||
"bitcoinjs-lib": "~6.1.3",
|
"bitcoinjs-lib": "~6.1.3",
|
||||||
"crypto-js": "~4.2.0",
|
"crypto-js": "~4.2.0",
|
||||||
"express": "~4.21.1",
|
"express": "~4.21.1",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.12.0",
|
"mysql2": "~3.13.0",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
|
@ -55,6 +55,8 @@ class BitcoinRoutes {
|
|||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
.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', this.getBlocksByBulk.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', 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
|
// Temporarily add txs/package endpoint for all backends until esplora supports it
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
|
||||||
// Internal routes
|
// Internal routes
|
||||||
@ -404,8 +406,8 @@ class BitcoinRoutes {
|
|||||||
|
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
|
||||||
res.json(block);
|
res.json(block);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
handleError(req, res, 500, 'Failed to get block');
|
handleError(req, res, e?.response?.status === 404 ? 404 : 500, 'Failed to get block');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -981,6 +983,92 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
export default new BitcoinRoutes();
|
||||||
|
@ -36,7 +36,7 @@ class FailoverRouter {
|
|||||||
maxHeight: number = 0;
|
maxHeight: number = 0;
|
||||||
hosts: FailoverHost[];
|
hosts: FailoverHost[];
|
||||||
multihost: boolean;
|
multihost: boolean;
|
||||||
gitHashInterval: number = 600000; // 10 minutes
|
gitHashInterval: number = 60000; // 1 minute
|
||||||
pollInterval: number = 60000; // 1 minute
|
pollInterval: number = 60000; // 1 minute
|
||||||
pollTimer: NodeJS.Timeout | null = null;
|
pollTimer: NodeJS.Timeout | null = null;
|
||||||
pollConnection = axios.create();
|
pollConnection = axios.create();
|
||||||
@ -111,7 +111,7 @@ class FailoverRouter {
|
|||||||
for (const host of this.hosts) {
|
for (const host of this.hosts) {
|
||||||
try {
|
try {
|
||||||
const result = await (host.socket
|
const result = await (host.socket
|
||||||
? this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT })
|
? this.pollConnection.get<number>('http://api/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT })
|
||||||
: this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT })
|
: this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT })
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
@ -288,7 +288,7 @@ class FailoverRouter {
|
|||||||
let url;
|
let url;
|
||||||
if (host.socket) {
|
if (host.socket) {
|
||||||
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
axiosConfig = { socketPath: host.host, timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
||||||
url = path;
|
url = 'http://api' + path;
|
||||||
} else {
|
} else {
|
||||||
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
axiosConfig = { timeout: config.ESPLORA.REQUEST_TIMEOUT, responseType };
|
||||||
url = host.host + path;
|
url = host.host + path;
|
||||||
|
@ -1391,7 +1391,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
|
public async $getBlockAuditSummary(hash: string): Promise<BlockAudit | null> {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) {
|
||||||
return BlocksAuditsRepository.$getBlockAudit(hash);
|
return BlocksAuditsRepository.$getBlockAudit(hash);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@ -1399,7 +1399,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
|
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && Common.auditIndexingEnabled()) {
|
||||||
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
|
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
@ -1469,11 +1469,11 @@ class Blocks {
|
|||||||
if (rows && Array.isArray(rows)) {
|
if (rows && Array.isArray(rows)) {
|
||||||
return rows.map(r => r.definition_hash);
|
return rows.map(r => r.definition_hash);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
|
logger.debug(`Unable to retrieve list of blocks.definition_hash from db (no result)`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
|
logger.debug(`Unable to retrieve list of blocks.definition_hash from db (exception: ${e})`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1484,11 +1484,11 @@ class Blocks {
|
|||||||
if (rows && Array.isArray(rows)) {
|
if (rows && Array.isArray(rows)) {
|
||||||
return rows.map(r => r.hash);
|
return rows.map(r => r.hash);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
|
logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (no result)`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
|
logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -739,6 +739,13 @@ export class Common {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static auditIndexingEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
Common.indexingEnabled() &&
|
||||||
|
config.MEMPOOL.AUDIT === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static gogglesIndexingEnabled(): boolean {
|
static gogglesIndexingEnabled(): boolean {
|
||||||
return (
|
return (
|
||||||
Common.blocksSummariesIndexingEnabled() &&
|
Common.blocksSummariesIndexingEnabled() &&
|
||||||
|
@ -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
|
* 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)
|
* 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)) {
|
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
|
||||||
tx.cpfpDirty = false;
|
tx.cpfpDirty = false;
|
||||||
return {
|
return {
|
||||||
@ -198,17 +200,26 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool:
|
|||||||
totalFee += tx.fees.base;
|
totalFee += tx.fees.base;
|
||||||
}
|
}
|
||||||
const effectiveFeePerVsize = totalFee / totalVsize;
|
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 {
|
return {
|
||||||
ancestors: tx.ancestors || [],
|
ancestors: tx.ancestors || [],
|
||||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 95;
|
private static currentVersion = 96;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -1130,6 +1130,11 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
|
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
|
||||||
await this.updateToSchemaVersion(95);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,6 +8,7 @@ import mining from './mining/mining';
|
|||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
import BlocksRepository from '../repositories/BlocksRepository';
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
import redisCache from './redis-cache';
|
import redisCache from './redis-cache';
|
||||||
|
import blocks from './blocks';
|
||||||
|
|
||||||
class PoolsParser {
|
class PoolsParser {
|
||||||
miningPools: any[] = [];
|
miningPools: any[] = [];
|
||||||
@ -42,6 +43,8 @@ class PoolsParser {
|
|||||||
await this.$insertUnknownPool();
|
await this.$insertUnknownPool();
|
||||||
|
|
||||||
let reindexUnknown = false;
|
let reindexUnknown = false;
|
||||||
|
let clearCache = false;
|
||||||
|
|
||||||
|
|
||||||
for (const pool of this.miningPools) {
|
for (const pool of this.miningPools) {
|
||||||
if (!pool.id) {
|
if (!pool.id) {
|
||||||
@ -78,17 +81,20 @@ class PoolsParser {
|
|||||||
logger.debug(`Inserting new mining pool ${pool.name}`);
|
logger.debug(`Inserting new mining pool ${pool.name}`);
|
||||||
await PoolsRepository.$insertNewMiningPool(pool, slug);
|
await PoolsRepository.$insertNewMiningPool(pool, slug);
|
||||||
reindexUnknown = true;
|
reindexUnknown = true;
|
||||||
|
clearCache = true;
|
||||||
} else {
|
} else {
|
||||||
if (poolDB.name !== pool.name) {
|
if (poolDB.name !== pool.name) {
|
||||||
// Pool has been renamed
|
// Pool has been renamed
|
||||||
const newSlug = pool.name.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
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}`);
|
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);
|
await PoolsRepository.$renameMiningPool(poolDB.id, newSlug, pool.name);
|
||||||
|
clearCache = true;
|
||||||
}
|
}
|
||||||
if (poolDB.link !== pool.link) {
|
if (poolDB.link !== pool.link) {
|
||||||
// Pool link has changed
|
// Pool link has changed
|
||||||
logger.debug(`Updating link for ${pool.name} mining pool`);
|
logger.debug(`Updating link for ${pool.name} mining pool`);
|
||||||
await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link);
|
await PoolsRepository.$updateMiningPoolLink(poolDB.id, pool.link);
|
||||||
|
clearCache = true;
|
||||||
}
|
}
|
||||||
if (JSON.stringify(pool.addresses) !== poolDB.addresses ||
|
if (JSON.stringify(pool.addresses) !== poolDB.addresses ||
|
||||||
JSON.stringify(pool.regexes) !== poolDB.regexes) {
|
JSON.stringify(pool.regexes) !== poolDB.regexes) {
|
||||||
@ -96,6 +102,7 @@ class PoolsParser {
|
|||||||
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
|
logger.notice(`Updating addresses and/or coinbase tags for ${pool.name} mining pool.`);
|
||||||
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
|
await PoolsRepository.$updateMiningPoolTags(poolDB.id, pool.addresses, pool.regexes);
|
||||||
reindexUnknown = true;
|
reindexUnknown = true;
|
||||||
|
clearCache = true;
|
||||||
await this.$reindexBlocksForPool(poolDB.id);
|
await this.$reindexBlocksForPool(poolDB.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,6 +118,19 @@ class PoolsParser {
|
|||||||
}
|
}
|
||||||
await this.$reindexBlocksForPool(unknownPool.id);
|
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 {
|
public matchBlockMiner(scriptsig: string, addresses: string[], pools: PoolTag[]): PoolTag | undefined {
|
||||||
|
@ -30,6 +30,7 @@ const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
|
|||||||
class WalletApi {
|
class WalletApi {
|
||||||
private wallets: Record<string, Wallet> = {};
|
private wallets: Record<string, Wallet> = {};
|
||||||
private syncing = false;
|
private syncing = false;
|
||||||
|
private lastSync = 0;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
|
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
|
||||||
@ -47,7 +48,38 @@ class WalletApi {
|
|||||||
if (!config.WALLETS.ENABLED || this.syncing) {
|
if (!config.WALLETS.ENABLED || this.syncing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncing = true;
|
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<string, boolean> = 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const walletKey of Object.keys(this.wallets)) {
|
for (const walletKey of Object.keys(this.wallets)) {
|
||||||
const wallet = this.wallets[walletKey];
|
const wallet = this.wallets[walletKey];
|
||||||
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
|
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
|
||||||
@ -72,6 +104,7 @@ class WalletApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.syncing = false;
|
this.syncing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +420,29 @@ class TransactionUtils {
|
|||||||
|
|
||||||
return { prioritized, deprioritized };
|
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();
|
export default new TransactionUtils();
|
||||||
|
@ -1011,15 +1011,19 @@ class WebsocketHandler {
|
|||||||
const blockTransactions = structuredClone(transactions);
|
const blockTransactions = structuredClone(transactions);
|
||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
await statistics.runStatistics();
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||||
|
await statistics.runStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
const candidateTxs = await memPool.getMempoolCandidates();
|
const candidateTxs = await memPool.getMempoolCandidates();
|
||||||
let candidates: GbtCandidates | undefined = (memPool.limitGBT && candidateTxs) ? { txs: candidateTxs, added: [], removed: [] } : undefined;
|
let candidates: GbtCandidates | undefined = (memPool.limitGBT && candidateTxs) ? { txs: candidateTxs, added: [], removed: [] } : undefined;
|
||||||
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
||||||
|
|
||||||
const accelerations = Object.values(mempool.getAccelerations());
|
if (config.DATABASE.ENABLED) {
|
||||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
const accelerations = Object.values(mempool.getAccelerations());
|
||||||
|
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||||
|
}
|
||||||
|
|
||||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
@ -1095,7 +1099,9 @@ class WebsocketHandler {
|
|||||||
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
|
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
|
||||||
const firstSeen = getRecentFirstSeen(block.id);
|
const firstSeen = getRecentFirstSeen(block.id);
|
||||||
if (firstSeen) {
|
if (firstSeen) {
|
||||||
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
|
if (config.DATABASE.ENABLED) {
|
||||||
|
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
|
||||||
|
}
|
||||||
block.extras.firstSeen = firstSeen;
|
block.extras.firstSeen = firstSeen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1392,7 +1398,9 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await statistics.runStatistics();
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
|
||||||
|
await statistics.runStatistics();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleNewStratumJob(job: StratumJob): void {
|
public handleNewStratumJob(job: StratumJob): void {
|
||||||
|
@ -164,6 +164,7 @@ interface IConfig {
|
|||||||
},
|
},
|
||||||
WALLETS: {
|
WALLETS: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
|
AUTO: boolean;
|
||||||
WALLETS: string[];
|
WALLETS: string[];
|
||||||
},
|
},
|
||||||
STRATUM: {
|
STRATUM: {
|
||||||
@ -334,6 +335,7 @@ const defaults: IConfig = {
|
|||||||
},
|
},
|
||||||
'WALLETS': {
|
'WALLETS': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
|
'AUTO': false,
|
||||||
'WALLETS': [],
|
'WALLETS': [],
|
||||||
},
|
},
|
||||||
'STRATUM': {
|
'STRATUM': {
|
||||||
|
@ -131,6 +131,9 @@ class Server {
|
|||||||
this.app
|
this.app
|
||||||
.use((req: Request, res: Response, next: NextFunction) => {
|
.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
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();
|
next();
|
||||||
})
|
})
|
||||||
.use(express.urlencoded({ extended: true }))
|
.use(express.urlencoded({ extended: true }))
|
||||||
@ -153,7 +156,9 @@ class Server {
|
|||||||
|
|
||||||
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
|
||||||
await syncAssets.syncAssets$();
|
await syncAssets.syncAssets$();
|
||||||
await mempoolBlocks.updatePools$();
|
if (config.DATABASE.ENABLED) {
|
||||||
|
await mempoolBlocks.updatePools$();
|
||||||
|
}
|
||||||
if (config.MEMPOOL.ENABLED) {
|
if (config.MEMPOOL.ENABLED) {
|
||||||
if (config.MEMPOOL.CACHE_ENABLED) {
|
if (config.MEMPOOL.CACHE_ENABLED) {
|
||||||
await diskCache.$loadMempoolCache();
|
await diskCache.$loadMempoolCache();
|
||||||
|
@ -93,7 +93,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query);
|
const [rows]: any[] = await DB.query(query);
|
||||||
return rows.map(row => row.timestamp);
|
return rows.map(row => row.timestamp);
|
||||||
} catch (e) {
|
} 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;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,8 @@ class PoolsUpdater {
|
|||||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
|
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastRun = now - 600; // Try again in 10 minutes
|
// 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);
|
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
FROM node:20.15.0-buster-slim AS builder
|
FROM rust:1.84-bookworm AS builder
|
||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
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 . .
|
COPY . .
|
||||||
|
|
||||||
RUN apt-get update
|
ENV PATH="/usr/local/cargo/bin:$PATH"
|
||||||
RUN apt-get install -y build-essential python3 pkg-config curl ca-certificates
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
COPY --from=backend . .
|
COPY --from=backend . .
|
||||||
COPY --from=rustgbt . ../rust/
|
COPY --from=rustgbt . ../rust/
|
||||||
@ -24,7 +24,14 @@ RUN npm install --omit=dev --omit=optional
|
|||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN npm run package
|
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
|
WORKDIR /backend
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
|
|||||||
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
||||||
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
||||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
__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_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_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_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:20.15.0-buster-slim AS builder
|
FROM node:22.14.0-bookworm-slim AS builder
|
||||||
|
|
||||||
ARG commitHash
|
ARG commitHash
|
||||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||||
|
@ -261,20 +261,14 @@
|
|||||||
"proxyConfig": "proxy.conf.mixed.js",
|
"proxyConfig": "proxy.conf.mixed.js",
|
||||||
"verbose": true
|
"verbose": true
|
||||||
},
|
},
|
||||||
"staging": {
|
|
||||||
"proxyConfig": "proxy.conf.js",
|
|
||||||
"disableHostCheck": true,
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"verbose": true
|
|
||||||
},
|
|
||||||
"local-prod": {
|
"local-prod": {
|
||||||
"proxyConfig": "proxy.conf.js",
|
"proxyConfig": "proxy.conf.js",
|
||||||
"disableHostCheck": true,
|
"disableHostCheck": true,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"verbose": false
|
"verbose": false
|
||||||
},
|
},
|
||||||
"local-staging": {
|
"parameterized": {
|
||||||
"proxyConfig": "proxy.conf.staging.js",
|
"proxyConfig": "proxy.conf.parameterized.js",
|
||||||
"disableHostCheck": true,
|
"disableHostCheck": true,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"verbose": false
|
"verbose": false
|
||||||
@ -371,4 +365,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,10 +16,10 @@
|
|||||||
"mobileOrder": 4
|
"mobileOrder": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "balance",
|
"component": "walletBalance",
|
||||||
"mobileOrder": 1,
|
"mobileOrder": 1,
|
||||||
"props": {
|
"props": {
|
||||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
"wallet": "ONBTC"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -30,21 +30,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "address",
|
"component": "wallet",
|
||||||
"mobileOrder": 2,
|
"mobileOrder": 2,
|
||||||
"props": {
|
"props": {
|
||||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
|
"wallet": "ONBTC",
|
||||||
"period": "1m"
|
"period": "1m",
|
||||||
|
"label": "bitcoin.gob.sv"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "blocks"
|
"component": "blocks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": "addressTransactions",
|
"component": "walletTransactions",
|
||||||
"mobileOrder": 3,
|
"mobileOrder": 3,
|
||||||
"props": {
|
"props": {
|
||||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
"wallet": "ONBTC"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -57,11 +57,6 @@ describe('Liquid', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the tv page - desktop', () => {
|
|
||||||
cy.visit(`${basePath}/tv`);
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs page - mobile', () => {
|
it('loads the graphs page - mobile', () => {
|
||||||
cy.visit(`${basePath}`)
|
cy.visit(`${basePath}`)
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
@ -57,11 +57,6 @@ describe('Liquid Testnet', () => {
|
|||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the tv page - desktop', () => {
|
|
||||||
cy.visit(`${basePath}/tv`);
|
|
||||||
cy.waitForSkeletonGone();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the graphs page - mobile', () => {
|
it('loads the graphs page - mobile', () => {
|
||||||
cy.visit(`${basePath}`)
|
cy.visit(`${basePath}`)
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { emitMempoolInfo, dropWebSocket } from '../../support/websocket';
|
import { emitMempoolInfo, dropWebSocket, receiveWebSocketMessageFromServer } from '../../support/websocket';
|
||||||
|
|
||||||
const baseModule = Cypress.env('BASE_MODULE');
|
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').its('length').should('equal', 2);
|
||||||
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`);
|
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', () => {
|
describe('blocks navigation', () => {
|
||||||
@ -397,6 +460,7 @@ describe('Mainnet', () => {
|
|||||||
cy.get('#dropdownFees').should('be.visible');
|
cy.get('#dropdownFees').should('be.visible');
|
||||||
cy.get('.btn-group').should('be.visible');
|
cy.get('.btn-group').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('check buttons - tablet', () => {
|
it('check buttons - tablet', () => {
|
||||||
cy.viewport('ipad-2');
|
cy.viewport('ipad-2');
|
||||||
cy.visit('/graphs');
|
cy.visit('/graphs');
|
||||||
@ -405,6 +469,7 @@ describe('Mainnet', () => {
|
|||||||
cy.get('#dropdownFees').should('be.visible');
|
cy.get('#dropdownFees').should('be.visible');
|
||||||
cy.get('.btn-group').should('be.visible');
|
cy.get('.btn-group').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('check buttons - desktop', () => {
|
it('check buttons - desktop', () => {
|
||||||
cy.viewport('macbook-16');
|
cy.viewport('macbook-16');
|
||||||
cy.visit('/graphs');
|
cy.visit('/graphs');
|
||||||
@ -415,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', () => {
|
it('loads the api screen', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
@ -516,7 +561,44 @@ describe('Mainnet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('RBF transactions', () => {
|
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', {
|
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
|
||||||
fixture: 'mainnet_tx_cached.json'
|
fixture: 'mainnet_tx_cached.json'
|
||||||
}).as('cached_tx');
|
}).as('cached_tx');
|
||||||
@ -527,7 +609,7 @@ describe('Mainnet', () => {
|
|||||||
|
|
||||||
cy.viewport('iphone-xr');
|
cy.viewport('iphone-xr');
|
||||||
cy.mockMempoolSocket();
|
cy.mockMempoolSocket();
|
||||||
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
|
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f?mode=details');
|
||||||
|
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
|
||||||
@ -545,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('.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)', () => {
|
it('shows RBF transactions properly (desktop)', () => {
|
||||||
|
@ -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', () => {
|
it('loads the api screen', () => {
|
||||||
cy.visit('/signet');
|
cy.visit('/signet');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
@ -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', () => {
|
it('loads the api screen', () => {
|
||||||
cy.visit('/testnet4');
|
cy.visit('/testnet4');
|
||||||
cy.waitForSkeletonGone();
|
cy.waitForSkeletonGone();
|
||||||
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"gitCommit": "62f80296"
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
[]
|
260
frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json
Normal file
260
frontend/cypress/fixtures/details_rbf/api_mining_pools_1w.json
Normal file
@ -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
|
||||||
|
}
|
55
frontend/cypress/fixtures/details_rbf/tx01_api_cached.json
Normal file
55
frontend/cypress/fixtures/details_rbf/tx01_api_cached.json
Normal file
@ -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
|
||||||
|
}
|
34
frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json
Normal file
34
frontend/cypress/fixtures/details_rbf/tx01_api_rbf.json
Normal file
@ -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
|
||||||
|
}
|
579
frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json
Normal file
579
frontend/cypress/fixtures/details_rbf/tx01_ws_blocks_01.json
Normal file
@ -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<Eá\u0014\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010X¿\u0001B§¦Á`Ñ\r\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
|
||||||
|
"header": "00405521b248037921d29416dc0582180ee6173e792544e2b20f02000000000000000000742472183b6666a11dc7334d7711d3570037656d3aa1149ee9ebb1bffdd593c73d3eec676c79021734a210ae",
|
||||||
|
"utxoSetSize": null,
|
||||||
|
"totalInputAmt": null,
|
||||||
|
"pool": {
|
||||||
|
"id": 73,
|
||||||
|
"name": "ViaBTC",
|
||||||
|
"slug": "viabtc",
|
||||||
|
"minerNames": null
|
||||||
|
},
|
||||||
|
"matchRate": 100,
|
||||||
|
"expectedFees": 7342711,
|
||||||
|
"expectedWeight": 3991920,
|
||||||
|
"similarity": 0.9978345275528634
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c",
|
||||||
|
"height": 890441,
|
||||||
|
"version": 540188672,
|
||||||
|
"timestamp": 1743535803,
|
||||||
|
"bits": 386038124,
|
||||||
|
"nonce": 3418993591,
|
||||||
|
"difficulty": 113757508810854,
|
||||||
|
"merkle_root": "25457e2f9e9b55cacde6166acc58ebc2367007a7af5d9b39f46dc1ce060fd63e",
|
||||||
|
"tx_count": 2813,
|
||||||
|
"size": 1671284,
|
||||||
|
"weight": 3993398,
|
||||||
|
"previousblockhash": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4",
|
||||||
|
"mediantime": 1743532471,
|
||||||
|
"stale": false,
|
||||||
|
"extras": {
|
||||||
|
"reward": 315271317,
|
||||||
|
"coinbaseRaw": "0349960d04bb3eec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f01530a4561e7020000000000",
|
||||||
|
"orphans": [],
|
||||||
|
"medianFee": 2.0116279069767438,
|
||||||
|
"feeRange": [
|
||||||
|
1.0567375886524824,
|
||||||
|
1.1917098445595855,
|
||||||
|
1.9007678676904902,
|
||||||
|
2.1298076923076925,
|
||||||
|
2.825034262220192,
|
||||||
|
3.175202156334232,
|
||||||
|
121
|
||||||
|
],
|
||||||
|
"totalFees": 2771317,
|
||||||
|
"avgFee": 985,
|
||||||
|
"avgFeeRate": 2,
|
||||||
|
"utxoSetChange": 37,
|
||||||
|
"avgTxSize": 593.97,
|
||||||
|
"totalInputs": 8679,
|
||||||
|
"totalOutputs": 8716,
|
||||||
|
"totalOutputAmt": 299780221694,
|
||||||
|
"segwitTotalTxs": 2379,
|
||||||
|
"segwitTotalSize": 1535232,
|
||||||
|
"segwitTotalWeight": 3449298,
|
||||||
|
"feePercentiles": null,
|
||||||
|
"virtualSize": 998349.5,
|
||||||
|
"coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
|
||||||
|
"coinbaseAddresses": [
|
||||||
|
"bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
|
||||||
|
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"
|
||||||
|
],
|
||||||
|
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3",
|
||||||
|
"coinbaseSignatureAscii": "\u0003I\r\u0004»>ì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\u0016Vß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ßwap§·³¡_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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1235
frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json
Normal file
1235
frontend/cypress/fixtures/details_rbf/tx01_ws_stratum_jobs.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"txReplaced": {
|
||||||
|
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
|
||||||
|
}
|
||||||
|
}
|
9
frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json
Normal file
9
frontend/cypress/fixtures/details_rbf/tx02_api_cpfp.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ancestors": [],
|
||||||
|
"bestDescendant": null,
|
||||||
|
"descendants": [],
|
||||||
|
"effectiveFeePerVsize": 4.285714285714286,
|
||||||
|
"sigops": 4,
|
||||||
|
"fee": 960,
|
||||||
|
"adjustedVsize": 224
|
||||||
|
}
|
36
frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json
Normal file
36
frontend/cypress/fixtures/details_rbf/tx02_api_rbf.json
Normal file
@ -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"
|
||||||
|
]
|
||||||
|
}
|
38
frontend/cypress/fixtures/details_rbf/tx02_api_tx.json
Normal file
38
frontend/cypress/fixtures/details_rbf/tx02_api_tx.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
1743541726
|
||||||
|
]
|
116
frontend/cypress/fixtures/details_rbf/tx02_ws_block.json
Normal file
116
frontend/cypress/fixtures/details_rbf/tx02_ws_block.json
Normal file
@ -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Ùïý\u000b9È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"
|
||||||
|
}
|
@ -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Ùïý\u000b9È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"
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"txPosition": {
|
||||||
|
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
|
||||||
|
"position": {
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 110880
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
frontend/cypress/fixtures/rbf_page/rbf_01.json
Normal file
37
frontend/cypress/fixtures/rbf_page/rbf_01.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
68
frontend/cypress/fixtures/rbf_page/rbf_02.json
Normal file
68
frontend/cypress/fixtures/rbf_page/rbf_02.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -44,6 +44,7 @@
|
|||||||
|
|
||||||
import { PageIdleDetector } from './PageIdleDetector';
|
import { PageIdleDetector } from './PageIdleDetector';
|
||||||
import { mockWebSocket } from './websocket';
|
import { mockWebSocket } from './websocket';
|
||||||
|
import { mockWebSocketV2 } from './websocket';
|
||||||
|
|
||||||
/* global Cypress */
|
/* global Cypress */
|
||||||
const codes = {
|
const codes = {
|
||||||
@ -72,6 +73,10 @@ Cypress.Commands.add('mockMempoolSocket', () => {
|
|||||||
mockWebSocket();
|
mockWebSocket();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('mockMempoolSocketV2', () => {
|
||||||
|
mockWebSocketV2();
|
||||||
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
|
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
|
||||||
cy.get('.dropdown-toggle').click().then(() => {
|
cy.get('.dropdown-toggle').click().then(() => {
|
||||||
cy.get(`a.${network}`).click().then(() => {
|
cy.get(`a.${network}`).click().then(() => {
|
||||||
|
1
frontend/cypress/support/index.d.ts
vendored
1
frontend/cypress/support/index.d.ts
vendored
@ -5,6 +5,7 @@ declare namespace Cypress {
|
|||||||
waitForSkeletonGone(): Chainable<any>
|
waitForSkeletonGone(): Chainable<any>
|
||||||
waitForPageIdle(): Chainable<any>
|
waitForPageIdle(): Chainable<any>
|
||||||
mockMempoolSocket(): Chainable<any>
|
mockMempoolSocket(): Chainable<any>
|
||||||
|
mockMempoolSocketV2(): Chainable<any>
|
||||||
changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,6 +27,37 @@ const createMock = (url: string) => {
|
|||||||
return mocks[url];
|
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 = () => {
|
export const mockWebSocket = () => {
|
||||||
cy.on('window:before:load', (win) => {
|
cy.on('window:before:load', (win) => {
|
||||||
const winWebSocket = win.WebSocket;
|
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 = ({
|
export const emitMempoolInfo = ({
|
||||||
params
|
params
|
||||||
}: { params?: any } = {}) => {
|
}: { params?: any } = {}) => {
|
||||||
@ -82,16 +134,22 @@ export const emitMempoolInfo = ({
|
|||||||
switch (params.command) {
|
switch (params.command) {
|
||||||
case "init": {
|
case "init": {
|
||||||
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
|
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));
|
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));
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "rbfTransaction": {
|
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));
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "3.1.0-dev",
|
"version": "3.2.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "3.1.0-dev",
|
"version": "3.2.0",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "^17.3.1",
|
"@angular-devkit/build-angular": "^17.3.1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "3.1.0-dev",
|
"version": "3.2.0",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@ -25,14 +25,12 @@
|
|||||||
"i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf",
|
"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 --parallel --minimum-perc 1 --force",
|
||||||
"serve": "npm run generate-config && npm run ng -- serve -c local",
|
"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-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": "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: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-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",
|
"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",
|
"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/'",
|
"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: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: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: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: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: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: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": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "^17.3.1",
|
"@angular-devkit/build-angular": "^17.3.1",
|
||||||
|
36
frontend/proxy.conf.parameterized.js
Normal file
36
frontend/proxy.conf.parameterized.js
Normal file
@ -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;
|
@ -1,12 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
let PROXY_CONFIG = require('./proxy.conf');
|
|
||||||
|
|
||||||
PROXY_CONFIG.forEach(entry => {
|
|
||||||
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
|
|
||||||
console.log(`e2e tests running against ${hostname}`);
|
|
||||||
entry.target = entry.target.replace("mempool.space", hostname);
|
|
||||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = PROXY_CONFIG;
|
|
@ -11,6 +11,7 @@
|
|||||||
<div class="about-text">
|
<div class="about-text">
|
||||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ®</ng-template></h5>
|
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ®</ng-template></h5>
|
||||||
<p i18n>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.</p>
|
<p i18n>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.</p>
|
||||||
|
<h5>Be your own explorer™</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
|
<video #promoVideo (click)="unmutePromoVideo()" (touchstart)="unmutePromoVideo()" src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
|
||||||
@ -145,6 +146,15 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Bull Bitcoin</span>
|
<span>Bull Bitcoin</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://fortris.com/" target="_blank" title="Fortris">
|
||||||
|
<svg id="fortris-logo" viewBox="0 0 140.08 129.13" version="1.1" width="74px" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs><style>.fortris-cls-1{fill:#29384a;}.fortris-cls-1,.fortris-cls-2{stroke-width:0px;}.fortris-cls-2{fill:#ff6e72;}</style></defs>
|
||||||
|
<rect class="fortris-cls-2" x="0" y="0" width="140.08" height="30.59" rx="15.29" ry="15.29" />
|
||||||
|
<rect class="fortris-cls-2" x="0" y="98.540001" width="69.779999" height="30.59" rx="15.29" ry="15.29" />
|
||||||
|
<rect class="fortris-cls-2" x="0" y="49.27" width="109.5" height="30.59" rx="15.29" ry="15.29" />
|
||||||
|
</svg>
|
||||||
|
<span>Fortris</span>
|
||||||
|
</a>
|
||||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||||
@ -449,7 +459,7 @@
|
|||||||
Trademark Notice<br>
|
Trademark Notice<br>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, 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, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Liquidity™, 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, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||||
|
@ -264,6 +264,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 800px;
|
max-width: 850px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
<!-- MEMPOOL BASE FEE -->
|
<!-- MEMPOOL BASE FEE -->
|
||||||
<tr>
|
<tr>
|
||||||
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
|
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator® fees</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee">
|
<tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee">
|
||||||
<td class="info">
|
<td class="info">
|
||||||
@ -567,14 +567,29 @@
|
|||||||
} @else if (step === 'success') {
|
} @else if (step === 'success') {
|
||||||
<div class="row mb-1 text-center">
|
<div class="row mb-1 text-center">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content><span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span></h1>
|
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content>
|
||||||
|
@if (accelerationResponse) {
|
||||||
|
<span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span>
|
||||||
|
} @else {
|
||||||
|
<span class="default-slot" i18n="accelerator.success-message-third-party">Transaction is already being accelerated!</span>
|
||||||
|
}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row text-center mt-1">
|
<div class="row text-center mt-1">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="d-flex flex-row justify-content-center align-items-center">
|
<div class="d-flex flex-row justify-content-center align-items-center">
|
||||||
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
|
@if (accelerationResponse) {
|
||||||
|
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
|
||||||
|
} @else {
|
||||||
|
<span i18n="accelerator.confirmed-acceleration-with-miners-third-party">Transaction has already been accepted for acceleration by our mining pool partners.</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (accelerationResponse?.receiptUrl) {
|
||||||
|
<div class="d-flex flex-row justify-content-center align-items-center">
|
||||||
|
<span i18n="accelerator.receipt-label"><a [href]="accelerationResponse.receiptUrl" target="_blank">Click here to get a receipt.</a></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -87,6 +87,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
math = Math;
|
math = Math;
|
||||||
isMobile: boolean = window.innerWidth <= 767.98;
|
isMobile: boolean = window.innerWidth <= 767.98;
|
||||||
isProdDomain = false;
|
isProdDomain = false;
|
||||||
|
accelerationResponse: { receiptUrl: string | null } | undefined;
|
||||||
|
|
||||||
private _step: CheckoutStep = 'summary';
|
private _step: CheckoutStep = 'summary';
|
||||||
simpleMode: boolean = true;
|
simpleMode: boolean = true;
|
||||||
@ -194,16 +195,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
||||||
}
|
}
|
||||||
if (changes.accelerating && this.accelerating) {
|
if (changes.accelerating && this.accelerating) {
|
||||||
if (this.step === 'processing' || this.step === 'paid') {
|
this.moveToStep('success', true);
|
||||||
this.moveToStep('success', true);
|
|
||||||
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
|
||||||
this.closeModal();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToStep(step: CheckoutStep, force: boolean = false): void {
|
moveToStep(step: CheckoutStep, force: boolean = false): void {
|
||||||
if (this.isCheckoutLocked > 0 && !force) {
|
if (this.isCheckoutLocked > 0 && !force || this.step === 'success') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
@ -525,7 +522,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (tokenResult?.status === 'OK') {
|
if (tokenResult?.status === 'OK') {
|
||||||
const card = tokenResult.details?.card;
|
const card = tokenResult.details?.card;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retrieve payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
@ -541,7 +538,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
costUSD
|
costUSD
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: (response) => {
|
||||||
|
this.accelerationResponse = response;
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
@ -643,7 +641,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (tokenResult?.status === 'OK') {
|
if (tokenResult?.status === 'OK') {
|
||||||
const card = tokenResult.details?.card;
|
const card = tokenResult.details?.card;
|
||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retrieve payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
@ -668,7 +666,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
costUSD,
|
costUSD,
|
||||||
verificationToken.userChallenged
|
verificationToken.userChallenged
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: (response) => {
|
||||||
|
this.accelerationResponse = response;
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
@ -777,7 +776,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
costUSD,
|
costUSD,
|
||||||
verificationToken.userChallenged
|
verificationToken.userChallenged
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: (response) => {
|
||||||
|
this.accelerationResponse = response;
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
@ -870,7 +870,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
tokenResult.details.cashAppPay.referenceId,
|
tokenResult.details.cashAppPay.referenceId,
|
||||||
costUSD
|
costUSD
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: () => {
|
next: (response) => {
|
||||||
|
this.accelerationResponse = response;
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
@ -936,7 +937,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.loadingBtcpayInvoice = true;
|
this.loadingBtcpayInvoice = true;
|
||||||
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
||||||
switchMap(response => {
|
switchMap(response => {
|
||||||
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
return this.servicesApiService.retrieveInvoice$(response.btcpayInvoiceId);
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -44,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() right: number | string = 10;
|
@Input() right: number | string = 10;
|
||||||
@Input() left: number | string = 70;
|
@Input() left: number | string = 70;
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
|
@Input() label: string = '';
|
||||||
@Input() defaultFiat: boolean = false;
|
@Input() defaultFiat: boolean = false;
|
||||||
@Input() showLegend: boolean = true;
|
@Input() showLegend: boolean = true;
|
||||||
@Input() showYAxis: boolean = true;
|
@Input() showYAxis: boolean = true;
|
||||||
@ -55,6 +56,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
hoverData: any[] = [];
|
hoverData: any[] = [];
|
||||||
conversions: any;
|
conversions: any;
|
||||||
allowZoom: boolean = false;
|
allowZoom: boolean = false;
|
||||||
|
labelGraphic: any;
|
||||||
|
|
||||||
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
|
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
|
||||||
|
|
||||||
@ -85,6 +87,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
this.labelGraphic = this.label ? {
|
||||||
|
type: 'text',
|
||||||
|
right: '36px',
|
||||||
|
bottom: '36px',
|
||||||
|
z: 100,
|
||||||
|
silent: true,
|
||||||
|
style: {
|
||||||
|
fill: '#fff',
|
||||||
|
text: this.label,
|
||||||
|
font: '24px sans-serif'
|
||||||
|
}
|
||||||
|
} : undefined;
|
||||||
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -205,6 +219,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
right: this.adjustedRight,
|
right: this.adjustedRight,
|
||||||
left: this.adjustedLeft,
|
left: this.adjustedLeft,
|
||||||
},
|
},
|
||||||
|
graphic: this.labelGraphic ? [{
|
||||||
|
...this.labelGraphic,
|
||||||
|
right: this.adjustedRight + 22 + 'px',
|
||||||
|
}] : undefined,
|
||||||
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
|
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
@ -443,6 +461,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
right: this.adjustedRight,
|
right: this.adjustedRight,
|
||||||
left: this.adjustedLeft,
|
left: this.adjustedLeft,
|
||||||
},
|
},
|
||||||
|
graphic: this.labelGraphic ? [{
|
||||||
|
...this.labelGraphic,
|
||||||
|
right: this.adjustedRight + 22 + 'px',
|
||||||
|
}] : undefined,
|
||||||
legend: {
|
legend: {
|
||||||
selected: this.selected,
|
selected: this.selected,
|
||||||
},
|
},
|
||||||
|
@ -36,6 +36,8 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
sent = 0;
|
sent = 0;
|
||||||
totalUnspent = 0;
|
totalUnspent = 0;
|
||||||
|
|
||||||
|
ogSession: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
@ -58,7 +60,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
this.rawAddress = params.get('id') || '';
|
this.rawAddress = params.get('id') || '';
|
||||||
this.openGraphService.waitFor('address-data-' + this.rawAddress);
|
this.ogSession = this.openGraphService.waitFor('address-data-' + this.rawAddress);
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.isLoadingAddress = true;
|
this.isLoadingAddress = true;
|
||||||
this.loadedConfirmedTxCount = 0;
|
this.loadedConfirmedTxCount = 0;
|
||||||
@ -79,7 +81,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.error = err;
|
this.error = err;
|
||||||
console.log(err);
|
console.log(err);
|
||||||
this.openGraphService.fail('address-data-' + this.rawAddress);
|
this.openGraphService.fail({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
|
||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -97,7 +99,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.address = address;
|
this.address = address;
|
||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.openGraphService.waitOver('address-data-' + this.rawAddress);
|
this.openGraphService.waitOver({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe(() => {},
|
.subscribe(() => {},
|
||||||
@ -105,7 +107,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.openGraphService.fail('address-data-' + this.rawAddress);
|
this.openGraphService.fail({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && block?.extras?.pool">
|
||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||||
|
@ -35,6 +35,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
overviewSubscription: Subscription;
|
overviewSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
|
|
||||||
|
ogSession: number;
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -53,8 +55,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
const block$ = this.route.paramMap.pipe(
|
const block$ = this.route.paramMap.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
this.rawId = params.get('id') || '';
|
this.rawId = params.get('id') || '';
|
||||||
this.openGraphService.waitFor('block-viz-' + this.rawId);
|
this.ogSession = this.openGraphService.waitFor('block-viz-' + this.rawId);
|
||||||
this.openGraphService.waitFor('block-data-' + this.rawId);
|
this.ogSession = this.openGraphService.waitFor('block-data-' + this.rawId);
|
||||||
|
|
||||||
const blockHash: string = params.get('id') || '';
|
const blockHash: string = params.get('id') || '';
|
||||||
this.block = undefined;
|
this.block = undefined;
|
||||||
@ -86,8 +88,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.error = err;
|
this.error = err;
|
||||||
this.seoService.logSoft404();
|
this.seoService.logSoft404();
|
||||||
this.openGraphService.fail('block-data-' + this.rawId);
|
this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
|
||||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||||
return of(null);
|
return of(null);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -114,7 +116,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingOverview = true;
|
this.isLoadingOverview = true;
|
||||||
this.overviewError = null;
|
this.overviewError = null;
|
||||||
|
|
||||||
this.openGraphService.waitOver('block-data-' + this.rawId);
|
this.openGraphService.waitOver({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
|
||||||
}),
|
}),
|
||||||
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
|
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
@ -129,7 +131,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(
|
.pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.overviewError = err;
|
this.overviewError = err;
|
||||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||||
return of([]);
|
return of([]);
|
||||||
}),
|
}),
|
||||||
switchMap((transactions) => {
|
switchMap((transactions) => {
|
||||||
@ -138,7 +140,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
),
|
),
|
||||||
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||||
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
||||||
.pipe(catchError(() => {
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
return of([]);
|
return of([]);
|
||||||
}))
|
}))
|
||||||
: of([])
|
: of([])
|
||||||
@ -169,8 +172,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
this.seoService.logSoft404();
|
this.seoService.logSoft404();
|
||||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||||
this.openGraphService.fail('block-data-' + this.rawId);
|
this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
|
||||||
if (this.blockGraph) {
|
if (this.blockGraph) {
|
||||||
this.blockGraph.destroy();
|
this.blockGraph.destroy();
|
||||||
}
|
}
|
||||||
@ -196,6 +199,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onGraphReady(): void {
|
onGraphReady(): void {
|
||||||
this.openGraphService.waitOver('block-viz-' + this.rawId);
|
this.openGraphService.waitOver({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight"></app-address-graph>
|
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight" [label]="widget.props.label"></app-address-graph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -272,7 +272,7 @@
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight"></app-address-graph>
|
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight" [label]="widget.props.label"></app-address-graph>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,11 +70,6 @@
|
|||||||
<li class="nav-item" routerLinkActive="active" id="btn-graphs">
|
<li class="nav-item" routerLinkActive="active" id="btn-graphs">
|
||||||
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
|
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
|
||||||
</li>
|
</li>
|
||||||
<!--
|
|
||||||
<li class="nav-item d-none d-lg-block" routerLinkActive="active">
|
|
||||||
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon></a>
|
|
||||||
</li>
|
|
||||||
-->
|
|
||||||
<li class="nav-item" routerLinkActive="active" id="btn-assets">
|
<li class="nav-item" routerLinkActive="active" id="btn-assets">
|
||||||
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
|
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -30,6 +30,8 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
|
|
||||||
slug: string = undefined;
|
slug: string = undefined;
|
||||||
|
|
||||||
|
ogSession: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
@ -47,22 +49,22 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.imageLoaded = false;
|
this.imageLoaded = false;
|
||||||
this.slug = slug;
|
this.slug = slug;
|
||||||
this.openGraphService.waitFor('pool-hash-' + this.slug);
|
this.ogSession = this.openGraphService.waitFor('pool-hash-' + this.slug);
|
||||||
this.openGraphService.waitFor('pool-stats-' + this.slug);
|
this.ogSession = this.openGraphService.waitFor('pool-stats-' + this.slug);
|
||||||
this.openGraphService.waitFor('pool-chart-' + this.slug);
|
this.ogSession = this.openGraphService.waitFor('pool-chart-' + this.slug);
|
||||||
this.openGraphService.waitFor('pool-img-' + this.slug);
|
this.ogSession = this.openGraphService.waitFor('pool-img-' + this.slug);
|
||||||
return this.apiService.getPoolHashrate$(this.slug)
|
return this.apiService.getPoolHashrate$(this.slug)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((data) => {
|
switchMap((data) => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate]));
|
this.prepareChartOptions(data.map(val => [val.timestamp * 1000, val.avgHashrate]));
|
||||||
this.openGraphService.waitOver('pool-hash-' + this.slug);
|
this.openGraphService.waitOver({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession });
|
||||||
return [slug];
|
return [slug];
|
||||||
}),
|
}),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.seoService.logSoft404();
|
this.seoService.logSoft404();
|
||||||
this.openGraphService.fail('pool-hash-' + this.slug);
|
this.openGraphService.fail({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession });
|
||||||
return of([slug]);
|
return of([slug]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -72,7 +74,7 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.seoService.logSoft404();
|
this.seoService.logSoft404();
|
||||||
this.openGraphService.fail('pool-stats-' + this.slug);
|
this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
|
||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -90,11 +92,11 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||||
|
|
||||||
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
this.openGraphService.waitOver({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
|
||||||
|
|
||||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
|
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
|
||||||
if (logoSrc === this.lastImgSrc) {
|
if (logoSrc === this.lastImgSrc) {
|
||||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
|
||||||
}
|
}
|
||||||
this.lastImgSrc = logoSrc;
|
this.lastImgSrc = logoSrc;
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
@ -103,7 +105,7 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.openGraphService.fail('pool-stats-' + this.slug);
|
this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
|
||||||
return of(null);
|
return of(null);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -170,16 +172,16 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChartReady(): void {
|
onChartReady(): void {
|
||||||
this.openGraphService.waitOver('pool-chart-' + this.slug);
|
this.openGraphService.waitOver({ event: 'pool-chart-' + this.slug, sessionId: this.ogSession });
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageLoad(): void {
|
onImageLoad(): void {
|
||||||
this.imageLoaded = true;
|
this.imageLoaded = true;
|
||||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
|
||||||
}
|
}
|
||||||
|
|
||||||
onImageFail(): void {
|
onImageFail(): void {
|
||||||
this.imageLoaded = false;
|
this.imageLoaded = false;
|
||||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,74 +84,79 @@ div.scrollable {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coinbase {
|
||||||
|
width: 20%;
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.coinbase {
|
.health {
|
||||||
width: 20%;
|
@media (max-width: 1150px) {
|
||||||
@media (max-width: 875px) {
|
display: none;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.height {
|
.height {
|
||||||
width: 10%;
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
@media (max-width: 875px) {
|
||||||
|
padding-left: 50px;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 625px) {
|
||||||
.timestamp {
|
display: none;
|
||||||
@media (max-width: 875px) {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
@media (max-width: 685px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mined {
|
.mined {
|
||||||
width: 13%;
|
width: 13%;
|
||||||
@media (max-width: 1100px) {
|
}
|
||||||
display: none;
|
|
||||||
}
|
.txs {
|
||||||
|
@media (max-width: 938px) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
padding-right: 40px;
|
||||||
.txs {
|
@media (max-width: 1100px) {
|
||||||
padding-right: 40px;
|
padding-right: 10px;
|
||||||
@media (max-width: 1100px) {
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
@media (max-width: 567px) {
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@media (max-width: 875px) {
|
||||||
.size {
|
padding-right: 20px;
|
||||||
width: 12%;
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
@media (max-width: 875px) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
@media (max-width: 650px) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@media (max-width: 567px) {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.scriptmessage {
|
.size {
|
||||||
overflow: hidden;
|
min-width: 80px;
|
||||||
display: inline-block;
|
width: 12%;
|
||||||
text-overflow: ellipsis;
|
@media (max-width: 1000px) {
|
||||||
vertical-align: middle;
|
width: 15%;
|
||||||
width: auto;
|
}
|
||||||
text-align: left;
|
}
|
||||||
|
|
||||||
|
.scriptmessage {
|
||||||
|
max-width: 340px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward {
|
||||||
|
@media (max-width: 1035px) {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,14 +45,14 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<h4>USING MEMPOOL ACCELERATOR™</h4>
|
<h4>USING MEMPOOL ACCELERATOR®</h4>
|
||||||
|
|
||||||
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator™ your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
|
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator® your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
|
||||||
|
|
||||||
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator™ the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
|
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator® the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<ng-container *ngIf="officialMempoolSpace">
|
<ng-container *ngIf="officialMempoolSpace">
|
||||||
|
|
||||||
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
<li>If you sign up for a subscription to Mempool Enterprise™ we also collect your company name which is not shared with any third-party.</li>
|
<li>If you sign up for a subscription to Mempool Enterprise™ we also collect your company name which is not shared with any third-party.</li>
|
||||||
|
|
||||||
<li>If you sign up for an account on mempool.space and use Mempool Accelerator™ Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
|
<li>If you sign up for an account on mempool.space and use Mempool Accelerator® Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
<p>We aim to retain your data only as long as necessary:</p>
|
<p>We aim to retain your data only as long as necessary:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator™ Pro account credit</li>
|
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator® Pro account credit</li>
|
||||||
<li>If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
|
<li>If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -194,14 +194,16 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||||||
applyScrollLeft(): void {
|
applyScrollLeft(): void {
|
||||||
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
|
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
|
||||||
let lastScrollLeft = null;
|
let lastScrollLeft = null;
|
||||||
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
|
if (!this.timeLtr) {
|
||||||
lastScrollLeft = this.scrollLeft;
|
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
|
||||||
this.scrollLeft += this.pageWidth;
|
lastScrollLeft = this.scrollLeft;
|
||||||
}
|
this.scrollLeft += this.pageWidth;
|
||||||
lastScrollLeft = null;
|
}
|
||||||
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
|
lastScrollLeft = null;
|
||||||
lastScrollLeft = this.scrollLeft;
|
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
|
||||||
this.scrollLeft -= this.pageWidth;
|
lastScrollLeft = this.scrollLeft;
|
||||||
|
this.scrollLeft -= this.pageWidth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
|
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,6 @@
|
|||||||
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
|
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
|
||||||
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" i18n-title="footer.clock-mempool" title="Clock (Mempool)"></fa-icon>
|
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" i18n-title="footer.clock-mempool" title="Clock (Mempool)"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
|
||||||
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-toggle-rows" name="radioBasic">
|
<div class="btn-toggle-rows" name="radioBasic">
|
||||||
<div class="btn-group btn-group-toggle">
|
<div class="btn-group btn-group-toggle">
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,25 +0,0 @@
|
|||||||
<div id="tv-wrapper">
|
|
||||||
<div class="tv-container">
|
|
||||||
<div class="chart-holder">
|
|
||||||
<app-mempool-graph
|
|
||||||
[template]="'advanced'"
|
|
||||||
[height]="600"
|
|
||||||
[left]="60"
|
|
||||||
[right]="10"
|
|
||||||
[data]="statsSubscription$ | async"
|
|
||||||
[showZoom]="false"
|
|
||||||
></app-mempool-graph>
|
|
||||||
</div>
|
|
||||||
<div class="blockchain-wrapper" [dir]="timeLtr ? 'rtl' : 'ltr'" [class.time-ltr]="timeLtr">
|
|
||||||
<div class="position-container">
|
|
||||||
<span>
|
|
||||||
<div class="blocks-wrapper">
|
|
||||||
<app-mempool-blocks></app-mempool-blocks>
|
|
||||||
<app-blockchain-blocks></app-blockchain-blocks>
|
|
||||||
</div>
|
|
||||||
<div id="divider"></div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,80 +0,0 @@
|
|||||||
|
|
||||||
.loading {
|
|
||||||
margin: auto;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100vh;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tv-wrapper {
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-holder {
|
|
||||||
position: relative;
|
|
||||||
height: 655px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 30px auto 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blockchain-wrapper {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 240px;
|
|
||||||
position: relative;
|
|
||||||
top: 30px;
|
|
||||||
|
|
||||||
.position-container {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 170px;
|
|
||||||
transform: translateX(50vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
#divider {
|
|
||||||
width: 2px;
|
|
||||||
height: 175px;
|
|
||||||
left: 0;
|
|
||||||
top: -40px;
|
|
||||||
position: absolute;
|
|
||||||
img {
|
|
||||||
position: absolute;
|
|
||||||
left: -100px;
|
|
||||||
top: -28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.time-ltr {
|
|
||||||
.blocks-wrapper {
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(.ltr-layout) {
|
|
||||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
|
||||||
.blockchain-wrapper .blocks-wrapper {
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(.rtl-layout) {
|
|
||||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
|
||||||
.blockchain-wrapper .blocks-wrapper {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tv-container {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0px;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
|||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
||||||
import { WebsocketService } from '@app/services/websocket.service';
|
|
||||||
import { OptimizedMempoolStats } from '@interfaces/node-api.interface';
|
|
||||||
import { StateService } from '@app/services/state.service';
|
|
||||||
import { ApiService } from '@app/services/api.service';
|
|
||||||
import { SeoService } from '@app/services/seo.service';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { map, scan, startWith, switchMap, tap } from 'rxjs/operators';
|
|
||||||
import { interval, merge, Observable, Subscription } from 'rxjs';
|
|
||||||
import { ChangeDetectionStrategy } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-television',
|
|
||||||
templateUrl: './television.component.html',
|
|
||||||
styleUrls: ['./television.component.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class TelevisionComponent implements OnInit, OnDestroy {
|
|
||||||
|
|
||||||
mempoolStats: OptimizedMempoolStats[] = [];
|
|
||||||
statsSubscription$: Observable<OptimizedMempoolStats[]>;
|
|
||||||
fragment: string;
|
|
||||||
timeLtrSubscription: Subscription;
|
|
||||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private websocketService: WebsocketService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private stateService: StateService,
|
|
||||||
private seoService: SeoService,
|
|
||||||
private route: ActivatedRoute
|
|
||||||
) { }
|
|
||||||
|
|
||||||
refreshStats(time: number, fn: Observable<OptimizedMempoolStats[]>) {
|
|
||||||
return interval(time).pipe(startWith(0), switchMap(() => fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
|
|
||||||
this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`);
|
|
||||||
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
|
|
||||||
|
|
||||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
|
||||||
this.timeLtr = !!ltr;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.statsSubscription$ = merge(
|
|
||||||
this.stateService.live2Chart$.pipe(map(stats => [stats])),
|
|
||||||
this.route.fragment
|
|
||||||
.pipe(
|
|
||||||
tap(fragment => { this.fragment = fragment ?? '2h'; }),
|
|
||||||
switchMap((fragment) => {
|
|
||||||
const minute = 60000; const hour = 3600000;
|
|
||||||
switch (fragment) {
|
|
||||||
case '24h': return this.apiService.list24HStatistics$();
|
|
||||||
case '1w': return this.refreshStats(5 * minute, this.apiService.list1WStatistics$());
|
|
||||||
case '1m': return this.refreshStats(30 * minute, this.apiService.list1MStatistics$());
|
|
||||||
case '3m': return this.refreshStats(2 * hour, this.apiService.list3MStatistics$());
|
|
||||||
case '6m': return this.refreshStats(3 * hour, this.apiService.list6MStatistics$());
|
|
||||||
case '1y': return this.refreshStats(8 * hour, this.apiService.list1YStatistics$());
|
|
||||||
case '2y': return this.refreshStats(8 * hour, this.apiService.list2YStatistics$());
|
|
||||||
case '3y': return this.refreshStats(12 * hour, this.apiService.list3YStatistics$());
|
|
||||||
default /* 2h */: return this.apiService.list2HStatistics$();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
scan((mempoolStats, newStats) => {
|
|
||||||
if (newStats.length > 1) {
|
|
||||||
mempoolStats = newStats;
|
|
||||||
} else if (['2h', '24h'].includes(this.fragment)) {
|
|
||||||
mempoolStats.unshift(newStats[0]);
|
|
||||||
const now = Date.now() / 1000;
|
|
||||||
const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) );
|
|
||||||
mempoolStats = mempoolStats.filter(p => p.added >= start);
|
|
||||||
}
|
|
||||||
return mempoolStats;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.timeLtrSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
@ -67,9 +67,9 @@
|
|||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<h4>MEMPOOL ACCELERATOR™</h4>
|
<h4>MEMPOOL ACCELERATOR®</h4>
|
||||||
|
|
||||||
<p><a href="https://mempool.space/accelerator">Mempool Accelerator™</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
|
<p><a href="https://mempool.space/accelerator">Mempool Accelerator®</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
||||||
<li>Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests. </li>
|
<li>Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests. </li>
|
||||||
@ -84,11 +84,11 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<li>All acceleration payments and Mempool Accelerator™ account credit top-ups are non-refundable. </li>
|
<li>All acceleration payments and Mempool Accelerator® account credit top-ups are non-refundable. </li>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<li>Mempool Accelerator™ account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
|
<li>Mempool Accelerator® account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div *ngIf="officialMempoolSpace">
|
<div *ngIf="officialMempoolSpace">
|
||||||
<h2>Trademark Policy and Guidelines</h2>
|
<h2>Trademark Policy and Guidelines</h2>
|
||||||
<h5>The Mempool Open Source Project ®</h5>
|
<h5>The Mempool Open Source Project ®</h5>
|
||||||
<h6>Updated: August 19, 2024</h6>
|
<h6>Updated: February 11, 2025</h6>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
@ -59,6 +59,7 @@
|
|||||||
<tr><td>Mempool Accelerator</td></tr>
|
<tr><td>Mempool Accelerator</td></tr>
|
||||||
<tr><td>Mempool Enterprise</td></tr>
|
<tr><td>Mempool Enterprise</td></tr>
|
||||||
<tr><td>Mempool Liquidity</td></tr>
|
<tr><td>Mempool Liquidity</td></tr>
|
||||||
|
<tr><td>Mempool</td></tr>
|
||||||
<tr><td>mempool.space</td></tr>
|
<tr><td>mempool.space</td></tr>
|
||||||
<tr><td>Be your own explorer</td></tr>
|
<tr><td>Be your own explorer</td></tr>
|
||||||
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
|
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
|
||||||
@ -340,7 +341,8 @@
|
|||||||
|
|
||||||
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
||||||
|
|
||||||
<p>"The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
|
<p>"The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®, Mempool Liquidity™, Mempool®, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
|
||||||
|
|
||||||
<li>What to Do When You See Abuse</li>
|
<li>What to Do When You See Abuse</li>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
<br>
|
||||||
|
<div class="title">
|
||||||
|
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
|
||||||
|
</div>
|
||||||
|
<div class="box cpfp-details">
|
||||||
|
<table class="table table-fixed table-borderless table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
|
||||||
|
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||||
|
<th *only-vsize class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
|
||||||
|
<th *only-weight class="d-none d-lg-table-cell" i18n="transaction.weight|Transaction Weight">Weight</th>
|
||||||
|
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||||
|
<th class="d-none d-lg-table-cell"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
|
||||||
|
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
|
||||||
|
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||||
|
<td>
|
||||||
|
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
|
||||||
|
</td>
|
||||||
|
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||||
|
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
|
||||||
|
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
|
||||||
|
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
|
||||||
|
<tr>
|
||||||
|
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||||
|
<td class="txids">
|
||||||
|
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
|
||||||
|
</td>
|
||||||
|
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
|
||||||
|
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight | wuBytes: 2"></td>
|
||||||
|
<td><app-fee-rate [fee]="cpfpInfo.bestDescendant.fee" [weight]="cpfpInfo.bestDescendant.weight"></app-fee-rate></td>
|
||||||
|
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
|
||||||
|
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
||||||
|
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
||||||
|
<td class="txids">
|
||||||
|
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
|
||||||
|
</td>
|
||||||
|
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||||
|
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
|
||||||
|
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
|
||||||
|
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
@ -0,0 +1,32 @@
|
|||||||
|
.title {
|
||||||
|
h2 {
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpfp-details {
|
||||||
|
.txids {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.txids {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-green {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-red {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||||
|
import { CpfpInfo } from '@interfaces/node-api.interface';
|
||||||
|
import { Transaction } from '@interfaces/electrs.interface';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cpfp-info',
|
||||||
|
templateUrl: './cpfp-info.component.html',
|
||||||
|
styleUrls: ['./cpfp-info.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class CpfpInfoComponent implements OnInit {
|
||||||
|
@Input() cpfpInfo: CpfpInfo;
|
||||||
|
@Input() tx: Transaction;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
roundToOneDecimal(cpfpTx: any): number {
|
||||||
|
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
||||||
|
}
|
||||||
|
}
|
@ -153,7 +153,7 @@
|
|||||||
|
|
||||||
<ng-template #etaRow>
|
<ng-template #etaRow>
|
||||||
@if (!isLoadingTx) {
|
@if (!isLoadingTx) {
|
||||||
@if (!replaced && !isCached) {
|
@if (!replaced && !isCached && !unbroadcasted) {
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||||
<td>
|
<td>
|
||||||
@ -170,7 +170,7 @@
|
|||||||
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
|
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
|
||||||
<div class="d-flex accelerate">
|
<div class="d-flex accelerate">
|
||||||
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||||
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator™ tooltip" ngbTooltip="This transaction cannot be accelerated">
|
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator® tooltip" ngbTooltip="This transaction cannot be accelerated">
|
||||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -184,7 +184,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
} @else {
|
} @else if (!unbroadcasted){
|
||||||
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -213,11 +213,11 @@
|
|||||||
@if (!isLoadingTx) {
|
@if (!isLoadingTx) {
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
<td class="text-wrap">{{ (tx.fee | number) ?? '-' }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
@if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
@if (isAcceleration && accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
||||||
}
|
}
|
||||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
|
<span class="fiat"><app-fiat *ngIf="tx.fee >= 0" [blockConversion]="tx.price" [value]="tx.fee + (isAcceleration ? ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0) : 0)"></app-fiat></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
} @else {
|
} @else {
|
||||||
@ -318,4 +318,4 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -38,6 +38,7 @@ export class TransactionDetailsComponent implements OnInit {
|
|||||||
@Input() replaced: boolean;
|
@Input() replaced: boolean;
|
||||||
@Input() isCached: boolean;
|
@Input() isCached: boolean;
|
||||||
@Input() ETA$: Observable<ETA>;
|
@Input() ETA$: Observable<ETA>;
|
||||||
|
@Input() unbroadcasted: boolean;
|
||||||
|
|
||||||
@Output() accelerateClicked = new EventEmitter<boolean>();
|
@Output() accelerateClicked = new EventEmitter<boolean>();
|
||||||
@Output() toggleCpfp$ = new EventEmitter<void>();
|
@Output() toggleCpfp$ = new EventEmitter<void>();
|
||||||
|
@ -43,6 +43,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
opReturns: Vout[];
|
opReturns: Vout[];
|
||||||
extraData: 'none' | 'coinbase' | 'opreturn';
|
extraData: 'none' | 'coinbase' | 'opreturn';
|
||||||
|
|
||||||
|
ogSession: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
@ -75,7 +77,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe((cpfpInfo) => {
|
.subscribe((cpfpInfo) => {
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
this.openGraphService.waitOver('cpfp-data-' + this.txId);
|
this.openGraphService.waitOver({ event: 'cpfp-data-' + this.txId, sessionId: this.ogSession });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscription = this.route.paramMap
|
this.subscription = this.route.paramMap
|
||||||
@ -83,8 +85,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
const urlMatch = (params.get('id') || '').split(':');
|
const urlMatch = (params.get('id') || '').split(':');
|
||||||
this.txId = urlMatch[0];
|
this.txId = urlMatch[0];
|
||||||
this.openGraphService.waitFor('tx-data-' + this.txId);
|
this.ogSession = this.openGraphService.waitFor('tx-data-' + this.txId);
|
||||||
this.openGraphService.waitFor('tx-time-' + this.txId);
|
this.ogSession = this.openGraphService.waitFor('tx-time-' + this.txId);
|
||||||
this.seoService.setTitle(
|
this.seoService.setTitle(
|
||||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||||
);
|
);
|
||||||
@ -138,7 +140,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((tx: Transaction) => {
|
.subscribe((tx: Transaction) => {
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
this.seoService.logSoft404();
|
this.seoService.logSoft404();
|
||||||
this.openGraphService.fail('tx-data-' + this.txId);
|
this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,10 +157,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (tx.status.confirmed) {
|
if (tx.status.confirmed) {
|
||||||
this.transactionTime = tx.status.block_time;
|
this.transactionTime = tx.status.block_time;
|
||||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
|
||||||
} else if (!tx.status.confirmed && tx.firstSeen) {
|
} else if (!tx.status.confirmed && tx.firstSeen) {
|
||||||
this.transactionTime = tx.firstSeen;
|
this.transactionTime = tx.firstSeen;
|
||||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
|
||||||
} else {
|
} else {
|
||||||
this.getTransactionTime();
|
this.getTransactionTime();
|
||||||
}
|
}
|
||||||
@ -184,11 +186,11 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openGraphService.waitOver('tx-data-' + this.txId);
|
this.openGraphService.waitOver({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
this.seoService.logSoft404();
|
this.seoService.logSoft404();
|
||||||
this.openGraphService.fail('tx-data-' + this.txId);
|
this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.isLoadingTx = false;
|
this.isLoadingTx = false;
|
||||||
}
|
}
|
||||||
@ -205,7 +207,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe((transactionTimes) => {
|
.subscribe((transactionTimes) => {
|
||||||
this.transactionTime = transactionTimes[0];
|
this.transactionTime = transactionTimes[0];
|
||||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,215 @@
|
|||||||
|
<div class="container-xl">
|
||||||
|
|
||||||
|
@if (!transaction) {
|
||||||
|
|
||||||
|
<h1 style="margin-top: 19px;" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
|
||||||
|
|
||||||
|
<form [formGroup]="pushTxForm" (submit)="decodeTransaction()" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea formControlName="txRaw" class="form-control" rows="5" i18n-placeholder="transaction.hex-and-psbt" placeholder="Transaction hex or base64 encoded PSBT"></textarea>
|
||||||
|
</div>
|
||||||
|
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.preview|Preview">Preview</button>
|
||||||
|
<input type="checkbox" [checked]="!offlineMode" id="offline-mode" (change)="onOfflineModeChange($event)">
|
||||||
|
<label class="label" for="offline-mode">
|
||||||
|
<span i18n="transaction.fetch-prevout-data">Fetch missing prevouts</span>
|
||||||
|
</label>
|
||||||
|
<p *ngIf="error" class="red-color d-inline">Error decoding transaction, reason: {{ error }}</p>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (transaction && !error && !isLoading) {
|
||||||
|
<div class="title-block">
|
||||||
|
<h1 i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</h1>
|
||||||
|
|
||||||
|
<span class="tx-link">
|
||||||
|
<span class="txid">
|
||||||
|
<app-truncate [text]="transaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, transaction.txid]" [disabled]="!successBroadcast">
|
||||||
|
<app-clipboard [text]="transaction.txid"></app-clipboard>
|
||||||
|
</app-truncate>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="container-buttons">
|
||||||
|
<button class="btn btn-sm" style="margin-left: 10px; padding: 0;" (click)="resetForm()">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="red-color d-inline">{{ errorBroadcast }}</p>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
<div class="alert alert-mempool" style="align-items: center;">
|
||||||
|
<span>
|
||||||
|
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
<ng-container *ngIf="!successBroadcast" i18n="transaction.local-tx|This transaction is stored locally in your browser.">
|
||||||
|
This transaction is stored locally in your browser. Broadcast it to add it to the mempool.
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="successBroadcast" i18n="transaction.redirecting|Redirecting to transaction page...">
|
||||||
|
Redirecting to transaction page...
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary btn-broadcast" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
|
||||||
|
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor btn-broadcast" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!hasPrevouts) {
|
||||||
|
<div class="alert alert-mempool">
|
||||||
|
@if (offlineMode) {
|
||||||
|
<span><strong>Missing prevouts are not loaded. Some fields like fee rate cannot be calculated.</strong></span>
|
||||||
|
} @else {
|
||||||
|
<span><strong>Error loading missing prevouts</strong>. {{ errorPrevouts ? 'Reason: ' + errorPrevouts : '' }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (errorCpfpInfo) {
|
||||||
|
<div class="alert alert-mempool">
|
||||||
|
<span><strong>Error loading CPFP data</strong>. Reason: {{ errorCpfpInfo }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<app-transaction-details
|
||||||
|
[unbroadcasted]="true"
|
||||||
|
[network]="stateService.network"
|
||||||
|
[tx]="transaction"
|
||||||
|
[isLoadingTx]="false"
|
||||||
|
[isMobile]="isMobile"
|
||||||
|
[isLoadingFirstSeen]="false"
|
||||||
|
[featuresEnabled]="true"
|
||||||
|
[filters]="filters"
|
||||||
|
[hasEffectiveFeeRate]="hasEffectiveFeeRate"
|
||||||
|
[cpfpInfo]="cpfpInfo"
|
||||||
|
[hasCpfp]="hasCpfp"
|
||||||
|
(toggleCpfp$)="this.showCpfpDetails = !this.showCpfpDetails"
|
||||||
|
></app-transaction-details>
|
||||||
|
|
||||||
|
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="transaction"></app-cpfp-info>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
|
||||||
|
<div class="title float-left">
|
||||||
|
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="graph-container" #graphContainer>
|
||||||
|
<tx-bowtie-graph
|
||||||
|
[tx]="transaction"
|
||||||
|
[cached]="true"
|
||||||
|
[width]="graphWidth"
|
||||||
|
[height]="graphHeight"
|
||||||
|
[lineLimit]="inOutLimit"
|
||||||
|
[maxStrands]="graphExpanded ? maxInOut : 24"
|
||||||
|
[network]="stateService.network"
|
||||||
|
[tooltip]="true"
|
||||||
|
[connectors]="true"
|
||||||
|
[inputIndex]="null" [outputIndex]="null"
|
||||||
|
>
|
||||||
|
</tx-bowtie-graph>
|
||||||
|
</div>
|
||||||
|
<div class="toggle-wrapper" *ngIf="maxInOut > 24">
|
||||||
|
<button class="btn btn-sm btn-primary graph-toggle" (click)="expandGraph();" *ngIf="!graphExpanded; else collapseBtn"><span i18n="show-more">Show more</span></button>
|
||||||
|
<ng-template #collapseBtn>
|
||||||
|
<button class="btn btn-sm btn-primary graph-toggle" (click)="collapseGraph();"><span i18n="show-less">Show less</span></button>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #flowPlaceholder>
|
||||||
|
<div class="box hidden">
|
||||||
|
<div class="graph-container" #graphContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<div class="subtitle-block">
|
||||||
|
<div class="title">
|
||||||
|
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title-buttons">
|
||||||
|
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<app-transactions-list #txList [transactions]="[transaction]" [transactionPage]="true" [txPreview]="true"></app-transactions-list>
|
||||||
|
|
||||||
|
<div class="title text-left">
|
||||||
|
<h2 i18n="transaction.details|Transaction Details">Details</h2>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.size">Size</td>
|
||||||
|
<td [innerHTML]="'‎' + (transaction.size | bytes: 2)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
|
<td [innerHTML]="'‎' + (transaction.weight / 4 | vbytes: 2)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="adjustedVsize">
|
||||||
|
<td><ng-container i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</ng-container>
|
||||||
|
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
|
||||||
|
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td [innerHTML]="'‎' + (adjustedVsize | vbytes: 2)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.weight">Weight</td>
|
||||||
|
<td [innerHTML]="'‎' + (transaction.weight | wuBytes: 2)"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="transaction.version">Version</td>
|
||||||
|
<td [innerHTML]="'‎' + (transaction.version | number)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="transaction.locktime">Locktime</td>
|
||||||
|
<td [innerHTML]="'‎' + (transaction.locktime | number)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="transaction.sigops >= 0">
|
||||||
|
<td><ng-container i18n="transaction.sigops|Transaction Sigops">Sigops</ng-container>
|
||||||
|
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
|
||||||
|
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td [innerHTML]="'‎' + (transaction.sigops | number)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="transaction.hex">Transaction hex</td>
|
||||||
|
<td><app-clipboard [text]="rawHexTransaction" [leftPadding]="false"></app-clipboard></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (isLoading) {
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-light mt-2 mb-2"></div>
|
||||||
|
<h3 i18n="transaction.error.loading-prevouts">
|
||||||
|
Loading {{ isLoadingPrevouts ? 'transaction prevouts' : isLoadingCpfpInfo ? 'CPFP' : '' }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
@ -0,0 +1,202 @@
|
|||||||
|
.label {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-buttons {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
@media (min-width: 650px) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0rem;
|
||||||
|
margin-right: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: baseline;
|
||||||
|
width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-right: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-top: 8px;
|
||||||
|
@media (min-width: 651px) {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-right: 1em;
|
||||||
|
top: 1px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
width: 100%;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txid {
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-xl {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 850px) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.box.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background: var(--stat-box-bg);
|
||||||
|
padding: 10px 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.25em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-toggle {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
tr td {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
padding: 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
text-align: right;
|
||||||
|
@media (min-width: 850px) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wrap-cell {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.effective-fee-container {
|
||||||
|
display: block;
|
||||||
|
@media (min-width: 768px){
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
@media (max-width: 425px){
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.effective-fee-rating {
|
||||||
|
@media (max-width: 767px){
|
||||||
|
margin-right: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
h2 {
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-info {
|
||||||
|
margin-top: 5px;
|
||||||
|
@media (min-width: 768px){
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-toggle {
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
@media (min-width: 768px){
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-buttons {
|
||||||
|
flex-shrink: 1;
|
||||||
|
text-align: right;
|
||||||
|
.btn {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cpfp-details {
|
||||||
|
.txids {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.txids {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-cursor {
|
||||||
|
cursor: default !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-broadcast {
|
||||||
|
margin-left: 5px;
|
||||||
|
@media (max-width: 567px) {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,320 @@
|
|||||||
|
import { Component, OnInit, HostListener, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
||||||
|
import { Transaction, Vout } from '@interfaces/electrs.interface';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { Filter, toFilters } from '../../shared/filters.utils';
|
||||||
|
import { decodeRawTransaction, getTransactionFlags, addInnerScriptsToVin, countSigops } from '../../shared/transaction.utils';
|
||||||
|
import { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs';
|
||||||
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||||
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
|
import { SeoService } from '../../services/seo.service';
|
||||||
|
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||||
|
import { ApiService } from '../../services/api.service';
|
||||||
|
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
|
import { CpfpInfo } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-transaction-raw',
|
||||||
|
templateUrl: './transaction-raw.component.html',
|
||||||
|
styleUrls: ['./transaction-raw.component.scss'],
|
||||||
|
})
|
||||||
|
export class TransactionRawComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
pushTxForm: UntypedFormGroup;
|
||||||
|
rawHexTransaction: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
isLoadingPrevouts: boolean;
|
||||||
|
isLoadingCpfpInfo: boolean;
|
||||||
|
offlineMode: boolean = false;
|
||||||
|
transaction: Transaction;
|
||||||
|
error: string;
|
||||||
|
errorPrevouts: string;
|
||||||
|
errorCpfpInfo: string;
|
||||||
|
hasPrevouts: boolean;
|
||||||
|
missingPrevouts: string[];
|
||||||
|
isLoadingBroadcast: boolean;
|
||||||
|
errorBroadcast: string;
|
||||||
|
successBroadcast: boolean;
|
||||||
|
broadcastSubscription: Subscription;
|
||||||
|
|
||||||
|
isMobile: boolean;
|
||||||
|
@ViewChild('graphContainer')
|
||||||
|
graphContainer: ElementRef;
|
||||||
|
graphExpanded: boolean = false;
|
||||||
|
graphWidth: number = 1068;
|
||||||
|
graphHeight: number = 360;
|
||||||
|
inOutLimit: number = 150;
|
||||||
|
maxInOut: number = 0;
|
||||||
|
flowPrefSubscription: Subscription;
|
||||||
|
hideFlow: boolean = this.stateService.hideFlow.value;
|
||||||
|
flowEnabled: boolean;
|
||||||
|
adjustedVsize: number;
|
||||||
|
filters: Filter[] = [];
|
||||||
|
hasEffectiveFeeRate: boolean;
|
||||||
|
fetchCpfp: boolean;
|
||||||
|
cpfpInfo: CpfpInfo | null;
|
||||||
|
hasCpfp: boolean = false;
|
||||||
|
showCpfpDetails = false;
|
||||||
|
mempoolBlocksSubscription: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public route: ActivatedRoute,
|
||||||
|
public router: Router,
|
||||||
|
public stateService: StateService,
|
||||||
|
public electrsApi: ElectrsApiService,
|
||||||
|
public websocketService: WebsocketService,
|
||||||
|
public formBuilder: UntypedFormBuilder,
|
||||||
|
public seoService: SeoService,
|
||||||
|
public apiService: ApiService,
|
||||||
|
public relativeUrlPipe: RelativeUrlPipe,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.seoService.setTitle($localize`:@@meta.title.preview-tx:Preview Transaction`);
|
||||||
|
this.seoService.setDescription($localize`:@@meta.description.preview-tx:Preview a transaction to the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network using the transaction's raw hex data.`);
|
||||||
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
|
this.pushTxForm = this.formBuilder.group({
|
||||||
|
txRaw: ['', Validators.required],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async decodeTransaction(): Promise<void> {
|
||||||
|
this.resetState();
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network);
|
||||||
|
await this.fetchPrevouts(tx);
|
||||||
|
await this.fetchCpfpInfo(tx);
|
||||||
|
this.processTransaction(tx, hex);
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error.message;
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPrevouts(transaction: Transaction): Promise<void> {
|
||||||
|
const prevoutsToFetch = transaction.vin.filter(input => !input.prevout).map((input) => ({ txid: input.txid, vout: input.vout }));
|
||||||
|
|
||||||
|
if (!prevoutsToFetch.length || transaction.vin[0].is_coinbase || this.offlineMode) {
|
||||||
|
this.hasPrevouts = !prevoutsToFetch.length || transaction.vin[0].is_coinbase;
|
||||||
|
this.fetchCpfp = this.hasPrevouts && !this.offlineMode;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.missingPrevouts = [];
|
||||||
|
this.isLoadingPrevouts = true;
|
||||||
|
|
||||||
|
const prevouts: { prevout: Vout, unconfirmed: boolean }[] = await firstValueFrom(this.apiService.getPrevouts$(prevoutsToFetch));
|
||||||
|
|
||||||
|
if (prevouts?.length !== prevoutsToFetch.length) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchIndex = 0;
|
||||||
|
transaction.vin.forEach(input => {
|
||||||
|
if (!input.prevout) {
|
||||||
|
const fetched = prevouts[fetchIndex];
|
||||||
|
if (fetched) {
|
||||||
|
input.prevout = fetched.prevout;
|
||||||
|
} else {
|
||||||
|
this.missingPrevouts.push(`${input.txid}:${input.vout}`);
|
||||||
|
}
|
||||||
|
fetchIndex++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.missingPrevouts.length) {
|
||||||
|
throw new Error(`Some prevouts do not exist or are already spent (${this.missingPrevouts.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasPrevouts = true;
|
||||||
|
this.isLoadingPrevouts = false;
|
||||||
|
this.fetchCpfp = prevouts.some(prevout => prevout?.unconfirmed);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
this.errorPrevouts = error?.error?.error || error?.message;
|
||||||
|
this.isLoadingPrevouts = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasPrevouts) {
|
||||||
|
transaction.fee = transaction.vin.some(input => input.is_coinbase)
|
||||||
|
? 0
|
||||||
|
: transaction.vin.reduce((fee, input) => {
|
||||||
|
return fee + (input.prevout?.value || 0);
|
||||||
|
}, 0) - transaction.vout.reduce((sum, output) => sum + output.value, 0);
|
||||||
|
transaction.feePerVsize = transaction.fee / (transaction.weight / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.vin.forEach(addInnerScriptsToVin);
|
||||||
|
transaction.sigops = countSigops(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCpfpInfo(transaction: Transaction): Promise<void> {
|
||||||
|
// Fetch potential cpfp data if all prevouts were parsed successfully and at least one of them is unconfirmed
|
||||||
|
if (this.hasPrevouts && this.fetchCpfp) {
|
||||||
|
try {
|
||||||
|
this.isLoadingCpfpInfo = true;
|
||||||
|
const cpfpInfo: CpfpInfo[] = await firstValueFrom(this.apiService.getCpfpLocalTx$([{
|
||||||
|
txid: transaction.txid,
|
||||||
|
weight: transaction.weight,
|
||||||
|
sigops: transaction.sigops,
|
||||||
|
fee: transaction.fee,
|
||||||
|
vin: transaction.vin,
|
||||||
|
vout: transaction.vout
|
||||||
|
}]));
|
||||||
|
|
||||||
|
if (cpfpInfo?.[0]?.ancestors?.length) {
|
||||||
|
const { ancestors, effectiveFeePerVsize } = cpfpInfo[0];
|
||||||
|
transaction.effectiveFeePerVsize = effectiveFeePerVsize;
|
||||||
|
this.cpfpInfo = { ancestors, effectiveFeePerVsize };
|
||||||
|
this.hasCpfp = true;
|
||||||
|
this.hasEffectiveFeeRate = true;
|
||||||
|
}
|
||||||
|
this.isLoadingCpfpInfo = false;
|
||||||
|
} catch (error) {
|
||||||
|
this.errorCpfpInfo = error?.error?.error || error?.message;
|
||||||
|
this.isLoadingCpfpInfo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processTransaction(tx: Transaction, hex: string): void {
|
||||||
|
this.transaction = tx;
|
||||||
|
this.rawHexTransaction = hex;
|
||||||
|
|
||||||
|
this.transaction.flags = getTransactionFlags(this.transaction, this.cpfpInfo, null, null, this.stateService.network);
|
||||||
|
this.filters = this.transaction.flags ? toFilters(this.transaction.flags).filter(f => f.txPage) : [];
|
||||||
|
if (this.transaction.sigops >= 0) {
|
||||||
|
this.adjustedVsize = Math.max(this.transaction.weight / 4, this.transaction.sigops * 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupGraph();
|
||||||
|
this.setFlowEnabled();
|
||||||
|
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||||
|
this.hideFlow = !!hide;
|
||||||
|
this.setFlowEnabled();
|
||||||
|
});
|
||||||
|
this.setGraphSize();
|
||||||
|
|
||||||
|
this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$.subscribe(() => {
|
||||||
|
if (this.transaction) {
|
||||||
|
this.stateService.markBlock$.next({
|
||||||
|
txid: this.transaction.txid,
|
||||||
|
txFeePerVSize: this.transaction.effectiveFeePerVsize || this.transaction.feePerVsize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
postTx(): void {
|
||||||
|
this.isLoadingBroadcast = true;
|
||||||
|
this.errorBroadcast = null;
|
||||||
|
|
||||||
|
this.broadcastSubscription = this.apiService.postTransaction$(this.rawHexTransaction).pipe(
|
||||||
|
tap((txid: string) => {
|
||||||
|
this.isLoadingBroadcast = false;
|
||||||
|
this.successBroadcast = true;
|
||||||
|
this.transaction.txid = txid;
|
||||||
|
}),
|
||||||
|
switchMap((txid: string) =>
|
||||||
|
timer(2000).pipe(
|
||||||
|
tap(() => this.router.navigate([this.relativeUrlPipe.transform('/tx/' + txid)])),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
catchError((error) => {
|
||||||
|
if (typeof error.error === 'string') {
|
||||||
|
const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
|
||||||
|
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
|
||||||
|
} else if (error.message) {
|
||||||
|
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message;
|
||||||
|
}
|
||||||
|
this.isLoadingBroadcast = false;
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetState() {
|
||||||
|
this.transaction = null;
|
||||||
|
this.rawHexTransaction = null;
|
||||||
|
this.error = null;
|
||||||
|
this.errorPrevouts = null;
|
||||||
|
this.errorBroadcast = null;
|
||||||
|
this.successBroadcast = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.isLoadingPrevouts = false;
|
||||||
|
this.isLoadingCpfpInfo = false;
|
||||||
|
this.isLoadingBroadcast = false;
|
||||||
|
this.adjustedVsize = null;
|
||||||
|
this.showCpfpDetails = false;
|
||||||
|
this.hasCpfp = false;
|
||||||
|
this.fetchCpfp = false;
|
||||||
|
this.cpfpInfo = null;
|
||||||
|
this.hasEffectiveFeeRate = false;
|
||||||
|
this.filters = [];
|
||||||
|
this.hasPrevouts = false;
|
||||||
|
this.missingPrevouts = [];
|
||||||
|
this.stateService.markBlock$.next({});
|
||||||
|
this.mempoolBlocksSubscription?.unsubscribe();
|
||||||
|
this.broadcastSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.resetState();
|
||||||
|
this.pushTxForm.get('txRaw').setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
setGraphSize(): void {
|
||||||
|
this.isMobile = window.innerWidth < 850;
|
||||||
|
if (this.graphContainer?.nativeElement && this.stateService.isBrowser) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.graphContainer?.nativeElement?.clientWidth) {
|
||||||
|
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||||
|
} else {
|
||||||
|
setTimeout(() => { this.setGraphSize(); }, 1);
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => { this.setGraphSize(); }, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupGraph() {
|
||||||
|
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.transaction?.vin?.length || 1, this.transaction?.vout?.length + 1 || 1));
|
||||||
|
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleGraph() {
|
||||||
|
const showFlow = !this.flowEnabled;
|
||||||
|
this.stateService.hideFlow.next(!showFlow);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFlowEnabled() {
|
||||||
|
this.flowEnabled = !this.hideFlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
expandGraph() {
|
||||||
|
this.graphExpanded = true;
|
||||||
|
this.graphHeight = this.maxInOut * 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseGraph() {
|
||||||
|
this.graphExpanded = false;
|
||||||
|
this.graphHeight = Math.min(360, this.maxInOut * 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOfflineModeChange(e): void {
|
||||||
|
this.offlineMode = !e.target.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.mempoolBlocksSubscription?.unsubscribe();
|
||||||
|
this.flowPrefSubscription?.unsubscribe();
|
||||||
|
this.stateService.markBlock$.next({});
|
||||||
|
this.broadcastSubscription?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -67,64 +67,7 @@
|
|||||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||||
|
|
||||||
<!-- CPFP Details -->
|
<!-- CPFP Details -->
|
||||||
<ng-template [ngIf]="showCpfpDetails">
|
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="tx"></app-cpfp-info>
|
||||||
<br>
|
|
||||||
<div class="title">
|
|
||||||
<h2 class="text-left" i18n="transaction.related-transactions|CPFP List">Related Transactions</h2>
|
|
||||||
</div>
|
|
||||||
<div class="box cpfp-details">
|
|
||||||
<table class="table table-fixed table-borderless table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
|
|
||||||
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
|
|
||||||
<th *only-vsize class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
|
|
||||||
<th *only-weight class="d-none d-lg-table-cell" i18n="transaction.weight|Transaction Weight">Weight</th>
|
|
||||||
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
|
||||||
<th class="d-none d-lg-table-cell"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
|
|
||||||
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
|
|
||||||
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
|
||||||
<td>
|
|
||||||
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
|
|
||||||
</td>
|
|
||||||
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
|
||||||
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
|
|
||||||
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
|
|
||||||
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) > roundToOneDecimal(tx)" class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
|
|
||||||
<tr>
|
|
||||||
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
|
||||||
<td class="txids">
|
|
||||||
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
|
|
||||||
</td>
|
|
||||||
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
|
|
||||||
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight | wuBytes: 2"></td>
|
|
||||||
<td><app-fee-rate [fee]="cpfpInfo.bestDescendant.fee" [weight]="cpfpInfo.bestDescendant.weight"></app-fee-rate></td>
|
|
||||||
<td class="d-none d-lg-table-cell"><fa-icon class="arrow-green" [icon]="['fas', 'angle-double-up']" [fixedWidth]="true"></fa-icon></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
|
|
||||||
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
|
||||||
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
|
||||||
<td class="txids">
|
|
||||||
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
|
|
||||||
</td>
|
|
||||||
<td *only-vsize class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
|
||||||
<td *only-weight class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight | wuBytes: 2"></td>
|
|
||||||
<td><app-fee-rate [fee]="cpfpTx.fee" [weight]="cpfpTx.weight"></app-fee-rate></td>
|
|
||||||
<td class="d-none d-lg-table-cell"><fa-icon *ngIf="roundToOneDecimal(cpfpTx) < roundToOneDecimal(tx)" class="arrow-red" [icon]="['fas', 'angle-double-down']" [fixedWidth]="true"></fa-icon></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<!-- Accelerator -->
|
<!-- Accelerator -->
|
||||||
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary && (ETA$ | async) as eta;">
|
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary && (ETA$ | async) as eta;">
|
||||||
|
@ -227,18 +227,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cpfp-details {
|
|
||||||
.txids {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
.txids {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tx-list {
|
.tx-list {
|
||||||
.alert-link {
|
.alert-link {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -1049,10 +1049,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
}
|
}
|
||||||
|
|
||||||
roundToOneDecimal(cpfpTx: any): number {
|
|
||||||
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupGraph() {
|
setupGraph() {
|
||||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
||||||
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||||
|
@ -9,6 +9,8 @@ import { TransactionExtrasModule } from '@components/transaction/transaction-ext
|
|||||||
import { GraphsModule } from '@app/graphs/graphs.module';
|
import { GraphsModule } from '@app/graphs/graphs.module';
|
||||||
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||||
import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component';
|
import { AccelerateFeeGraphComponent } from '@components/accelerate-checkout/accelerate-fee-graph.component';
|
||||||
|
import { TransactionRawComponent } from '@components/transaction/transaction-raw.component';
|
||||||
|
import { CpfpInfoComponent } from '@components/transaction/cpfp-info.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -16,6 +18,10 @@ const routes: Routes = [
|
|||||||
redirectTo: '/',
|
redirectTo: '/',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'preview',
|
||||||
|
component: TransactionRawComponent,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: TransactionComponent,
|
component: TransactionComponent,
|
||||||
@ -49,12 +55,15 @@ export class TransactionRoutingModule { }
|
|||||||
TransactionDetailsComponent,
|
TransactionDetailsComponent,
|
||||||
AccelerateCheckout,
|
AccelerateCheckout,
|
||||||
AccelerateFeeGraphComponent,
|
AccelerateFeeGraphComponent,
|
||||||
|
TransactionRawComponent,
|
||||||
|
CpfpInfoComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
TransactionComponent,
|
TransactionComponent,
|
||||||
TransactionDetailsComponent,
|
TransactionDetailsComponent,
|
||||||
AccelerateCheckout,
|
AccelerateCheckout,
|
||||||
AccelerateFeeGraphComponent,
|
AccelerateFeeGraphComponent,
|
||||||
|
CpfpInfoComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class TransactionModule { }
|
export class TransactionModule { }
|
||||||
|
@ -16,6 +16,11 @@
|
|||||||
<div class="header-bg box" [attr.data-cy]="'tx-' + i">
|
<div class="header-bg box" [attr.data-cy]="'tx-' + i">
|
||||||
|
|
||||||
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
|
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
|
||||||
|
@if (similarityMatches.get(tx.txid)?.size) {
|
||||||
|
<div class="alert alert-mempool" role="alert">
|
||||||
|
<span i18n="transaction.poison.warning">Warning! This transaction involves deceptively similar addresses. It may be an address poisoning attack.</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vin">
|
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vin">
|
||||||
@ -68,9 +73,11 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #defaultAddress>
|
<ng-template #defaultAddress>
|
||||||
<a class="address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
<app-address-text
|
||||||
<app-truncate [text]="vin.prevout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
*ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType"
|
||||||
</a>
|
[address]="vin.prevout.scriptpubkey_address"
|
||||||
|
[similarity]="similarityMatches.get(tx.txid)?.get(vin.prevout.scriptpubkey_address)"
|
||||||
|
></app-address-text>
|
||||||
<ng-template #vinScriptPubkeyType>
|
<ng-template #vinScriptPubkeyType>
|
||||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -216,9 +223,11 @@
|
|||||||
'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2)))
|
'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2)))
|
||||||
}">
|
}">
|
||||||
<td class="address-cell">
|
<td class="address-cell">
|
||||||
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
<app-address-text
|
||||||
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
*ngIf="vout.scriptpubkey_address; else pubkey_type"
|
||||||
</a>
|
[address]="vout.scriptpubkey_address"
|
||||||
|
[similarity]="similarityMatches.get(tx.txid)?.get(vout.scriptpubkey_address)"
|
||||||
|
></app-address-text>
|
||||||
<ng-template #pubkey_type>
|
<ng-template #pubkey_type>
|
||||||
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||||
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
|
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
|
||||||
|
@ -14,6 +14,7 @@ import { StorageService } from '@app/services/storage.service';
|
|||||||
import { OrdApiService } from '@app/services/ord-api.service';
|
import { OrdApiService } from '@app/services/ord-api.service';
|
||||||
import { Inscription } from '@app/shared/ord/inscription.utils';
|
import { Inscription } from '@app/shared/ord/inscription.utils';
|
||||||
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
|
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
|
||||||
|
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transactions-list',
|
selector: 'app-transactions-list',
|
||||||
@ -37,6 +38,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
@Input() addresses: string[] = [];
|
@Input() addresses: string[] = [];
|
||||||
@Input() rowLimit = 12;
|
@Input() rowLimit = 12;
|
||||||
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
||||||
|
@Input() txPreview = false;
|
||||||
|
|
||||||
@Output() loadMore = new EventEmitter();
|
@Output() loadMore = new EventEmitter();
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
showFullScript: { [vinIndex: number]: boolean } = {};
|
showFullScript: { [vinIndex: number]: boolean } = {};
|
||||||
showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {};
|
showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {};
|
||||||
showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {};
|
showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {};
|
||||||
|
similarityMatches: Map<string, Map<string, { score: number, match: AddressMatch, group: number }>> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
@ -81,7 +84,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
this.refreshOutspends$
|
this.refreshOutspends$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((txIds) => {
|
switchMap((txIds) => {
|
||||||
if (!this.cached) {
|
if (!this.cached && !this.txPreview) {
|
||||||
// break list into batches of 50 (maximum supported by esplora)
|
// break list into batches of 50 (maximum supported by esplora)
|
||||||
const batches = [];
|
const batches = [];
|
||||||
for (let i = 0; i < txIds.length; i += 50) {
|
for (let i = 0; i < txIds.length; i += 50) {
|
||||||
@ -119,7 +122,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
),
|
),
|
||||||
this.refreshChannels$
|
this.refreshChannels$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(() => this.stateService.networkSupportsLightning()),
|
filter(() => this.stateService.networkSupportsLightning() && !this.txPreview),
|
||||||
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
// handle 404
|
// handle 404
|
||||||
@ -143,6 +146,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.refreshPrice();
|
this.refreshPrice();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateAddressSimilarities();
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshPrice(): void {
|
refreshPrice(): void {
|
||||||
@ -182,12 +187,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changes.transactions || changes.addresses) {
|
if (changes.transactions || changes.addresses) {
|
||||||
|
this.similarityMatches.clear();
|
||||||
|
this.updateAddressSimilarities();
|
||||||
if (!this.transactions || !this.transactions.length) {
|
if (!this.transactions || !this.transactions.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transactionsLength = this.transactions.length;
|
this.transactionsLength = this.transactions.length;
|
||||||
this.cacheService.setTxCache(this.transactions);
|
|
||||||
|
if (!this.txPreview) {
|
||||||
|
this.cacheService.setTxCache(this.transactions);
|
||||||
|
}
|
||||||
|
|
||||||
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
const confirmedTxs = this.transactions.filter((tx) => tx.status.confirmed).length;
|
||||||
this.transactions.forEach((tx) => {
|
this.transactions.forEach((tx) => {
|
||||||
@ -292,6 +302,56 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateAddressSimilarities(): void {
|
||||||
|
if (!this.transactions || !this.transactions.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const tx of this.transactions) {
|
||||||
|
if (this.similarityMatches.get(tx.txid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const similarityGroups: Map<string, number> = new Map();
|
||||||
|
let lastGroup = 0;
|
||||||
|
|
||||||
|
// Check for address poisoning similarity matches
|
||||||
|
this.similarityMatches.set(tx.txid, new Map());
|
||||||
|
const comparableVouts = [
|
||||||
|
...tx.vout.slice(0, 20),
|
||||||
|
...this.addresses.map(addr => ({ scriptpubkey_address: addr, scriptpubkey_type: detectAddressType(addr, this.stateService.network) }))
|
||||||
|
].filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v.scriptpubkey_type));
|
||||||
|
const comparableVins = tx.vin.slice(0, 20).map(v => v.prevout).filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v?.scriptpubkey_type));
|
||||||
|
for (const vout of comparableVouts) {
|
||||||
|
const address = vout.scriptpubkey_address;
|
||||||
|
const addressType = vout.scriptpubkey_type;
|
||||||
|
if (this.similarityMatches.get(tx.txid)?.has(address)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const compareAddr of [
|
||||||
|
...comparableVouts.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address),
|
||||||
|
...comparableVins.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address)
|
||||||
|
]) {
|
||||||
|
const similarity = checkedCompareAddressStrings(address, compareAddr.scriptpubkey_address, addressType as AddressType, this.stateService.network);
|
||||||
|
if (similarity?.status === 'comparable' && similarity.score > ADDRESS_SIMILARITY_THRESHOLD) {
|
||||||
|
let group = similarityGroups.get(address) || lastGroup++;
|
||||||
|
similarityGroups.set(address, group);
|
||||||
|
const bestVout = this.similarityMatches.get(tx.txid)?.get(address);
|
||||||
|
if (!bestVout || bestVout.score < similarity.score) {
|
||||||
|
this.similarityMatches.get(tx.txid)?.set(address, { score: similarity.score, match: similarity.left, group });
|
||||||
|
}
|
||||||
|
// opportunistically update the entry for the compared address
|
||||||
|
const bestCompare = this.similarityMatches.get(tx.txid)?.get(compareAddr.scriptpubkey_address);
|
||||||
|
if (!bestCompare || bestCompare.score < similarity.score) {
|
||||||
|
group = similarityGroups.get(compareAddr.scriptpubkey_address) || lastGroup++;
|
||||||
|
similarityGroups.set(compareAddr.scriptpubkey_address, group);
|
||||||
|
this.similarityMatches.get(tx.txid)?.set(compareAddr.scriptpubkey_address, { score: similarity.score, match: similarity.right, group });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onScroll(): void {
|
onScroll(): void {
|
||||||
this.loadMore.emit();
|
this.loadMore.emit();
|
||||||
}
|
}
|
||||||
@ -351,7 +411,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadMoreInputs(tx: Transaction): void {
|
loadMoreInputs(tx: Transaction): void {
|
||||||
if (!tx['@vinLoaded']) {
|
if (!tx['@vinLoaded'] && !this.txPreview) {
|
||||||
this.electrsApiService.getTransaction$(tx.txid)
|
this.electrsApiService.getTransaction$(tx.txid)
|
||||||
.subscribe((newTx) => {
|
.subscribe((newTx) => {
|
||||||
tx['@vinLoaded'] = true;
|
tx['@vinLoaded'] = true;
|
||||||
|
@ -34,29 +34,39 @@ export class TwitterWidgetComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIframeSrc(): void {
|
setIframeSrc(): void {
|
||||||
if (this.handle) {
|
if (!this.handle) {
|
||||||
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL,
|
return;
|
||||||
`https://syndication.x.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool`
|
|
||||||
+ '&dnt=true'
|
|
||||||
+ '&embedId=twitter-widget-0'
|
|
||||||
+ '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D'
|
|
||||||
+ '&frame=false'
|
|
||||||
+ '&hideBorder=true'
|
|
||||||
+ '&hideFooter=false'
|
|
||||||
+ '&hideHeader=true'
|
|
||||||
+ '&hideScrollBar=false'
|
|
||||||
+ `&lang=${this.lang}`
|
|
||||||
+ '&maxHeight=500px'
|
|
||||||
+ '&origin=https%3A%2F%2Fmempool.space%2F'
|
|
||||||
// + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
|
|
||||||
+ '&showHeader=false'
|
|
||||||
+ '&showReplies=false'
|
|
||||||
+ '&siteScreenName=mempool'
|
|
||||||
+ '&theme=dark'
|
|
||||||
+ '&transparent=true'
|
|
||||||
+ '&widgetsVersion=2615f7e52b7e0%3A1702314776716'
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
let url = `https://syndication.x.com/srv/timeline-profile/screen-name/${this.handle}?creatorScreenName=mempool`
|
||||||
|
+ '&dnt=true'
|
||||||
|
+ '&embedId=twitter-widget-0'
|
||||||
|
+ '&features=eyJ0ZndfdGltZWxpbmVfgbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D'
|
||||||
|
+ '&frame=false'
|
||||||
|
+ '&hideBorder=true'
|
||||||
|
+ '&hideFooter=false'
|
||||||
|
+ '&hideHeader=true'
|
||||||
|
+ '&hideScrollBar=false'
|
||||||
|
+ `&lang=${this.lang}`
|
||||||
|
+ '&maxHeight=500px'
|
||||||
|
+ '&origin=https%3A%2F%2Fmempool.space%2F'
|
||||||
|
// + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9'
|
||||||
|
+ '&showHeader=false'
|
||||||
|
+ '&showReplies=false'
|
||||||
|
+ '&siteScreenName=mempool'
|
||||||
|
+ '&theme=dark'
|
||||||
|
+ '&transparent=true'
|
||||||
|
+ '&widgetsVersion=2615f7e52b7e0%3A1702314776716';
|
||||||
|
switch (this.handle.toLowerCase()) {
|
||||||
|
case 'nayibbukele':
|
||||||
|
url = 'https://bitcoin.gob.sv/twidget';
|
||||||
|
break;
|
||||||
|
case 'metaplanet_jp':
|
||||||
|
url = 'https://metaplanet.mempool.space/twidget';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, url));
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(): void {
|
onReady(): void {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user