Merge branch 'master' into nymkappa/health-check

This commit is contained in:
nymkappa 2025-09-26 11:21:13 +02:00
commit 3afd7911d4
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
56 changed files with 1190 additions and 1045 deletions

View file

@ -1,181 +0,0 @@
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 }}"

View file

@ -24,6 +24,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 120
name: Build and push to DockerHub
outputs:
image-digest-frontend: ${{ matrix.service == 'frontend' && steps.docker-build.outputs.digest || '' }}
image-digest-backend: ${{ matrix.service == 'backend' && steps.docker-build.outputs.digest || '' }}
steps:
- name: Replace the current swap file
shell: bash
@ -99,15 +102,51 @@ jobs:
${{ runner.os }}-buildx-${{ matrix.service }}-
- name: Run Docker buildx for ${{ matrix.service }} against tag
id: docker-build
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
--build-context rustgbt=./rust \
--build-context backend=./backend \
--output "type=registry,push=true" \
--build-arg commitHash=$SHORT_SHA \
./${{ matrix.service }}/
tag-latest:
needs: build
if: ${{ needs.build.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 30
name: Tag images as latest
strategy:
matrix:
service:
- frontend
- backend
steps:
- name: Set env variables
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: linux/amd64,linux/arm64
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
driver-opts: |
network=host
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Tag multi-arch image as latest for ${{ matrix.service }}
run: |
docker buildx imagetools create \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG

View file

@ -12,7 +12,7 @@
"dependencies": {
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.10.0",
"axios": "1.12.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",
@ -2275,12 +2275,12 @@
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@ -2645,6 +2645,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2997,6 +3009,19 @@
"node": ">=6.0.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -3044,12 +3069,9 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": {
"node": ">= 0.4"
}
@ -3062,6 +3084,31 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@ -3683,12 +3730,14 @@
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@ -3774,15 +3823,20 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@ -3800,6 +3854,18 @@
"node": ">=8.0.0"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -3874,11 +3940,11 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dependencies": {
"get-intrinsic": "^1.1.3"
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -3928,10 +3994,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": {
"node": ">= 0.4"
},
@ -3939,10 +4005,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@ -6041,6 +6110,14 @@
"tmpl": "1.0.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/maxmind": {
"version": "4.3.11",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz",
@ -9457,12 +9534,12 @@
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
},
"axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@ -9744,6 +9821,15 @@
"set-function-length": "^1.2.1"
}
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -9991,6 +10077,16 @@
"esutils": "^2.0.2"
}
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -10029,18 +10125,34 @@
}
},
"es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"requires": {
"get-intrinsic": "^1.2.4"
}
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"requires": {
"es-errors": "^1.3.0"
}
},
"es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"requires": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
}
},
"escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@ -10513,12 +10625,14 @@
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
}
},
@ -10576,15 +10690,20 @@
"dev": true
},
"get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-package-type": {
@ -10593,6 +10712,15 @@
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@ -10643,12 +10771,9 @@
}
},
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"requires": {
"get-intrinsic": "^1.1.3"
}
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"graceful-fs": {
"version": "4.2.11",
@ -10685,15 +10810,18 @@
"es-define-property": "^1.0.0"
}
},
"has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"requires": {
"has-symbols": "^1.0.3"
}
},
"hasown": {
"version": "2.0.2",
@ -12245,6 +12373,11 @@
"tmpl": "1.0.5"
}
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"maxmind": {
"version": "4.3.11",
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.11.tgz",

View file

@ -41,7 +41,7 @@
"dependencies": {
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.10.0",
"axios": "1.12.2",
"bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0",
"express": "~4.21.1",

View file

@ -8,6 +8,7 @@ export interface AbstractBitcoinApi {
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
$getTransactionHex(txId: string): Promise<string>;
$getTransactionMerkleProof(txId: string): Promise<IEsploraApi.MerkleProof>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
$getTxIdsForBlock(hash: string): Promise<string[]>;

View file

@ -95,6 +95,10 @@ class BitcoinApi implements AbstractBitcoinApi {
});
}
$getTransactionMerkleProof(txId: string): Promise<IEsploraApi.MerkleProof> {
throw new Error('Method getTransactionMerkleProof not supported by the Bitcoin RPC API.');
}
$getBlockHeightTip(): Promise<number> {
return this.bitcoindClient.getBlockCount();
}
@ -294,7 +298,7 @@ class BitcoinApi implements AbstractBitcoinApi {
is_coinbase: !!vin.coinbase,
prevout: null,
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
scriptsig_asm: vin.scriptSig && transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) || '',
scriptsig_asm: vin.scriptSig ? transactionUtils.convertScriptSigAsm(vin.scriptSig.hex) : (vin.coinbase ? transactionUtils.convertScriptSigAsm(vin.coinbase) : ''),
sequence: vin.sequence,
txid: vin.txid || '',
vout: vin.vout || 0,

View file

@ -23,6 +23,7 @@ import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
import poolsUpdater from '../../tasks/pools-updater';
import BlocksRepository from '../../repositories/BlocksRepository';
import chainTips from '../chain-tips';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
@ -56,6 +57,7 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'chain-tips', this.getChainTips.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
@ -77,6 +79,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/merkle-proof', this.getTransactionMerkleProof)
.get(config.MEMPOOL.API_URL_PREFIX + 'txs/outspends', this.$getBatchedOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
@ -608,6 +611,26 @@ class BitcoinRoutes {
}
}
private async getChainTips(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
const tips = await chainTips.getChainTips();
if (tips.length > 0) {
res.json(tips);
} else {
handleError(req, res, 503, `Temporarily unavailable`);
return;
}
} else { // Liquid
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
return;
}
} catch (e) {
handleError(req, res, 500, 'Failed to get chain tips');
}
}
private async getLegacyBlocks(req: Request, res: Response) {
try {
const returnBlocks: IEsploraApi.Block[] = [];
@ -1006,6 +1029,19 @@ class BitcoinRoutes {
}
}
private async getTransactionMerkleProof(req: Request, res: Response): Promise<void> {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const result = await bitcoinApi.$getTransactionMerkleProof(req.params.txId);
res.json(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : 'Failed to get transaction merkle proof');
}
}
private getDifficultyChange(req: Request, res: Response) {
try {
const da = difficultyAdjustment.getDifficultyAdjustment();

View file

@ -197,6 +197,11 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
}
}
async $getTransactionMerkleProof(txId: string): Promise<IEsploraApi.MerkleProof> {
const tx = await this.$getRawTransaction(txId);
return this.electrumClient.blockchainTransaction_getMerkle(txId, tx.status.block_height);
}
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
}

View file

@ -186,4 +186,10 @@ export namespace IEsploraApi {
time: number;
tx_position?: number;
}
export interface MerkleProof {
merkle: string[];
block_height: number;
pos: number;
}
}

View file

@ -1,6 +1,7 @@
import config from '../../config';
import axios, { isAxiosError } from 'axios';
import http from 'http';
import https from 'https';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
@ -23,6 +24,7 @@ interface FailoverHost {
publicDomain: string,
hashes: {
frontend?: string,
hybrid?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
@ -142,7 +144,8 @@ class FailoverRouter {
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
this.$updateFrontendGitHash(host),
this.$updateBackendGitHash(host)
this.$updateBackendGitHash(host),
config.MEMPOOL.OFFICIAL ? this.$updateHybridGitHash(host) : Promise.resolve(),
]);
host.hashes.lastUpdated = Date.now();
}
@ -251,6 +254,47 @@ class FailoverRouter {
if (match && match[1]?.length) {
host.hashes.frontend = match[1];
}
const hybridMatch = response.data.match(/GIT_COMMIT_HASH_MEMPOOL_SPACE\s*=\s*['"](.*?)['"]/);
if (hybridMatch && hybridMatch[1]?.length) {
host.hashes.hybrid = hybridMatch[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
}
private async $updateHybridGitHash(host: FailoverHost): Promise<void> {
try {
const response: string = await new Promise((resolve, reject) => {
const req = https.request({
hostname: host.publicDomain.replace('https://', '').replace('http://', ''),
port: 443,
path: '/en-US/resources/config.js',
method: 'GET',
headers: {
'Host': 'mempool.space'
},
timeout: config.ESPLORA.FALLBACK_TIMEOUT,
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode === 200) {
resolve(data);
} else {
reject(new Error(`Failed to get hybrid git hash: ${res.statusCode}`));
}
});
});
req.on('error', (e) => {
reject(e);
});
req.end();
});
const match = response.match(/GIT_COMMIT_HASH_MEMPOOL_SPACE\s*=\s*['"](.*?)['"]/);
if (match && match[1]?.length) {
host.hashes.hybrid = match[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
@ -352,6 +396,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<string>('/tx/' + txId + '/hex');
}
$getTransactionMerkleProof(txId: string): Promise<IEsploraApi.MerkleProof> {
return this.failoverRouter.$get<IEsploraApi.MerkleProof>('/tx/' + txId + '/merkle-proof');
}
$getBlockHeightTip(): Promise<number> {
return this.failoverRouter.$get<number>('/blocks/tip/height');
}

View file

@ -86,6 +86,10 @@ class ChainTips {
return this.orphansByHeight[height] || [];
}
public getChainTips(): ChainTip[] {
return this.chainTips;
}
}
export default new ChainTips();

View file

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 100;
private static currentVersion = 101;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@ -1299,6 +1299,10 @@ class DatabaseMigration {
queries.push(`DELETE FROM state WHERE name = 'last_weekly_hashrates_indexing'`);
}
if (version < 101) {
queries.push(`DELETE FROM prices WHERE USD = -1`);
}
return queries;
}

View file

@ -180,7 +180,6 @@ class WalletApi {
// update list of treasuries
const treasuriesResponse = await axios.get(config.MEMPOOL_SERVICES.API + `/treasuries`);
console.log(treasuriesResponse.data);
this.treasuries = treasuriesResponse.data || [];
} catch (e) {
logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`);

View file

@ -887,7 +887,9 @@ class BlocksRepository {
SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.height
FROM blocks
LEFT JOIN blocks_prices ON blocks.height = blocks_prices.height
LEFT JOIN prices ON blocks_prices.price_id = prices.id
WHERE blocks_prices.height IS NULL
OR prices.id IS NULL
ORDER BY blocks.height
`);
return rows;
@ -907,6 +909,7 @@ class BlocksRepository {
query += ` (${price.height}, ${price.priceId}),`;
}
query = query.slice(0, -1);
query += ` ON DUPLICATE KEY UPDATE price_id = VALUES(price_id)`;
await DB.query(query);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart

View file

@ -296,6 +296,7 @@ class PricesRepository {
id,
USD
FROM prices
WHERE USD >= 0
ORDER BY time
`);
return times as {time: number, id: number, USD: number}[];
@ -305,6 +306,7 @@ class PricesRepository {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
WHERE USD >= 0
ORDER BY time DESC
LIMIT 1`
);
@ -321,6 +323,7 @@ class PricesRepository {
SELECT ${ApiPriceFields}
FROM prices
WHERE UNIX_TIMESTAMP(time) < ?
AND USD >= 0
ORDER BY time DESC
LIMIT 1`,
[timestamp]
@ -332,6 +335,7 @@ class PricesRepository {
const [latestPrices] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
WHERE USD >= 0
ORDER BY time DESC
LIMIT 1
`);
@ -429,6 +433,7 @@ class PricesRepository {
const [rates] = await DB.query(`
SELECT ${ApiPriceFields}
FROM prices
WHERE USD >= 0
ORDER BY time DESC
`);
if (!Array.isArray(rates)) {

View file

@ -271,11 +271,6 @@ class PriceUpdater {
}
this.latestPrices.time = Math.round(new Date().getTime() / 1000);
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
if (this.ratesChangedCallback) {
this.ratesChangedCallback(this.latestPrices);
}
if (!forceUpdate) {
this.cyclePosition++;
@ -283,6 +278,13 @@ class PriceUpdater {
if (this.latestPrices.USD === -1) {
this.latestPrices = await PricesRepository.$getLatestConversionRates();
logger.warn(`No BTC price available, falling back to latest known price: ${JSON.stringify(this.latestPrices)}`);
} else {
logger.info(`Latest BTC fiat averaged price: ${JSON.stringify(this.latestPrices)}`);
}
if (this.ratesChangedCallback && this.latestPrices.USD > 0) {
this.ratesChangedCallback(this.latestPrices);
}
}

View file

@ -18,7 +18,7 @@ export async function query(path, throwOnFail: boolean = false): Promise<object
headers: {
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
},
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 20000
};
let retry = 0;
let lastError: any = null;

File diff suppressed because it is too large Load diff

View file

@ -116,7 +116,7 @@
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
"start-server-and-test": "~2.0.0"
"start-server-and-test": "~2.1.0"
},
"scarfSettings": {
"enabled": false

View file

@ -7,8 +7,6 @@ import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempoo
import { ClockComponent } from '@components/clock/clock.component';
import { StatusViewComponent } from '@components/status-view/status-view.component';
import { AddressGroupComponent } from '@components/address-group/address-group.component';
import { TrackerComponent } from '@components/tracker/tracker.component';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from '@app/route-guards';
const browserWindow = window || {};

View file

@ -140,14 +140,11 @@
<span>Metaplanet</span>
</a>
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)" fill-rule="evenodd" clip-rule="evenodd" fill="#e21924">
<path d="M21.92 14.59a1.18 1.18 0 0 0-1.18-1.18h-1.82v2.36h1.82a1.18 1.18 0 0 0 1.18-1.18ZM21 17.07h-2v2.45h2a1.23 1.23 0 1 0 0-2.45Z"/>
<path d="M36.43 0 35 5.59l-8 2.64-2.43-3.61-4.74 2.05-4.74-2.05-2.43 3.61-8-2.64L3.21 0 0 7.86l7.89 5.86-5.56 4 5.56 1.12 2.69-.49v3.17l3.59 4.38.68 3.19 5 2.87 5-2.87.68-3.19 3.59-4.38v-3.17l2.7.49 5.56-1.12-5.56-4 7.89-5.86zM24.69 18.45a2.5 2.5 0 0 1-2.5 2.5h-1.11v1.56h-1.26V21h-.9v1.56h-1.27V21H15.3v-1.42h.64a.9.9 0 0 0 .9-.9V14.3a.901.901 0 0 0-.9-.91h-.64V12h2.35v-1.5h1.27V12h.9v-1.5h1.26V12h.68A2.269 2.269 0 0 1 24 14.31a2.25 2.25 0 0 1-.92 1.82 2.52 2.52 0 0 1 1.58 2.32z"/>
</g>
<defs>
<clipPath id="a"><path fill="#fff" d="M0 0h160v32H0z"/></clipPath>
</defs>
<svg width="80" version="1.1" viewBox="0 0 1128 1052.59" xmlns="http://www.w3.org/2000/svg">
<path transform="matrix(.13333 0 0 -.13333 0 952.59)" d="m4322.7 2375.3-38.64 2787.8 931.44 396 506.88-738.72 1706.8 581.4-474.93 1742.6 1506-2083.5-1805.8-1329.1 1015.4-775.2-1035.4-258.72-482.22 126.18-1829.7-448.74" style="fill:#b7000b"/>
<path transform="matrix(.13333 0 0 -.13333 0 952.59)" d="m4137.1 2378.5 40.86 2782.8-932.64 398.04-505.92-737.76-1709.1 580.92 475.26 1741.3-1505.6-2083.1 1805.6-1328.2-1015.7-775.92 1035.8-259.08 485.76 127.56 1825.6-446.52" style="fill:#b7000b"/>
<path transform="matrix(.13333 0 0 -.13333 0 952.59)" d="m2345.9 2694.6 5.04-585.36 722.52-883.08 142.56-656.64 958.32-569.52-15.36 1765.7-1813.1 928.92" style="fill:#b7000b"/>
<path transform="matrix(.13333 0 0 -.13333 0 952.59)" d="m6114.4 2694.6-5.04-585.36-722.52-883.08-142.56-656.64-958.32-569.52 15.36 1765.7 1813.1 928.92" style="fill:#b7000b"/>
</svg>
<span>Bull Bitcoin</span>
</a>

View file

@ -474,19 +474,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.applePay) {
this.applePay.destroy();
}
const costUSD = this.cost / 100_000_000 * conversions.USD;
const costUSD = this.cost / 100_000_000 * this.conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
@ -585,8 +580,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.processing = false;
console.error(e);
}
}
);
}
/**
@ -596,19 +589,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.googlePay) {
this.googlePay.destroy();
}
const costUSD = this.cost / 100_000_000 * conversions.USD;
const costUSD = this.cost / 100_000_000 * this.conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
@ -710,8 +698,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.isCheckoutLocked--;
}
});
}
);
}
/**
@ -721,16 +707,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
const costUSD = this.cost / 100_000_000 * conversions.USD;
const costUSD = this.cost / 100_000_000 * this.conversions.USD;
if (this.isCheckoutLocked > 0) {
return;
}
@ -815,8 +795,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.isCheckoutLocked--;
this.isTokenizing--;
}
}
);
}
/**
@ -826,20 +804,15 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
if (this.cashAppPay) {
this.cashAppPay.destroy();
}
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
const costUSD = this.cost / 100_000_000 * conversions.USD;
const costUSD = this.cost / 100_000_000 * this.conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
@ -902,8 +875,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
});
}
});
}
);
}
/**

View file

@ -1,4 +1,4 @@
<div class="container-lg widget-container" [class.widget]="widget" [class.full-height]="!widget">
<div class="container-lg widget-container p-0" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="accelerator.accelerations">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>

View file

@ -141,7 +141,7 @@ tr, td, th {
}
.fee {
width: 30%;
width: 25%;
text-align: end !important;
}
@ -153,7 +153,7 @@ tr, td, th {
}
.status {
width: 20%
width: 25%
}
}

View file

@ -293,7 +293,7 @@
<ng-template #volumeRow>
<td i18n="address.total-received">Total received</td>
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd" class="wrap-cell"><app-amount [satoshis]="chainStats.totalReceived"></app-amount></td>
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd" class="wrap-cell"><app-amount [satoshis]="chainStats.totalReceived" [noFiat]="true"></app-amount></td>
</ng-template>
<ng-template #typeRow>

View file

@ -1,4 +1,4 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.none-mode]="filterMode === 'nor'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges float-right" i18n-ngbTooltip="Mempool Goggles&trade; tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a>
@ -23,6 +23,9 @@
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')"><ng-container i18n="mempool-goggles.any">Any</ng-container>
</label>
<label class="btn btn-xs red mode-toggle" [class.active]="filterMode === 'nor'">
<input type="radio" [value]="'nor'" fragment="nor" (click)="setFilterMode('nor')"><ng-container i18n="mempool-goggles.none">None</ng-container>
</label>
</div>
</div>
<div class="filter-element">

View file

@ -94,6 +94,15 @@
}
}
&.none-mode {
.filter-tag {
border: solid 1px var(--danger);
&.selected {
background-color: var(--danger);
}
}
}
.btn-group {
font-size: 0.9em;
margin-right: 0.25em;
@ -126,6 +135,12 @@
background: var(--success);
}
}
&.red {
border: solid 1px var(--danger);
&.active {
background: var(--danger);
}
}
&.yellow {
border: solid 1px #bf7815;
&.active {

View file

@ -656,7 +656,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
let matches = false;
switch (this.filterMode) {
case 'and':
matches = (tx.bigintFlags & flags) === flags;
break;
case 'or':
matches = flags === 0n || (tx.bigintFlags & flags) > 0n;
break;
case 'nor':
matches = (tx.bigintFlags & flags) === 0n;
break;
}
if (matches) {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else {

View file

@ -19,6 +19,7 @@
<th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th>
<th class="height">Height</th>
<th class="hybrid only-large">Hybrid</th>
<th class="frontend only-large">Front</th>
<th class="backend only-large">Back</th>
<th class="electrs only-large">Electrs</th>
@ -31,10 +32,10 @@
<td class="rtt only-small">{{ (host.rtt / 1000) | number : '1.1-1' }} {{ host.rtt == null ? '' : 's'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="rtt only-large">{{ host.rtt | number : '1.0-0' }} {{ host.rtt == null ? '' : 'ms'}} {{ !host.checked ? '⏳' : (host.unreachable ? '🔥' : '✅') }}</td>
<td class="height">{{ host.latestHeight }} {{ !host.checked ? '⏳' : (host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '')) }}</td>
<ng-container *ngFor="let type of ['frontend', 'backend', 'electrs']">
<ng-container *ngFor="let type of ['hybrid', 'frontend', 'backend', 'electrs']">
<td class="{{type}} only-large" [style.background-color]="host.hashes?.[type] ? '#' + host.hashes[type].slice(0, 6) : ''">
@if (host.hashes?.[type]) {
<a [style.color]="'white'" href="https://github.com/mempool/{{type === 'electrs' ? 'electrs' : 'mempool'}}/commit/{{ host.hashes[type] }}" target="_blank">{{ host.hashes[type].slice(0, 8) || '?' }}</a>
<a [style.color]="'white'" href="https://github.com/mempool/{{repoMap[type]}}/commit/{{ host.hashes[type] }}" target="_blank">{{ host.hashes[type].slice(0, 8) || '?' }}</a>
} @else {
<span>?</span>
}

View file

@ -17,6 +17,13 @@ export class ServerHealthComponent implements OnInit {
interval: number;
now: number = Date.now();
repoMap = {
frontend: 'mempool',
hybrid: 'mempool.space',
backend: 'mempool',
electrs: 'electrs',
};
constructor(
private websocketService: WebsocketService,
private stateService: StateService,

View file

@ -326,9 +326,18 @@ export class TaprootAddressScriptsComponent implements OnChanges {
let hiddenScriptsMessage = '';
if (node.tooltip[0].label === 'Hash') {
const remaining = 128 - (node.depth ?? 0);
let upperBoundHtml: string;
if (remaining === 0) {
upperBoundHtml = '1';
} else if (remaining <= 39) {
upperBoundHtml = (2 ** remaining).toLocaleString();
} else {
upperBoundHtml = `2<sup style="font-size: 0.85em;">${remaining}</sup>`;
}
hiddenScriptsMessage = `
<div style="margin-top: 8px; color: #888; font-size: 11px; line-height: 1.3; font-style: italic; border-top: 1px solid #333; padding-top: 6px; word-break: break-word; white-space: normal">
This node might commit to one or more scripts that have not been revealed yet.
This node might commit to ${upperBoundHtml === '1' ? 'exactly 1 script' : `at most ${upperBoundHtml} scripts`}.
</div>`;
}

View file

@ -341,7 +341,22 @@
@if (vout.isRunestone) {
<button (click)="toggleOrdData(tx.txid, 'vout', vindex)" type="button" class="btn btn-sm badge badge-ord">Runestone</button>
} @else {
<a placement="bottom" [ngbTooltip]="vout.scriptpubkey_asm | slice:0:200 | hex2ascii"><span *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="badge badge-secondary scriptmessage">{{ vout.scriptpubkey_asm | slice:0:200 | hex2ascii }}</span></a>
<a placement="bottom" style="cursor: pointer;">
@if ((vout.scriptpubkey_asm | hex2ascii).length > 200) {
<span *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="badge badge-secondary scriptmessage opreturn-msg" (click)="toggleShowFullOpReturnPreview(vindex)">{{ vout.scriptpubkey_asm | hex2ascii | slice:0:200 }}</span>
<ng-container *ngIf="(showDetails$ | async) === false">
@if ((vout.scriptpubkey_asm | hex2ascii).length > 200) {
@if (!showFullOpReturnPreview[vindex]) {
<span class="badge badge-primary" (click)="toggleShowFullOpReturnPreview(vindex)">Show more</span>
} @else {
<span class="badge badge-primary" (click)="toggleShowFullOpReturnPreview(vindex)">Show less</span>
}
}
</ng-container>
} @else {
<span *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" [ngbTooltip]="vout.scriptpubkey_asm | hex2ascii" class="badge badge-secondary scriptmessage">{{ vout.scriptpubkey_asm | hex2ascii }}</span>
}
</a>
}
</ng-template>
<ng-template #otherPubkeyType>{{ vout.scriptpubkey_type | scriptpubkeyType }}</ng-template>
@ -392,6 +407,15 @@
<app-ord-data [runestone]="showOrdData[tx.txid + '-vout-' + vindex]['runestone']" [runeInfo]="showOrdData[tx.txid + '-vout-' + vindex]['runeInfo']" [type]="'vout'"></app-ord-data>
</td>
</tr>
<tr *ngIf="vout.scriptpubkey_type === 'op_return' && showFullOpReturnPreview[vindex]" [ngClass]="{
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
'highlight': addresses?.length && (addresses.includes(vout.scriptpubkey_address)),
'sigged': selectedSig && selectedSig.txIndex === i && sigHighlights.vout[vindex],
}">
<td colspan="3" style="white-space: pre-line;">
{{ vout.scriptpubkey_asm | hex2ascii | slice:0:8000 }}
</td>
</tr>
<tr *ngIf="(showDetails$ | async) === true">
<td [attr.colspan]="3" class="details-container">
<table class="table table-striped table-borderless details-table mb-3">
@ -428,7 +452,7 @@
</tr>
<tr *ngIf="vout.scriptpubkey_type == 'op_return'">
<td>OP_RETURN <span>data</span></td>
<td style="text-align: left; word-break: break-word;">
<td style="text-align: left; word-break: break-word; white-space: pre-line;">
@if (!showFullOpReturnData[vindex]) {
{{ (vout.scriptpubkey_asm | hex2ascii | slice:0:1000) }}
} @else {

View file

@ -1,12 +1,18 @@
.col {
&:first-child {
padding-right: 0;
@media (max-width: 992px) {
padding-right: 15px;
}
td:last-child {
padding-right: calc(0.3em + 15px);
}
}
&:last-child {
padding-left: 0;
@media (max-width: 992px) {
padding-left: 15px;
}
td:first-child {
padding-left: calc(0.3em + 15px);
}
@ -324,3 +330,25 @@ h2 {
background-color: var(--primary);
}
}
.scriptmessage.opreturn-msg {
max-width: 20px;
@media (min-width: 476px) {
max-width: 110px;
}
@media (min-width: 576px) {
max-width: 190px;
}
@media (min-width: 768px) {
max-width: 330px;
}
@media (min-width: 850px) {
max-width: 452px;
}
@media (min-width: 992px) {
max-width: 120px;
}
@media (min-width: 1200px) {
max-width: 180px;
}
}

View file

@ -66,6 +66,7 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
showFullScriptPubkeyAsm: { [voutIndex: number]: boolean } = {};
showFullScriptPubkeyHex: { [voutIndex: number]: boolean } = {};
showFullOpReturnData: { [voutIndex: number]: boolean } = {};
showFullOpReturnPreview: { [voutIndex: number]: boolean } = {};
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();
@ -342,11 +343,11 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
const isScriptSpend = vin.witness.length > (hasAnnex ? 2 : 1);
if (isScriptSpend) {
const controlBlock = hasAnnex ? vin.witness[vin.witness.length - 2] : vin.witness[vin.witness.length - 1];
const scriptHex = hasAnnex ? vin.witness[vin.witness.length - 3] : vin.witness[vin.witness.length - 2];
const scriptHex = hasAnnex ? vin.witness[vin.witness.length - 3] : vin.witness[vin.witness.length - 2]; // bip341 script element
const tapleafVersion = parseInt(controlBlock.slice(0, 2), 16) & 0xfe;
// simplicity script spend
if (tapleafVersion === 0xbe) {
vin.inner_simplicityscript = scriptHex;
vin.inner_simplicityscript = vin.witness[1]; // simplicity program is the second witness element
}
}
}
@ -551,6 +552,10 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
this.showFullOpReturnData[voutIndex] = !this.showFullOpReturnData[voutIndex];
}
toggleShowFullOpReturnPreview(voutIndex: number): void {
this.showFullOpReturnPreview[voutIndex] = !this.showFullOpReturnPreview[voutIndex];
}
toggleOrdData(txid: string, type: 'vin' | 'vout', index: number) {
const tx = this.transactions.find((tx) => tx.txid === txid);
if (!tx) {

View file

@ -170,7 +170,7 @@ export class TreasuriesGraphComponent implements OnInit, OnChanges, OnDestroy {
this.seriesNameToLabel = {};
for (const treasury of this.treasuries) {
this.seriesNameToLabel[treasury.wallet] = treasury.enterprise || treasury.name;
this.seriesNameToLabel[treasury.wallet] = treasury.name || treasury.enterprise || treasury.wallet;
}
const legendData = this.treasuries.map(treasury => ({
@ -301,7 +301,7 @@ export class TreasuriesGraphComponent implements OnInit, OnChanges, OnDestroy {
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${markerColor};"></span>`;
tooltip += `<div style="display: flex; justify-content: space-between;">
<span style="text-align: left; margin-right: 10px;">${marker} ${treasury.enterprise || treasury.name}:</span>
<span style="text-align: left; margin-right: 10px;">${marker} ${treasury.name || treasury.enterprise || treasury.wallet}:</span>
<span style="text-align: right;">${this.formatBTC(balance)}</span>
</div>`;
}

View file

@ -115,7 +115,7 @@ export class TreasuriesPieComponent implements OnChanges {
}[] = this.treasuries.map((treasury, index) => ({
treasury,
id: treasury.wallet,
label: treasury.enterprise || treasury.name,
label: treasury.name || treasury.enterprise || treasury.wallet,
balance: this.walletBalance[treasury.wallet],
share: (this.walletBalance[treasury.wallet] / total) * 100,
color: chartColors[index % chartColors.length],

View file

@ -23,7 +23,7 @@
<span class="position-number">{{i + 1}}</span>
</div>
</td>
<td class="table-cell-name">{{ treasury.enterprise || treasury.name }}</td>
<td class="table-cell-name">{{ treasury.name || treasury.enterprise || treasury.wallet }}</td>
<td class="table-cell-balance">
<app-amount [satoshis]="walletStats[treasury.wallet]?.balance || 0" digitsInfo="1.2-4" [noFiat]="true"></app-amount>
</td>

View file

@ -170,7 +170,7 @@ export class AddressTypeInfo {
this.processScript(new ScriptInfo('inner_witnessscript', scriptHex, v.inner_witnessscript_asm, v.witness, controlBlock, vinIds?.[i]));
} else if (this.network === 'liquid' || this.network === 'liquidtestnet' && tapleafVersion === 0xbe) {
this.simplicity = true;
v.inner_simplicityscript = scriptHex;
v.inner_simplicityscript = v.witness[1];
this.processScript(new ScriptInfo('inner_simplicityscript', scriptHex, null, v.witness, controlBlock, vinIds?.[i]));
}
}

View file

@ -9,7 +9,7 @@ export interface Filter {
txPage?: boolean,
}
export type FilterMode = 'and' | 'or';
export type FilterMode = 'and' | 'or' | 'nor';
export type GradientMode = 'fee' | 'age';

View file

@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faCogs, faDatabase, faExchangeAlt, faInfoCircle,
@ -9,7 +8,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faTimeline,
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle, faLockOpen, faPaperclip, faAddressCard,
faMedal, faBug, faFilePdf } from '@fortawesome/free-solid-svg-icons';
faMedal, faBug, faFilePdf, faPiggyBank, faLayerGroup, faHeart, faCashRegister } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '@components/menu/menu.component';
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
@ -478,5 +477,9 @@ export class SharedModule {
library.addIcons(faAddressCard);
library.addIcons(faBug);
library.addIcons(faFilePdf);
library.addIcons(faPiggyBank);
library.addIcons(faLayerGroup);
library.addIcons(faHeart);
library.addIcons(faCashRegister);
}
}

View file

@ -61,6 +61,12 @@ try {
}
function download(filename, url) {
if (!filename || !url) {
if (verbose) {
console.log('skipping malformed download request: ', filename, url);
}
return;
}
https.get(url, (response) => {
if (response.statusCode < 200 || response.statusCode > 299) {
throw new Error('HTTP Error ' + response.statusCode + ' while fetching \'' + filename + '\'');
@ -122,6 +128,9 @@ function downloadMiningPoolLogos$() {
}
let downloadedCount = 0;
for (const poolLogo of poolLogos) {
if (poolLogo.type !== 'file' || poolLogo.download_url == null) {
continue;
}
if (verbose) {
console.log(`${LOG_TAG} Processing ${poolLogo.name}`);
}
@ -217,6 +226,9 @@ function downloadPromoVideoSubtiles$() {
}
let downloadedCount = 0;
for (const language of videoLanguages) {
if (language.type !== 'file' || language.download_url == null) {
continue;
}
if (verbose) {
console.log(`${LOG_TAG} Processing ${language.name}`);
}
@ -253,7 +265,7 @@ function downloadPromoVideoSubtiles$() {
let download_url = language.download_url;
if (MEMPOOL_CDN) {
download_url = downloadownload_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/subtitles", "mempool.space/resources/promo-video");
download_url = download_url.replace("raw.githubusercontent.com/mempool/mempool-promo/master/subtitles", "mempool.space/resources/promo-video");
}
if (DRY_RUN) {
console.log(`${LOG_TAG} \tDRY_RUN is set, not downloading ${language.name} but we should`);

View file

@ -1,6 +1,6 @@
# Deploying an Enterprise Production Instance
These instructions are for setting up a serious production Mempool website for Bitcoin (mainnet, testnet, signet), Liquid (mainnet, testnet), and Bisq.
These instructions are for setting up a serious production Mempool website for Bitcoin (mainnet, testnet, signet), Liquid (mainnet, testnet).
Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods). Support is only provided to project sponsors through [Mempool Enterprise®](https://mempool.space/enterprise).
@ -38,7 +38,6 @@ nvm 3.62T 1.25T 2.38T - - 2% 34% 1.00x ONLINE
For maximum flexibility of configuration, I recommend separate partitions for each data folder:
```
Filesystem Size Used Avail Capacity Mounted on
nvm/bisq 766G 1.1G 765G 0% /bisq
nvm/bitcoin 766G 648M 765G 0% /bitcoin
nvm/bitcoin/blocks 1.1T 375G 765G 33% /bitcoin/blocks
nvm/bitcoin/chainstate 770G 4.5G 765G 1% /bitcoin/chainstate
@ -72,7 +71,6 @@ nvm/elements/liquidv1 777G 11G 765G 1% /elements/li
nvm/mempool 789G 24G 765G 3% /mempool
nvm/mysql 766G 648M 765G 0% /mysql
tmpfs 1.0G 1.3M 1.0G 0% /var/cache/nginx
tmpfs 3.0G 1.9G 1.1G 63% /bisq/statsnode-data/btc_mainnet/db/json
```
### Build Dependencies
@ -124,10 +122,6 @@ HiddenServiceDir /var/db/tor/mempool
HiddenServicePort 80 127.0.0.1:81
HiddenServiceVersion 3
HiddenServiceDir /var/db/tor/bisq
HiddenServicePort 80 127.0.0.1:82
HiddenServiceVersion 3
HiddenServiceDir /var/db/tor/liquid
HiddenServicePort 80 127.0.0.1:83
HiddenServiceVersion 3
@ -262,15 +256,6 @@ grant all on mempool_liquidtestnet.* to 'mempool_liquidtestnet'@'localhost' iden
```
### Bisq
Build bisq-statsnode normally and run using options like this:
```
./bisq-statsnode --dumpBlockchainData=true --dumpStatistics=true
```
If Bisq is happy, it should dump JSON files for Bisq Markets and BSQ data into `/bisq` for the Mempool backend to use.
### Mempool
After all 3 electrs instances are fully indexed, install your 3 Mempool nodes:

View file

@ -1,10 +1,10 @@
# start elements on reboot
@reboot sleep 5 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
@reboot sleep 5 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
@reboot sleep 15 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
@reboot sleep 25 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
# start electrs on reboot
@reboot sleep 20 ; screen -dmS liquidv1 /elements/electrs/start liquid
@reboot sleep 20 ; screen -dmS liquidtestnet /elements/electrs/start liquidtestnet
@reboot sleep 35 ; screen -dmS liquidv1 /elements/electrs/start liquid
@reboot sleep 45 ; screen -dmS liquidtestnet /elements/electrs/start liquidtestnet
# hourly asset update and electrs restart
6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs

View file

@ -110,8 +110,6 @@ build_backend()
-e "s!__MEMPOOL_LIQUID_PASS__!${MEMPOOL_LIQUID_PASS}!" \
-e "s!__MEMPOOL_LIQUIDTESTNET_USER__!${MEMPOOL_LIQUIDTESTNET_USER}!" \
-e "s!__MEMPOOL_LIQUIDTESTNET_PASS__!${MEMPOOL_LIQUIDTESTNET_PASS}!" \
-e "s!__MEMPOOL_BISQ_USER__!${MEMPOOL_BISQ_USER}!" \
-e "s!__MEMPOOL_BISQ_PASS__!${MEMPOOL_BISQ_PASS}!" \
"mempool-config.json"
fi
npm install --omit=dev --omit=optional || exit 1

View file

@ -1,38 +0,0 @@
{
"MEMPOOL": {
"OFFICIAL": true,
"NETWORK": "bisq",
"BACKEND": "esplora",
"HTTP_PORT": 8996,
"MINED_BLOCKS_CACHE": 144,
"API_URL_PREFIX": "/api/v1/",
"POLL_RATE_MS": 2000
},
"SYSLOG" : {
"MIN_PRIORITY": "debug"
},
"CORE_RPC": {
"USERNAME": "__BITCOIN_RPC_USER__",
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5000",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet"
},
"DATABASE": {
"ENABLED": false,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"USERNAME": "__MEMPOOL_BISQ_USER__",
"PASSWORD": "__MEMPOOL_BISQ_PASS__",
"DATABASE": "mempool_bisq"
},
"STATISTICS": {
"ENABLED": true,
"TX_PER_SECOND_SAMPLE_PERIOD": 150
},
"BISQ": {
"ENABLED": true,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
}
}

View file

@ -4,12 +4,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

View file

@ -5,12 +5,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"MEMPOOL_BLOCKS_AMOUNT": 2,
"KEEP_BLOCKS_AMOUNT": 16

View file

@ -8,7 +8,6 @@
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,

View file

@ -4,12 +4,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

View file

@ -4,12 +4,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

View file

@ -4,12 +4,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

View file

@ -4,12 +4,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

View file

@ -4,12 +4,9 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": true,
"LIQUID_TESTNET_ENABLED": true,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"ITEMS_PER_PAGE": 25,
"LIGHTNING": true,
"ACCELERATOR": true,

View file

@ -1,7 +1,5 @@
/var/log/nginx/access.log www:www 644 10 * @T00 C /var/run/mempool.pid 30
/var/log/nginx/error.log www:www 644 10 * @T00 C /var/run/mempool.pid 30
/var/log/nginx/bisq-access.log www:www 644 10 * @T00 C /var/run/mempool.pid 30
/var/log/nginx/bisq-error.log www:www 644 10 * @T00 C /var/run/mempool.pid 30
/var/log/nginx/liquid-access.log www:www 644 10 * @T00 C /var/run/mempool.pid 30
/var/log/nginx/liquid-error.log www:www 644 10 * @T00 C /var/run/mempool.pid 30
/var/log/nginx/mempool-access.log www:www 644 10 * @T00 C /var/run/mempool.pid 30

View file

@ -13,13 +13,6 @@ location = /liquidtestnet {
return 308;
}
# redirect mempool.space/bisq to bisq.markets
location = /bisq {
rewrite /bisq/(.*) https://bisq.markets/$1;
rewrite /bisq https://bisq.markets/;
return 308;
}
# redirect /api to /docs/api
location = /api {
return 308 https://$host/docs/api;

View file

@ -482,9 +482,9 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1233,9 +1233,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dependencies": {
"balanced-match": "^1.0.0"
}
@ -3105,9 +3105,9 @@
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -3663,9 +3663,9 @@
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"requires": {
"balanced-match": "^1.0.0"
}